2e4e41a8f7
Closes WP1.2 of the arch-review remediation plan (finding #2, High): SqliteAuditWriterOptions.DatabasePath defaulted to CWD-relative "auditlog.db", which on the docker rig resolves onto the container's ephemeral overlayfs (not the mounted /app/data volume), silently discarding the pending audit forward-state backlog on every recreate; nothing in docker/ or docker-env2/ overrode it; FlushIntervalMs was validated but never read by the writer loop (one commit per event even at trickle rate); and no PRAGMA synchronous was set (SQLite's FULL default fsyncs every commit). - DatabasePath now has no default (mirrors ZB.MOM.WW.LocalDb's LocalDbOptions.Path) and is required pre-host for Site nodes only, via a new StartupValidator raw-config check (top-level "AuditLog:SiteWriter:DatabasePath", NOT nested under ScadaBridge: AddAuditLog binds that section off the configuration root). SqliteAuditWriterOptionsValidator deliberately does NOT check DatabasePath itself, because AddAuditLog runs its ValidateOnStart on both Central and Site composition roots but only Site nodes ever resolve the writer — checking it there would fail Central's boot too. - All 8 site-node appsettings under docker/ and docker-env2/ now set AuditLog:SiteWriter:DatabasePath to /app/data/auditlog.db (mounted volume, survives container recreate, same convention as LocalDb:Path); the local-dev base appsettings.Site.json sets ./data/auditlog.db to match. - The writer loop now honors FlushIntervalMs: after draining the immediately available burst, it keeps the transaction open (bounded by FlushIntervalMs from the first event) waiting for more trickle-rate events before committing, instead of flushing (and fsyncing) per event. - PRAGMA synchronous = NORMAL on the write connection — audit is best-effort by design (CLAUDE.md: "Audit-write failure NEVER aborts the user-facing action"), so NORMAL's narrower power-loss window is an acceptable trade for far fewer fsyncs; WAL mode still guarantees no corruption. - Tests: StartupValidator site-required/blank/central-exempt cases; writer trickle-load single-transaction coalescing + beyond-interval separate-transaction regression (new FlushCountForTests seam); options-validator doc updates reflecting the moved responsibility. Full suite runs green: AuditLog.Tests 368/368, Host.Tests 480/480. One-time migration note: the existing container-local auditlog.db (wherever it landed under CWD) is abandoned by this change, not migrated — already-forwarded rows are safe centrally (AuditLog is the durable copy), and any still-Pending rows on the abandoned path are lost once. This is the exact bug being fixed, not a new loss: those rows were already living outside the mounted volume and would not have survived the next container recreate regardless. Cross-reference docs/known-issues/2026-07-20-cached-telemetry-drain-hot-loop.md, which this placement bug caused.
94 lines
5.5 KiB
JSON
94 lines
5.5 KiB
JSON
{
|
|
"ScadaBridge": {
|
|
"_nodeName": "Host-018: NodeName stamps SourceNode on AuditLog/Notifications/SiteCalls rows (CLAUDE.md 'Centralized Audit Log' decision) and backs IX_AuditLog_Node_Occurred. Convention: 'node-a'/'node-b' for site nodes, 'central-a'/'central-b' for central nodes. Override per-node in multi-node deployments (the docker per-node configs do this). When left at the default below, single-node dev rows are stamped with 'node-a'; an empty value normalises to a NULL SourceNode.",
|
|
"Node": {
|
|
"Role": "Site",
|
|
"NodeHostname": "localhost",
|
|
"SiteId": "site-a",
|
|
"RemotingPort": 8082,
|
|
"GrpcPort": 8083,
|
|
"MetricsPort": 8084,
|
|
"NodeName": "node-a"
|
|
},
|
|
"Cluster": {
|
|
"_seedNodes": "Host-0xx: second entry is the FUTURE node-b remoting port (8085) for a two-node localhost site. It must be an Akka remoting endpoint — never this node's GrpcPort (8083) or MetricsPort (8084); StartupValidator rejects both.",
|
|
"SeedNodes": [
|
|
"akka.tcp://scadabridge@localhost:8082",
|
|
"akka.tcp://scadabridge@localhost:8085"
|
|
],
|
|
"SplitBrainResolverStrategy": "auto-down",
|
|
"StableAfter": "00:00:15",
|
|
"HeartbeatInterval": "00:00:02",
|
|
"FailureDetectionThreshold": "00:00:10",
|
|
"MinNrOfMembers": 1,
|
|
"_bootstrapGuard": "Gitea #33 simultaneous-cold-start split-brain guard. Dark switch, default off (Akka's config-driven self-first auto-join, unchanged). Enable ONLY where both pair VMs can power up together (shared power/hypervisor event) with no start-order serialization: then the lower host:port founds self-first and the higher probes-then-joins, so the pair converges to one cluster instead of two 1-node clusters. Timings validated > 0 when enabled.",
|
|
"BootstrapGuard": {
|
|
"Enabled": false,
|
|
"PartnerProbeSeconds": 25,
|
|
"PartnerProbeIntervalMs": 500,
|
|
"ProbeConnectTimeoutMs": 1000
|
|
}
|
|
},
|
|
"Database": {
|
|
// Migration-only as of LocalDb Phase 2. The site config tables now live in the
|
|
// consolidated LocalDb database (LocalDb:Path). SiteDbPath is read once at boot to drain
|
|
// a pre-Phase-2 scadabridge.db, and is unused after that - keep it until this node has
|
|
// started once.
|
|
"SiteDbPath": "./data/scadabridge.db"
|
|
},
|
|
"DataConnection": {
|
|
"ReconnectInterval": "00:00:05",
|
|
"TagResolutionRetryInterval": "00:00:10",
|
|
"WriteTimeout": "00:00:30"
|
|
},
|
|
"StoreAndForward": {
|
|
// Migration-only as of LocalDb Phase 2. The store-and-forward buffer now lives in the
|
|
// consolidated LocalDb database (LocalDb:Path) as the replicated sf_messages table.
|
|
// SqliteDbPath is read once at boot by SiteLocalDbLegacyMigrator to drain a pre-Phase-2
|
|
// file, and is unused after that - keep it until this node has started once.
|
|
"SqliteDbPath": "./data/store-and-forward.db"
|
|
},
|
|
"Communication": {
|
|
"_grpcPsk": "REQUIRED on Site nodes (StartupValidator fails the boot without it). The preshared key the gRPC control plane authenticates with: ControlPlaneAuthInterceptor is fail-closed, so an unset key refuses every SiteStream call — live subscriptions, audit pulls, cached-telemetry ingest — while the node still reports healthy. Supply it as ${secret:SB-GRPC-PSK-<siteId>} so the plaintext never sits in this file, and seed the SAME value on central: either as the secret SB-GRPC-PSK-<siteId> in its store, or as ScadaBridge:Communication:SitePsks:<siteId>. BOTH nodes of the pair carry the same key. Distinct from LocalDb:Replication:ApiKey, which authenticates the pair partner, not central — never share the two.",
|
|
"GrpcPsk": "${secret:SB-GRPC-PSK-site-1}",
|
|
"_centralGrpcEndpoints": "gRPC (CentralControlService) is the only site→central transport since the ClusterClient→gRPC migration's Phase 4. Each entry MUST be a central node's gRPC (h2c) endpoint on its CentralGrpcPort (default 8083) — NOT this site's own gRPC port, and NOT via Traefik (HTTP/1 only). The single dev-loopback default below points only at central-a (localhost:8083). In a multi-central deployment add the second central node here (e.g. 'http://central-b-host:8083') so the channel pair can fail over when central-a is down. StartupValidator requires a Site node to list at least one endpoint.",
|
|
"CentralGrpcEndpoints": [
|
|
"http://localhost:8083"
|
|
],
|
|
"DeploymentTimeout": "00:02:00",
|
|
"LifecycleTimeout": "00:00:30",
|
|
"QueryTimeout": "00:00:30",
|
|
"TransportHeartbeatInterval": "00:00:05",
|
|
"TransportFailureThreshold": "00:00:15"
|
|
},
|
|
"HealthMonitoring": {
|
|
"ReportInterval": "00:00:30",
|
|
"OfflineTimeout": "00:01:00"
|
|
},
|
|
"SiteEventLog": {
|
|
"RetentionDays": 30,
|
|
"MaxStorageMb": 1024,
|
|
"PurgeScheduleCron": "0 2 * * *"
|
|
},
|
|
"Notification": {},
|
|
"Logging": {
|
|
"MinimumLevel": "Information"
|
|
}
|
|
},
|
|
// Consolidated site database (LocalDb Phase 1): OperationTracking + site_events.
|
|
// LocalDb:Path is REQUIRED and validated on start - a site node with no value here
|
|
// fails to boot, so every site config must set it.
|
|
"LocalDb": {
|
|
"Path": "./data/site-localdb.db"
|
|
},
|
|
// arch-review remediation WP1.2: the site hot-path audit writer's SQLite file has no
|
|
// default path (mirrors LocalDb:Path above) - AuditLog:SiteWriter:DatabasePath is
|
|
// REQUIRED and validated pre-host on Site nodes (StartupValidator), the same way
|
|
// LocalDb:Path is, so every site config must set it.
|
|
"AuditLog": {
|
|
"SiteWriter": {
|
|
"DatabasePath": "./data/auditlog.db"
|
|
}
|
|
}
|
|
}
|