Dead-node reprobe mechanism allows partition-healed nodes to rejoin the cluster automatically. When a reprobe ping reaches a dead-declared node, the piggyback exchange triggers incarnation-bump refutation, transitioning the node back to Alive. Death declarations are re-enqueued fresh before each reprobe to ensure piggyback carries useful membership info. Added SWIM property invariant checks (completeness, accuracy, convergence) as reusable post-condition validators for simulation tests. Investigated 3 flaky MT gossip tests: rewrote convergence_curve_is_monotonic_mt (strict monotonicity invalid under non-atomic MT snapshots), tuned partition_heals_and_converges_mt (reduced nodes, relaxed threshold), documented all_nodes_receive_all_keys_in_ring_1000_mt (stable in isolation). Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
3.2 KiB
Dead-Node Reprobe — Design & Rationale
Problem
When a network partition heals, SWIM nodes on both sides may have declared each
other Dead. The alive_members() filter excludes Dead nodes from probing
targets, so neither side initiates communication — creating a permanent split
even after connectivity is restored.
The existing refutation mechanism (incarnation bump on learning of own death declaration) handles resurrection correctly, but depends on someone telling the dead-declared node about its status. With no probes to dead nodes, nobody does.
Solution: Independent Dead-Node Reprobe Cycle
Added a lightweight reprobe mechanism to SwimProbe that periodically pings
dead nodes. The existing piggyback + refutation mechanism handles the rest:
- Reprober pings dead node with piggybacked "you are Dead(inc=N)"
- Target receives piggyback, sees it's declared Dead → refutes → bumps incarnation
- Target replies with Ack carrying piggybacked "I'm Alive(new_inc)"
- Reprober applies piggyback → target transitions Dead→Alive
Key insight: we re-enqueue the death declaration in the dissemination queue before packing the reprobe ping's piggyback. Without this, the original death declaration's transmit budget would be long exhausted, and the piggyback would carry no useful membership info.
Design Decisions
Why inside SwimProbe (not SwimNode)?
- SwimProbe owns the tick counter, sequence counter, and member list access
- All probe-related logic stays in one place
- The reprobe is a simple independent cycle — doesn't interfere with ProbePhase
Why no new wire messages?
SwimAction::SendPingworks identically for normal probes and reprobes- The ack from a reprobe targets a different sequence than the current probe cycle, so the probe state machine ignores it — but the piggyback is applied at the SwimNode layer before the probe state machine sees the ack
Configuration
dead_reprobe_interval: u64(default: 50 ticks, ~5× probe_interval)- Set to 0 to disable completely
- Existing test configs use 0 to avoid interference with timing
Files Changed
| File | Change |
|---|---|
crates/distribution/src/swim/probe.rs |
dead_reprobe_interval in config, maybe_reprobe_dead() method |
crates/distribution/src/swim/member_list.rs |
Added dead_members() |
crates/distribution/src/swim/node.rs |
Re-enqueue death declaration in translate_probe_actions |
All SwimConfig struct literals |
Added dead_reprobe_interval field |
Edge Cases
- Truly dead nodes: Reprobe ping is lost (no ack), no harm done
- All members dead: Reprobe cycles through them round-robin
- Concurrent reprobe + normal probe: Independent, different sequences
- Dissemination budget: Death re-enqueued fresh each reprobe, not stale
Alternatives Considered
- Dead node grace period + resurrection timer: More complex, adds new state tracking alongside suspicion timers. Rejected for simplicity.
- Periodic re-join via seed nodes: Requires seed node availability, doesn't work when seed is itself dead-declared. Rejected.
- Direct liveness inference from Ping reception: Would require changing
handle_pingto special-case pings from dead nodes. More invasive.