3c3513ea3e
Item 1: IHistorianDataSource.MaxRawValuesPerRead (Gateway: learned ceiling - 16 = L; 0 elsewhere, guard-tested). RawPageLimit plans every raw page on top of #692's lookahead: with L > 0 the read (page P + boundary ties k + lookahead) is <= L, P = min(N, L - k - 1) (uncapped: L - k - 1); no room => straight to the tie-cluster over-fetch (never an uncapped or underflowing request). One re-plan + re-read when the read taught a new limit or was refused for crowding pre-window rows (HistoryReadLimitExceededException.PreWindowRows): the page shrinks by them. Over-fetch bound min(MaxTieClusterOverfetch, L - 1); slice size never 0. Review fixes: F1 a window starting at/before the epoch counts AVEVA's synthetic row as pre-window (a ceiling-truncated read was reported complete); F2 the page path disables ReverseRawReader's own re-run (cold end-only ceiling 20/N=3000 cost 3,023 reads, now <= 40); F3 crowded look-back re-read around, not refused; F4 the #687 crowding re-read room at least doubles (up to 8 re-reads), so a cluster of hundreds just before a resume point no longer fails on a default gateway; a crowding refusal that leaves no room is BadTooManyOperations, never a tick-by-tick cursor. Item 2: ServerHistorian:EventReadHorizon (default 00:00:00 = exact, unchanged). When set, every fresh HistoryReadEvents window's earlier bound is raised to now - horizon (both directions, all backends). Lossy by design; documented. Item 3 (native TimeAverage/Total re-check) stays blocked on historiangw#27.
87 lines
7.9 KiB
JSON
87 lines
7.9 KiB
JSON
{
|
|
"Serilog": {
|
|
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
|
|
"WriteTo": [
|
|
{ "Name": "Console" },
|
|
{ "Name": "File", "Args": { "path": "logs/otopcua-.log", "rollingInterval": "Day" } }
|
|
]
|
|
},
|
|
"Security": {
|
|
"Auth": {
|
|
"DisableLogin": false
|
|
}
|
|
},
|
|
"Secrets": {
|
|
"MasterKey": {
|
|
"Source": "Environment",
|
|
"EnvVarName": "ZB_SECRETS_MASTER_KEY"
|
|
},
|
|
"RunMigrationsOnStartup": true,
|
|
"ResolveCacheTtl": "00:00:30",
|
|
"Replication": {
|
|
"_comment": "Peer-to-peer secret replication over the Akka cluster. OPT-IN: Enabled=false keeps the plain local SQLite store on every node. FUNCTIONAL since ZB.MOM.WW.Secrets.Replicator.AkkaDotNet 0.2.2 (0.2.1 fixed the shadowed ISecretReplicator registration, 0.2.2 the hosted-process DI deadlock) and live-gated PASS 4/4 on 2026-08-07 — see docs/plans/2026-08-07-secrets-pair-local-live-gate.md. SCOPE: replication is PAIR-LOCAL. Since mesh Phase 6 each application cluster is its own 2-node Akka mesh, so a secret converges within its pair and nowhere else — seed every pair independently (central via /admin/secrets, site pairs via the secret CLI or a ciphertext-only bundle import). Both nodes of a pair MUST share the same KEK, and re-wraps do not replicate, so 'secret rewrap-all' runs against each node's store individually.",
|
|
"Enabled": false,
|
|
"AnnounceInterval": "00:00:30",
|
|
"ActorName": "zb-secret-replication"
|
|
}
|
|
},
|
|
"ServerHistorian": {
|
|
"_comment": "Server-side HistoryRead backend (the ZB.MOM.WW.HistorianGateway gRPC client). Disabled => NullHistorianDataSource (historized nodes return GoodNoData). The gateway must run RuntimeDb:EventReadsEnabled=true for alarm-history ReadEvents, and the API key must carry historian:read + historian:write + historian:tags:write scopes.",
|
|
"Enabled": false,
|
|
"Endpoint": "",
|
|
"ApiKey": "",
|
|
"_ApiKeyComment": "NEVER commit a real key. Supply via env var ServerHistorian__ApiKey, either as a literal or as a ${secret:otopcua/historian/api-key} token resolved fail-closed by the pre-host secrets expander.",
|
|
"UseTls": true,
|
|
"AllowUntrustedServerCertificate": false,
|
|
"CaCertificatePath": null,
|
|
"CallTimeout": "00:00:30",
|
|
"MaxTieClusterOverfetch": 65536,
|
|
"EventReadHorizon": "00:00:00",
|
|
"_EventReadHorizonComment": "Gitea #694, opt-in. 00:00:00 = no horizon: HistoryReadEvents is exact to each read's window (on the Gateway a reverse/end-only events read proves 'nothing older' with one read down to 1970 - seconds on a large Runtime.dbo.Events). A positive span (e.g. 365.00:00:00) never reads events older than now minus it, in either direction - LOSSY: older events become invisible to HistoryReadEvents."
|
|
},
|
|
"ContinuousHistorization": {
|
|
"_comment": "Continuous historization of driver (non-Galaxy) tag values: a crash-safe FasterLog outbox + recorder draining to the ServerHistorian gateway's WriteLiveValues. Disabled => no recorder is spawned. Requires ServerHistorian to be configured; the gateway connection (endpoint/key/TLS) is sourced from the ServerHistorian section, not here.",
|
|
"Enabled": false,
|
|
"OutboxPath": "",
|
|
"_OutboxPathComment": "Directory holding the FasterLog segment + commit files. Required when Enabled=true. In production set an ABSOLUTE path on durable storage.",
|
|
"CommitMode": "PerEntry",
|
|
"_CommitModeComment": "PerEntry means one fsync per ingest BURST, not per value: the recorder appends a whole burst through AppendBatchAsync, which enqueues every entry and issues a single group commit. Periodic batches commits onto a timer every CommitIntervalMs (bounded loss window).",
|
|
"CommitIntervalMs": 100,
|
|
"DrainBatchSize": 1024,
|
|
"_DrainBatchSizeComment": "Entries peeked + written per drain pass. Was 64 when a pass wrote its per-tag groups sequentially; the peek is an in-memory scan and the pass is now bounded-parallel over DrainConcurrency, so a wide batch is cheap — and 64 let the outbox fall permanently behind during any gateway hiccup.",
|
|
"DrainIntervalSeconds": 2,
|
|
"DrainConcurrency": 8,
|
|
"_DrainConcurrencyComment": "Historian tags a single drain pass writes concurrently (one gateway round-trip per tag group). 1 restores the fully sequential drain. The ack is unchanged: still all-or-nothing after every group reports.",
|
|
"Capacity": 500000,
|
|
"_CapacityComment": "Max un-acked outbox entries before drop-oldest; 0 = unbounded. The Gitea #538 disk backstop for a historian refusing EVERY write — the one case the per-tag quarantine cannot resolve.",
|
|
"MaxWriteAttempts": 5,
|
|
"_MaxWriteAttemptsComment": "Gitea #538: consecutive failed drain passes one tag may accumulate — counted only across passes where OTHER tags wrote successfully — before its queued entries are abandoned so the FIFO head can advance.",
|
|
"MinBackoffSeconds": 1,
|
|
"MaxBackoffSeconds": 30,
|
|
"CaptureWhilePrimaryUnreachable": true,
|
|
"_CaptureWhilePrimaryUnreachableComment": "Gitea #672: a standby stood down for its mesh Primary resumes capture as soon as that Primary is reported UNREACHABLE (not after auto-down) — shrinks a crash-failover capture gap to the failure-detector window, at the cost of short duplicate windows. false = pre-#672."
|
|
},
|
|
"AlarmHistorian": {
|
|
"_comment": "Durable store-and-forward alarm sink. Buffers into the node's consolidated LocalDb (see the LocalDb section) so the queue replicates to the redundant pair peer, and drains alarm events to the ServerHistorian gateway's SendEvent path from whichever node holds the Primary role. The downstream connection (endpoint/key/TLS) is sourced from the ServerHistorian section.",
|
|
"Enabled": false,
|
|
"DrainIntervalSeconds": 5,
|
|
"Capacity": 1000000,
|
|
"DeadLetterRetentionDays": 30
|
|
},
|
|
"LocalDb": {
|
|
"_comment": "Node-local SQLite store caching the deployed-configuration artifact, so a driver node can boot from its last-known-good config when central SQL is unreachable. Registered on driver-role nodes only (see LocalDbRegistration). Storage is unconditional; replication to the redundant pair peer is default-OFF.",
|
|
"Path": "./data/otopcua-localdb.db",
|
|
"_SyncListenPortComment": "Port for the dedicated cleartext-HTTP/2 (h2c) listener serving the passive sync endpoint. 0 = no listener bound and the endpoint is not mapped. Must be a port distinct from the main HTTP listener: prior-knowledge h2c cannot share a cleartext Http1AndHttp2 port.",
|
|
"SyncListenPort": 0,
|
|
"Replication": {
|
|
"_comment": "Empty = passive/off. Set PeerAddress on exactly ONE node of the pair (the stream is bidirectional, so the other side still pushes its own deltas back). ApiKey must be BYTE-IDENTICAL on both nodes — the host interceptor fail-closes, so a typo silently stops convergence rather than degrading to unauthenticated. Supply it via ${secret:...} in production, never committed.",
|
|
"_MaxBatchSizeComment": "Secondary ROW-COUNT cap, and the DB read-page size, since LocalDb 0.2.0. The wire message is bounded by MaxBatchBytes (2 MB default, sized under gRPC's 4 MB cap), so this is no longer the hand-tuned byte proxy it once was. It still matters: OplogStore/SnapshotStreamer materialise a whole read page in memory BEFORE the byte-budget split runs, so keep it at 16 on the pair to bound that transient allocation (artifact chunk rows are ~171 KB base64, so 16 x 171 KB ~= 2.7 MB).",
|
|
"_MaxBatchBytesComment": "Primary cap on one outbound delta/snapshot message: summed serialized entry size, default 2 MB. Leave unset unless the gRPC message limit is retuned. A single row over budget is sent alone rather than stalling the stream."
|
|
}
|
|
},
|
|
"Deployment": {
|
|
"_comment": "R2-11 (05/CONV-2): deploy-gate TagConfig strictness. Warn (default) = non-blocking warnings logged + appended to the deployment result message; Error = a config with a typo'd enum or unparseable TagConfig is rejected at the draft gate. Running servers are untouched; the gate only sees re-deploys.",
|
|
"TagConfigValidationMode": "Warn"
|
|
}
|
|
}
|