Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json
T
Joseph Doherty 1b2c36eb87 fix(historian): capture while the recording Primary is unreachable (#672)
A SIGKILLed Primary stays an unreachable-but-not-downed member until
auto-down, and the redundancy singleton usually dies with it, so the
standby's cached snapshot kept naming it Primary and nobody captured for
~30 s. DriverHostActor now holds its own ReachabilityEvent (+MemberUp)
subscription, caches the last snapshot, and excludes unreachable peers
from the historization peer-is-Primary predicate only, republishing just
the capture verdict on a reachability change. Alarm drain and the Primary
data-plane verdict are unchanged. Gated by
ContinuousHistorization:CaptureWhilePrimaryUnreachable (default true).

Tests: message-driven cases with real ReachabilityEvents (incl. a
falsification control on the drain/data-plane verdicts) plus a real
two-node cluster crash test with downing disabled.
2026-10-11 08:26:39 -04:00

85 lines
7.4 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
},
"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"
}
}