Every downing decision of LeaderAutoDowningProvider is now logged under a [leader-auto-down] prefix: start-up snapshot (leader, window, already-unreachable), unreachable observed + its window (Warning), skipped because already Down/Exiting, pending Down cancelled on reachable-again/removed, timed out on a non-leader and kept pending, leadership gained/lost, and DOWNING (Warning) before Cluster.Down. Akka's own AutoDown logged only the final Down. Decision semantics are unchanged. New real two-node scenarios (IntegrationTests/Cluster/LeaderAutoDownScenarioTests), built from production BuildHocon with self-first seeds: - both nodes cold-start together -> one cluster, nobody downed past FD + window; - lone cold start with the peer dead (the down-if-alone cold-recreate shape) stays Up, then the peer cold-starts and joins; - flapping peer via Akka's trttl throttler Blackhole: unreachable shorter than the window, repeated past it in total -> never downed; positive control asserts the cuts really produced unreachability; - partition held past the window -> each side downs the other, both stay Up and leader (the accepted dual-active trade); - switch off: auto-down loads AutoDowning, keep-oldest loads SplitBrainResolverProvider, only leader-auto-down loads the in-repo provider. Unit: flapping never downs; a flapping member that finally stays unreachable is downed exactly once a full window after its LAST unreachable; each decision log line. Mutation-proved: no timer cancel on reachable -> flap-then-stay unit fails; reachable ignored -> both unit flapping tests, the cancel-log test and the real-cluster flapping test fail; DOWNING log dropped -> log test fails; provider never downs -> partition test fails; auto-down miswired to the new provider -> switch-off theory fails. Docker failover drill not run (it would rebuild the shared scadabridge:latest image and reuse the shared rig volumes while another agent works in the repo); filed as deploy gate ScadaBridge#116. Default remains auto-down. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
8.9 KiB
Replacing Akka AutoDowning before an Akka.NET upgrade removes it (plan, 2026-11-09)
Status: PLAN ONLY (ScadaBridge#51, register row 70). No config or code changes are made by this document.
Update 2026-11-09 (ScadaBridge#106): option B is implemented as the opt-in strategy leader-auto-down
(LeaderAutoDowningProvider, placed in Host rather than ClusterInfrastructure: the Code Placement rule in
Component-ClusterInfrastructure.md keeps ClusterInfrastructure Akka-free, and the provider is runtime wiring).
DownRemovalMargin matches AutoDowning exactly (akka.cluster.down-removal-margin, default off), not a StableAfter-derived value.
Unit, HOCON, validator and two-node real-cluster tests are green. Remaining gate before switching the default: the
docker failover drill (section 6) on the new value, then the per-environment rollout in section 5.
Update 2026-11-10 (ScadaBridge#106 complete): every downing decision is now logged with a [leader-auto-down]
prefix (start-up snapshot, unreachable observed + window at Warning, skipped Down/Exiting, cancelled on reachable/removed,
timed out on a non-leader → pending, leadership gained/lost, and DOWNING at Warning before Cluster.Down). New
real-cluster scenarios in IntegrationTests/Cluster/LeaderAutoDownScenarioTests.cs: joint cold start, lone cold start
with the peer dead then the peer joining, a flapping peer (Akka trttl throttler blackhole, unreachable shorter than the
window, repeated past it — never downed, with a positive control that the cuts produced unreachability), a partition held
past the window (each side downs the other, both stay Up — the accepted dual-active), and the switch off (auto-down →
AutoDowning, keep-oldest → SplitBrainResolverProvider). The docker drill and rollout are tracked as the deploy gate
ScadaBridge#116.
Related decision: 2026-07-21-auto-down-availability-decision.md.
1. Current state (read from the code)
-
Akka.NET pinned at 1.5.62 (
Directory.Packages.props: Akka, Akka.Cluster, Akka.Cluster.Tools, Akka.Remote). -
AkkaHostedService.BuildHoconalways installs a downing provider explicitly (Akka defaults toNoDowning).ClusterOptions.SplitBrainResolverStrategyis"auto-down"(default) or"keep-oldest":# auto-down (default, every 2-node pair) downing-provider-class = "Akka.Cluster.AutoDowning, Akka.Cluster" auto-down-unreachable-after = <ClusterOptions.StableAfter> # 15s production # keep-oldest (opt-in) downing-provider-class = "Akka.Cluster.SBR.SplitBrainResolverProvider, Akka.Cluster" split-brain-resolver { active-strategy = keep-oldest; stable-after = <StableAfter>; keep-oldest { down-if-alone = on|off } } -
All 16 appsettings (src Host x2,
docker/x8,docker-env2/x4, wonder overlay x2) useauto-down. -
ClusterOptionsValidatoraccepts onlyauto-down|keep-oldest. -
Every node logs on boot that
auto-down-unreachable-afteris deprecated and "keep-majority will be used instead". Drill-verified (2026-08-15, 1.5.62) that the substitution does NOT happen whiledowning-provider-classis explicit.
2. Which Akka.NET version deprecates / removes it
- Deprecated: v1.5.2 (Akka.NET v1.5 upgrade advisory). Behavioural change from 1.5.2: the advisory states that
auto-down-unreachable-afteris deprecated and keep-majority SBR is used instead when the key is present and no explicit provider is set. - Removed: no version announced. The advisory says only "will be removed in a future version". The 1.5.62
Akka.Cluster.xmlstill documentsAkka.Cluster.AutoDowningand carries no removal notice (checked in~/.nuget/packages/akka.cluster/1.5.62). It has survived 60 patch releases since deprecation, so the realistic removal window is a 1.6 / 2.0 line, but this must be re-checked per bump. - Sources: Akka.NET v1.5 Upgrade Advisories, Petabridge SBR post.
3. Candidate replacements (2-node pairs, availability over partition-safety)
Evidence for rejecting every built-in SBR strategy is in the 2026-07-21 decision (live drill + Akka source): keep-oldest (+down-if-alone) downs the survivor when the oldest crashes in a 1-vs-1; keep-majority keeps the lowest-address side (same hole); static-quorum 1 = DownAll, 2 = survivor self-downs; lease-majority needs a shared lease store the sites do not have. None of them gives "either node crashes -> survivor takes over" on two nodes.
| Option | Availability (either node crashes) | Partition behaviour | Effort / risk |
|---|---|---|---|
A. Keep AutoDowning until removed, pin Akka |
Yes | dual-active (accepted) | none now; blocks Akka bumps once removed |
B. In-repo custom DowningProvider reproducing AutoDowning |
Yes (identical) | dual-active (accepted) | small: ~100 lines, public DowningProvider API |
C. SBR keep-oldest, down-if-alone=on |
NO - oldest crash = outage | partition-safe | rejected (proven) |
| D. SBR keep-majority | NO - lowest-address crash = outage | partition-safe | rejected |
| E. Third arbiter node / lease | Yes | partition-safe | needs infra sites do not have |
4. Recommendation
Option B, consistent with the family preference (availability over partition safety for 2-node pairs, accepting dual-active on a real partition). The 2026-07-21 decision already said "if a future Akka.NET release removes AutoDowning, port it then"; this plan makes that concrete:
- Implement
ScadaBridgeDowningProvider : Akka.Cluster.DowningProviderinClusterInfrastructure(DownRemovalMargin=StableAfter-derived,DowningActorProps= a small actor that, on the leader among reachable members, downs members unreachable forstable-after, usingCluster.Down(address); skip when this node is not leader). Behaviour must matchAutoDowningexactly so no operational contract changes. - Add
ClusterOptions.SplitBrainResolverStrategyvalue"leader-auto-down"(name TBD), wire it inBuildHoconwithdowning-provider-class = "<our type>, ZB.MOM.WW.ScadaBridge.ClusterInfrastructure"and noauto-down-unreachable-afterkey, which also removes the boot deprecation warning. Keep"auto-down"working while Akka still ships it; switch the default only after the drills below pass. - If the provider API proves to be removed/unstable in the target Akka version, fallback is a leader-side actor using
Cluster.Downdriven byReachabilityEvent/UnreachableMemberwith a scheduled timer - same semantics.
Do NOT adopt SBR keep-oldest/keep-majority for pairs: it re-introduces the proven total outage on oldest/lowest-address crash.
5. Migration plan
- Trigger: any Akka.NET bump. Read release notes for
AutoDowning/auto-down-unreachable-after/DowningProviderchanges before accepting it. - Build option B on a branch while still on 1.5.62 (so the new provider is proven against the current runtime before any bump).
- Roll out strategy-by-strategy: docker rig, then
docker-env2, then QA pair vq03/vq04, then DEV vd03 and sites. Pairs stop/start together (no rolling restart across a downing-provider change). Flip appsettings value only; rollback = flip back toauto-down. - Update
Component-ClusterInfrastructure.md,docker/README.md, CLAUDE.md, and close register row 70 / this issue once the default is switched.
6. Test plan
- Unit:
HoconBuilderTests- new strategy emits our provider class and noauto-down-unreachable-afterkey; validator accepts it; existingauto-down/keep-oldestemission unchanged. - 2-node TestKit / real-cluster (IntegrationTests/Cluster,
TwoNodeClusterFixturewith itsstrategyparameter): clone the two existingSbrFailoverTestscases for the new strategy:..._HardCrashOfOldestNode_YoungerSurvivorTakesOverSingleton(the direction keep-oldest cannot pass) and..._HardCrashOfYoungerNode_OldestKeepsSingleton; add a "leader is not the unreachable node, not leader -> no Down call" case and a "reachable again before stable-after -> no Down" case. Positive control: run the oldest-crash test againstkeep-oldestand confirm it fails. - Docker rig drill:
DRILL_MODE=active bash docker/failover-drill.sh(kill active/oldestcentral-a; survivor must take over in ~25s; victim rejoins as standby) on central and one site pair, with the new strategy; repeat with the default drill mode for the standby-crash direction. Run the same drill once after every Akka bump regardless of option. - Partition check: confirm the accepted dual-active still occurs and recovers by restarting one side (documented operator path), and that
both nodes then show
/health/active200 until recovery (monitoring contract unchanged). - Gate for the bump PR: new-strategy integration tests green, drill green, no deprecation warning in boot logs.