Frozen node returns as a permanent second active after being downed (ThisActorSystemQuarantinedEvent ignored) — auto-down and leader-auto-down #132
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found during the #116 rig drill on 2026-11-12. Evidence:
docs/plans/2026-11-12-leader-auto-down-rig-drill.md§5.What happens. A node that is frozen (
docker pause) for longer than failure detection plus the down window (about 25 s) comes back as a second active node, and it stays that way:The remote system has quarantined this system. It marks the survivor unreachable, becomes leader of its own one-node view, downs the survivor 15 s later, and reports/health/active200.Reproduced on central and on site-a. It also happens on today's default
auto-down, so this is not a regression inLeaderAutoDowningProvider, which is a faithful port. On a site pair, both nodes keep their data connections open and run scripts while in this state, so data is collected twice.Why. Akka.Remote 1.5.62 publishes
ThisActorSystemQuarantinedEventon the event stream (EndpointManager, on anInvalidAssociationwithDisassociateInfo.Quarantined). Akka.Cluster 1.5.62 does not subscribe to it:ClusterCoreDaemonhandles only the oppositeQuarantinedEvent. So nothing tells the quarantined node that it was removed.Option (recommended, needs an owner decision). In
LeaderAutoDowningProvider, subscribe toThisActorSystemQuarantinedEvent. When the quarantining address is a peer in this cluster, log it and down this node (Cluster.Down(SelfAddress)). The existingWhenTerminated → StopApplicationpath then exits the process. The restart policy (dockerunless-stopped, or NSSM on the boxes) brings it back as a new incarnation, and it rejoins as standby. This would apply toleader-auto-downonly. Fit: this is the same "a removed node must not keep acting" rule that Akka JVM and Akka's own SBR follow. It suits the availability-first posture, because it trades an indefinite dual-active for a restart of about 10–30 s.Trade-off to accept. After a real partition heals, both sides have quarantined each other, so both would down themselves and restart. That is a short full-pair outage that heals itself, instead of today's dual-active that waits for an operator. It changes the contract in #116 gate 4 ("both stay 200 until an operator restarts one side") and in the in-process test
PartitionLongerThanWindow_EachSideDownsTheOther_BothStayUp. Alternative: leave the behaviour as it is and document freeze longer than ~25 s as part of the accepted dual-active class, with Overview's existing Split brain banner as the detection.Not blocking the #116 rig gate, because it is parity with the current default.
Fixed:
main@952db02a(code) +482ac665(docs and rig evidence)The owner-approved approach is implemented under
leader-auto-downonly. The shipped default staysauto-down, and no wonder box was touched.What fires (Akka.NET 1.5.62, from the decompiled assemblies, confirmed in-process and on the rig)
Two different signals, depending on who still counts whom as a member:
ThisActorSystemQuarantinedEvent: "the peer has quarantined this node". It is published byEndpointManageron anInvalidAssociationwithDisassociateInfo.Quarantined. The frozen node gets it at unpause (rig log:The remote system has quarantined this system).ClusterCoreDaemonsubscribes only toQuarantinedEvent, which is the root cause.AssociationErrorEventwhose cause reads "The remote system has a UID that has been quarantined" (ProtocolStateActor,ForbiddenUidReason): "the peer still runs the incarnation this node removed". After a healed partition this is the only signal, and both sides get it. Each side aborts the other's handshake on its own refused UID before any quarantine disassociate arrives. So S1 alone would not have fixed the partition case.Identifyto/system/cluster/core/daemon, with a 5 s → 60 s back-off) until that address is a member again. Mutation check: with the probe removed, even the in-process freeze test goes red.Design (
ShutOutRestart, a child of theLeaderAutoDowndowning actor, soLeaderAutoDownBasestays a line-for-line port)ShutOutGrace(60 s), because a frozen peer restarts itself on S1 in the meantime; if still split at the end, tiebreakMember.AddressOrdering: host, then port). The lower node stays and restarts only if the peer has neither rejoined nor gone quiet one more grace later, which covers the single-sided case where the stale side is the lower address.Why the address tiebreak, not age. Both nodes hold both addresses, in every signal and in their seed lists, so they compute the same answer without talking. Up-number is not safe: by the time a signal fires, each side has removed the other, so the peer's up-number exists only as remembered history. That history can differ after an unconverged join or a restart, and a disagreement means both nodes restart or neither does. The lower address is also the Akka leader of the re-formed pair and the bootstrap guard's preferred founder.
Restart reuses the existing self-down path:
Cluster.Down(self)→run-coordinated-shutdown-when-down→ theActorSystemterminates → the WhenTerminated watchdog runsStopApplication, with a directCoordinatedShutdownfallback after 20 s.Supervisor check.
restart: unless-stoppedrestarts on any exit code.sc.exe failure … restart/5000/restart/5000/restart/10000plussc.exe failureflag 1.failureflag 1the SCM restarts a service that stops on its own only if the reported exit code is non-zero. The watchdog used to exit 0. I did not test this on a box, because no wonder-box remote execution is allowed.AkkaHostedService.SelfDownExitCode).Programsets it after the host stops, and the watchdog sets it on the Windows service (ServiceBase.ExitCode) before the service reportsSERVICE_STOPPED. This also covers the existing SBR / auto-down self-down path.Config:
ScadaBridge:Cluster:LeaderAutoDown:RestartWhenShutOut(defaulttrue, takes effect only withleader-auto-down) andShutOutGrace(default60s, validated > 0). Logs use the[leader-auto-down]prefix:shut out by <peer> … tiebreak not needed — this node RESTARTSsplit from <peer> … Giving it 00:01:00Tiebreak says RESTART|STAYrejoined (Up) within the shut-out grace; this node staysTests (all in Release)
ShutOutRestartTests: 18 unit tests (TestKit). They cover the single-side shut-out in both address orders, the tiebreak in both orders, the both-sides case with exactly one restart (S1 in both delivery orders, and S2 as a real healed partition produces it), the grace (stays if the peer goes quiet or rejoins, restarts if still split), duplicate and stranger events, probe back-off, and settings.ShutOutRestartScenarioTests: 4 tests on real two-node clusters with the throttler transport:UnexpectedTerminationTests(exit flag, code ≠ 0),HoconBuilderTests,ClusterOptionsValidatorTests.TemplateEngine ScriptCompilerTests.GetOrAdd_SameCodeDifferentSurface_ComputesSeparateVerdictsfailed once in the parallel run and passed 3/3 alone. TemplateEngine is untouched, so this is a shared-cache flake.CoordinatedShutdownTests.HostSource_DoesNotContainEnvironmentExitcaught anEnvironment.ExitCodemention in a comment; the comment was reworded, and Host.Tests is now 717/717.Rig gates (local docker rig, image built from the fix; record in
docs/plans/2026-11-12-leader-auto-down-rig-drill.md§7, logssor-*.log)DRILL_ACTION=pause, active)shut out … RESTARTSwithin 1 s, exited 75 and rejoined as standby 34 s after unpause (30 s of that was the central host's own shutdown). 10/10 single-active.The rig is back on the default
auto-down, all 8 nodes recreated on the fix image. Every node is ready, each pair has one active node andmemberCount 2 / unreachableCount 0, and the active nodes are central-a, site-a-a, site-b-a and site-c-a.Contract change (#116 gate 4). The dual-active still happens during a partition longer than the window. After the heal it now clears itself in roughly one probe interval (up to 60 s), plus the 60 s grace, plus one restart. Before, it waited for an operator.
LeaderAutoDownScenarioTests.PartitionLongerThanWindow_EachSideDownsTheOther_BothStayUpstill holds, because that test never heals.Residual risks (documented):
ShutOutGraceto shut down after S1, the survivor may also restart. That is a short double restart that still converges.Docs updated:
Component-ClusterInfrastructure.md(Shut-out restart, and the downed-node recovery exit code),docs/components/ClusterInfrastructure.md, the #106 plan (§7 addendum) andCLAUDE.md. The untracked wonderdeploy/…/RUNBOOK.mdwas not changed.Closing: both rig gates pass. The rollout stays on #116.