Skip to content

Akka.Cluster: a Down node must never be leader, so it can't remove itself and linger outside the cluster - #8650

Merged
Aaronontheweb merged 2 commits into
akkadotnet:v1.5from
Aaronontheweb:fix/down-member-never-leader-v1.5
Sep 28, 2026
Merged

Aaronontheweb merged 2 commits into
akkadotnet:v1.5from
Aaronontheweb:fix/down-member-never-leader-v1.5

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

close #8651

Problem

MembershipState.LeaderOf is missing a pair of parentheses in the branch that runs when some members are unreachable:

.Where(m => m.Status != MemberStatus.Down && reachability.IsReachable(m.UniqueAddress) || m.UniqueAddress == SelfUniqueAddress)

&& binds tighter than ||, so this reads (not Down && reachable) || self. A node that is Down in its own view still counts as a leader candidate.

That becomes a real failure when every member is Down, for example after SBR's down-all-when-unstable or after a DownAll decision arrives through gossip:

  1. The downed node is the only candidate left, so it becomes leader of its own view.
  2. Unreachable Down members don't block convergence, so LeaderActionsOnConvergence runs.
  3. Other observers have marked this node unreachable, so it is in AllUnreachableOrTerminated. The leader removes it as an unreachable Down member: Leader is removing unreachable node [<self>].
  4. ShutdownSelfWhenDown only acts on Down. Self now reads as the Removed placeholder, so the node never shuts down.
  5. The next local gossip update stamps self as seen while self is no longer in Members. Every outgoing gossip then fails with ArgumentException: Unknown address [<its own address>] in cluster message.

The process keeps running outside the cluster until someone restarts it.

Fix

.Where(m => m.Status != MemberStatus.Down && (reachability.IsReachable(m.UniqueAddress) || m.UniqueAddress == SelfUniqueAddress))

A Down node is never leader, so it never runs leader actions and never removes itself. It stays in Members as Down, and the existing ShutdownSelfWhenDown path shuts it down. The all-reachable branch already filtered out Down members, so this makes both branches agree.

Tests

  • New GossipSpec.A_gossip_must_not_have_Down_self_as_leader_when_some_members_are_unreachable. The existing Down-leader test only covered the all-reachable branch. The new test covers the unreachable branch, and also checks that a self that is not Down still counts as leader when others see it as unreachable.
  • The first commit adds the test alone and fails on current v1.5: Expected State(g1, aDown).Leader to be <null>, but found UniqueAddress: (akka.tcp://sys@a:2552, 0).
  • With the fix, all 369 Akka.Cluster.Tests pass on net10.0.

Notes

…some members are unreachable

MembershipState.LeaderOf lets a Down self count as a leader candidate
whenever the cluster has unreachable members: `&&` binds tighter than
`||`, so the self check skips the Down filter.

Once every member is Down (e.g. SBR down-all-when-unstable), the downed
node becomes leader of its own view, removes itself as an unreachable
member in LeaderActionsOnConvergence, and never shuts down. It then
fails to serialize every outgoing gossip with "Unknown address ... in
cluster message", reporting its own address.

The existing Down-leader test only covers the all-reachable branch.
The new test covers the unreachable branch, and also checks that a
self that is not Down still counts as a leader candidate when other
members see it as unreachable.
…e unreachable

Add the missing parentheses in MembershipState.LeaderOf so the Down
filter applies to self too:

    m.Status != Down && (reachable(m) || m == self)

Before, a Down self stayed a leader candidate. After a down-all
decision it could become leader of its own view, remove itself in
LeaderActionsOnConvergence, and then never shut down, because
ShutdownSelfWhenDown only acts on Down and self now reads as Removed.
@Aaronontheweb

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Aaronontheweb added a commit that referenced this pull request Sep 28, 2026
…self and linger outside the cluster (forward-port of #8650) (#8652)

* Akka.Cluster: add failing test - a Down node must not be leader when some members are unreachable

MembershipState.LeaderOf lets a Down self count as a leader candidate
whenever the cluster has unreachable members: `&&` binds tighter than
`||`, so the self check skips the Down filter.

Once every member is Down (e.g. SBR down-all-when-unstable), the downed
node becomes leader of its own view, removes itself as an unreachable
member in LeaderActionsOnConvergence, and never shuts down. It then
fails to serialize every outgoing gossip with "Unknown address ... in
cluster message", reporting its own address.

The existing Down-leader test only covers the all-reachable branch.
The new test covers the unreachable branch, and also checks that a
self that is not Down still counts as a leader candidate when other
members see it as unreachable.

(cherry picked from commit 18bea7b)

* Akka.Cluster: never pick a Down member as leader when some members are unreachable

Add the missing parentheses in MembershipState.LeaderOf so the Down
filter applies to self too:

    m.Status != Down && (reachable(m) || m == self)

Before, a Down self stayed a leader candidate. After a down-all
decision it could become leader of its own view, remove itself in
LeaderActionsOnConvergence, and then never shut down, because
ShutdownSelfWhenDown only acts on Down and self now reads as Removed.

(cherry picked from commit 378ddb9)
@Aaronontheweb
Aaronontheweb merged commit 749513e into akkadotnet:v1.5 Sep 28, 2026
12 checks passed
@Aaronontheweb
Aaronontheweb deleted the fix/down-member-never-leader-v1.5 branch September 28, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant