Repository navigation
fix(membership): keep published membership views canonical - #11588
Conversation
A silo which starts against an empty membership table keeps its initial
view, {self: Created}, because the empty read has version MinValue and is
not a successor. If another silo inserts itself before this silo writes
its own entry, the next read lacks the local silo. ProcessMembershipUpdate
published that view as-is, so observers which diff consecutive views
(SiloStatusListenerManager, ClusterMembershipSnapshot.CreateUpdate) saw the
local entry removed and reported (self, Dead). ConsistentRingProvider and
VirtualBucketsRingProvider treat that as a local shutdown and permanently
exclude the silo from its own ring.
Preserve the local Created entry when a view lacks it. The silo has not
written its entry, so its absence says nothing about its status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Separate local lifecycle status from authoritative membership. Reconcile delayed registration evidence under the publication lock and preserve terminal death without synthesizing entries. Builds on the startup-race investigation and regression scenario contributed by nightBaker in dotnet#11581.
There was a problem hiding this comment.
🔵 Needs a closer look
Membership-driven self-termination and routing changes warrant final human review, with passing CI and coverage evidence not supplied.
No open issues 🎉
What changed in this PR
This PR separates canonical cluster membership from local lifecycle status, preventing startup refreshes from falsely marking a silo dead and disrupting reminder routing.
Changes:
- Publish authoritative membership snapshots without synthetic local entries.
- Reconcile delayed registration evidence and row removal while preserving terminal local death.
- Document the contracts and add regression coverage for startup routing, gossip, pruning, and delayed writes.
| File | Description |
|---|---|
test/Orleans.Core.Tests/Membership/MembershipTableManagerTests.cs |
Adds canonical-view, routing, and delayed-write regression tests. |
src/Orleans.Runtime/MembershipService/MembershipTableManager.cs |
Separates published membership from local status and reconciles registration evidence. |
src/Orleans.Runtime/MembershipService/IMembershipManager.cs |
Documents canonical snapshots and local lifecycle status contracts. |
Review effort: Balanced
Code coverage
Report-only conclusion: improved. The current-main baseline is commit Coverage combines every CI test matrix job, including providers, CodeGen, .NET 8/10, Linux, Windows, and macOS, using canonical physical source and branch identities. The comparison remains report-only while normal line and branch variance is calibrated. Coverage details |
Threshold cleanup (MaxDefunctSiloEntries) could prune a Dead row immediately, letting a silo whose Joining insert response was lost retry, find no row, and re-register after being declared dead. Add ClusterMembershipOptions.DefunctSiloMinimumRetention (default 1 hour, well above MaxJoinAttemptTime) and never pass a cleanup cutoff newer than now minus that period.
…tnet#11588) Co-authored-by: yerassylshalabayev <pro100.era@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit ad8435d)
…tnet#11588) Co-authored-by: yerassylshalabayev <pro100.era@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit ad8435d)

Problem
A starting silo can observe
Created -> Dead -> Joining -> Activewhen another silo joins before it writes its own membership entry. The initial syntheticCreatedrow disappears on refresh, and membership consumers interpret that removal as death. Both ring providers treat local termination as permanent, so this can leave the silo routing reminder registrations away from itself.Thanks to @nightBaker for the detailed investigation, reproduction, and PR #11581 which identified this race and its reminder-delivery impact. This PR builds on that work and supersedes its proposed implementation. The original contribution is retained in the commit history, and its regression scenario is extended here.
Solution
Keep canonical membership and local lifecycle status separate:
MinValuesnapshot and local statusCreated.Deadstatus and self-termination when a previously registered silo disappears, while publishing the actual removal.The regression coverage exercises startup notifications, both ring providers' local routing, canonical pre-join cleanup gossip and same-version heartbeat refreshes, removal after registration, and delayed write completion across newer views and same-version pruning.
Rationale
Membership versions describe authoritative cluster state. Keeping an uncommitted local
Createdrow in a published snapshot makes the same version mean different things on different silos. Startup predecessor cleanup can gossip that row before registration; a peer can then reject a same-version authoritative refresh because removing a non-dead row violates the successor rules.This change addresses the source of the false removal by removing synthetic membership entries, including the existing inferred
Deadentry. Local lifecycle reconciliation uses the existing serialized publication boundary and preserves the ring providers' terminal-status contract. All hosts are assumed to run this implementation together.Microsoft Reviewers: Open in CodeFlow