Files
ScadaBridge/docker/site-c-node-b/appsettings.Site.json
T
Joseph Doherty cf3bd52f93 feat(cluster): auto-down downing strategy — either-node crash now fails over (owner decision 2026-07-21: availability over partition-safety)
Two-node keep-oldest could NEVER survive a crash of the oldest/active node:
Akka.NET 1.5.62 KeepOldest.OldestDecision only lets down-if-alone rescue a
side with >= 2 members, so the 1-vs-1 survivor takes DownReachable and downs
ITSELF — proven live on the rig ('SBR took decision ... including myself')
before this change. static-quorum(1) is worse (IsTooManyMembers -> DownAll);
keep-majority just re-keys the fatal crash to the lowest address.

SplitBrainResolverStrategy gains 'auto-down' (new default): BuildHocon emits
Akka's AutoDowning provider with auto-down-unreachable-after = StableAfter.
The leader among the REACHABLE members downs the unreachable peer, so the
survivor takes over singletons and /health/active in ~25s regardless of which
node died. Accepted trade (explicit owner decision): a real network partition
runs dual-active until an operator restarts one side. keep-oldest remains
supported; DownIfAlone validation is now scoped to it.

Live drill on the rebuilt rig: active-crash TAKEOVER in 28s (victim still
down; all 7 singletons Younger->Oldest), standby-crash removal 27s with 0
routing blips; victims rejoin as standby in 2s. New real-cluster tests pin
both directions (SbrFailoverTests.AutoDown_*); TwoNodeClusterFixture gains a
strategy knob. All 16 appsettings flipped (src, docker, docker-env2, and the
gitignored wonder-app-vd03 overlay on disk — owner must sync to the host).
Docs: decision record docs/plans/2026-07-21-auto-down-availability-decision.md,
Component-ClusterInfrastructure downing section rewritten, drill + README
reworked (active mode now asserts takeover), deferred-work SBR row resolved.
2026-07-21 10:53:40 -04:00

77 lines
2.7 KiB
JSON

{
"ScadaBridge": {
"Node": {
"Role": "Site",
"NodeName": "node-b",
"NodeHostname": "scadabridge-site-c-b",
"SiteId": "site-c",
"RemotingPort": 8082,
"GrpcPort": 8083,
"MetricsPort": 8084
},
"Cluster": {
"SeedNodes": [
"akka.tcp://scadabridge@scadabridge-site-c-a:8082",
"akka.tcp://scadabridge@scadabridge-site-c-b:8082"
],
"SplitBrainResolverStrategy": "auto-down",
"StableAfter": "00:00:15",
"HeartbeatInterval": "00:00:02",
"FailureDetectionThreshold": "00:00:10",
"MinNrOfMembers": 1
},
"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": "/app/data/scadabridge.db"
},
"DataConnection": {
"ReconnectInterval": "00:00:05",
"TagResolutionRetryInterval": "00:00:10",
"WriteTimeout": "00:00:30",
"SeedReadTimeout": "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": "/app/data/store-and-forward.db"
},
"Communication": {
"CentralContactPoints": [
"akka.tcp://scadabridge@scadabridge-central-a:8081",
"akka.tcp://scadabridge@scadabridge-central-b:8081"
],
"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.
// 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"
}
}