6c7e150a32
Unreplicated, the standby holds no deployed configs, data connections or S&F buffer: a seeded active kill showed the survivor take over the cluster in 28 s but load no instances and run no scripts. Dev-only sync key. Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
110 lines
4.5 KiB
JSON
110 lines
4.5 KiB
JSON
{
|
|
"ScadaBridge": {
|
|
"Node": {
|
|
"Role": "Site",
|
|
"NodeName": "node-a",
|
|
"NodeHostname": "scadabridge-env2-site-x-a",
|
|
"SiteId": "site-x",
|
|
"RemotingPort": 8082,
|
|
"GrpcPort": 8083
|
|
},
|
|
"Cluster": {
|
|
"SeedNodes": [
|
|
"akka.tcp://scadabridge@scadabridge-env2-site-x-a:8082",
|
|
"akka.tcp://scadabridge@scadabridge-env2-site-x-b:8082"
|
|
],
|
|
"SplitBrainResolverStrategy": "auto-down",
|
|
"StableAfter": "00:00:15",
|
|
"HeartbeatInterval": "00:00:02",
|
|
"FailureDetectionThreshold": "00:00:10",
|
|
"MinNrOfMembers": 1,
|
|
"_bootstrapGuard": "Deterministic two-node bootstrap for simultaneous container recreation.",
|
|
"BootstrapGuard": {
|
|
"Enabled": true,
|
|
"PartnerProbeSeconds": 25,
|
|
"PartnerProbeIntervalMs": 500,
|
|
"ProbeConnectTimeoutMs": 1000
|
|
}
|
|
},
|
|
"OpcUa": {
|
|
"ApplicationUri": "urn:scadabridge-site-x-node-a:ScadaBridge-DCL",
|
|
"OwnCertificateStorePath": "/app/data/pki/own",
|
|
"TrustedPeerStorePath": "/app/data/pki/trusted",
|
|
"TrustedIssuerStorePath": "/app/data/pki/issuers",
|
|
"RejectedCertificateStorePath": "/app/data/pki/rejected"
|
|
},
|
|
"DataConnection": {
|
|
"ReconnectInterval": "00:00:05",
|
|
"TagResolutionRetryInterval": "00:00:10",
|
|
"WriteTimeout": "00:00:30",
|
|
"SeedReadTimeout": "00:00:30"
|
|
},
|
|
"Communication": {
|
|
// DEV-ONLY control-plane preshared key — NOT a real secret. Must be
|
|
// IDENTICAL on both nodes of the pair and match the central-side entry in
|
|
// ScadaBridge__Communication__SitePsks__<siteId> (docker-compose.yml).
|
|
// Production supplies this as ${secret:SB-GRPC-PSK-<siteId>}. Without it the
|
|
// node fails StartupValidator: the gate is fail-closed, so an unset key would
|
|
// refuse every SiteStream call while the node still looked healthy.
|
|
"GrpcPsk": "dev-grpc-psk-docker-env2-site-x",
|
|
"CentralGrpcEndpoints": [
|
|
"http://scadabridge-env2-central-a:8083",
|
|
"http://scadabridge-env2-central-b: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,
|
|
"_comment": "Retention uses the configured PurgeInterval delay after each purge; no wall-clock cron schedule is supported."
|
|
},
|
|
"Notification": {},
|
|
"Logging": {
|
|
"MinimumLevel": "Information"
|
|
}
|
|
},
|
|
// Consolidated site database (LocalDb Phase 1): OperationTracking + site_events.
|
|
// On the mounted /app/data volume so it survives container recreate - unlike the
|
|
// legacy site-tracking.db / site_events.db, which defaulted to CWD-relative paths
|
|
// outside the volume and were lost on every recreate.
|
|
// Replication is opt-in and configured separately; absent = local-only.
|
|
"LocalDb": {
|
|
"Path": "/app/data/site-localdb.db",
|
|
// site-x is env2's REPLICATED pair (since 2026-11-13, ScadaBridge#116 seeded soak),
|
|
// mirroring docker/ site-a. Without it the standby holds no deployed configs, data
|
|
// connections or S&F buffer, so a failover brings up an EMPTY active node: the
|
|
// cluster recovers but data collection does not.
|
|
//
|
|
// This node is the initiator (dials the peer); the stream is bidirectional. Port
|
|
// 8083 is the existing site gRPC listener. The ApiKey must be IDENTICAL on both
|
|
// nodes (LocalDbSyncAuthInterceptor is fail-closed). Dev-only key, not a secret.
|
|
"Replication": {
|
|
"PeerAddress": "http://scadabridge-env2-site-x-b:8083",
|
|
"ApiKey": "dev-env2-site-x-localdb-sync-key",
|
|
// Same sizing as docker/site-a (see the rationale there).
|
|
"MaxBatchSize": 64,
|
|
"MaxOplogRows": 250000,
|
|
"MaxOplogAge": "2.00:00:00"
|
|
}
|
|
},
|
|
// arch-review remediation WP1.2: the site hot-path audit writer's SQLite file has no
|
|
// default path (mirrors LocalDb:Path above) and StartupValidator now requires it for
|
|
// Site nodes — an unset value used to fall back to a bare "auditlog.db" resolved
|
|
// relative to CWD, i.e. the container's ephemeral overlayfs, so the pending audit
|
|
// backlog was silently discarded on every recreate. On the mounted /app/data volume,
|
|
// same as LocalDb:Path, so it survives container recreate.
|
|
"AuditLog": {
|
|
"SiteWriter": {
|
|
"DatabasePath": "/app/data/auditlog.db"
|
|
}
|
|
}
|
|
}
|