2026-02-12 09:13:50 +00:00
|
|
|
# SWIM Membership Protocol
|
|
|
|
|
|
|
|
|
|
SWIM (Scalable Weakly-consistent Infection-style Membership) handles failure
|
|
|
|
|
detection and membership dissemination. Each node periodically probes a random
|
|
|
|
|
peer; if the peer doesn't respond, indirect probes through relays confirm or
|
|
|
|
|
deny reachability before the node is suspected and eventually declared dead.
|
|
|
|
|
|
|
|
|
|
## Probe Cycle
|
|
|
|
|
|
feat: Docker realization, node binary, docs reorg, and simulation testing
Docker realization (bridging simulation to real TCP):
- NodeDriver (`crates/distribution/src/driver.rs`): bridges DistributedNode
tick loop to TcpTransport with piggyback-extended wire messages
- swactor-node binary (`crates/node/`): CLI node with --listen, --seed,
--dashboard-port, --actors flags
- Dockerfile: multi-stage build (rust:1.93-slim → debian:bookworm-slim)
- Docker integration tests (`tests/docker/`): 5-node cluster with 4
scenarios (convergence, failure detection, actor resolution, rejoin)
- LAN cluster scripts for cross-machine validation
- TCP transport retry-on-stale-connection logic
- /api/distribution REST endpoint on dashboard (feature-gated)
- Piggyback fields (piggyback + from_addr) on Ping/Ack/PingReq messages
Docs reorganization:
- docs/runtime/ — actor-model, runtime, worker-thread, channels
- docs/distribution/ — distribution, swim, kademlia, transport
- docs/diagrams/ — all SVG files
- docs/connectome/ — connectome analysis
- docs/development_history/ — DOCKER_REALIZATION.md, SIMULATION_TESTING.md
- render_docs.sh outputs to docs/diagrams/
- README links updated to new paths
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:11:12 +00:00
|
|
|
See [swim_probe_cycle.svg](../diagrams/swim_probe_cycle.svg) for the full state machine.
|
2026-02-12 09:13:50 +00:00
|
|
|
|
|
|
|
|
The probe cycle is a pure state machine driven by ticks:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
┌──────────────────────────────────────────────────────────────────────┐
|
|
|
|
|
│ ProbePhase │
|
|
|
|
|
│ │
|
|
|
|
|
│ Idle ──probe_interval──► WaitingDirectAck ──timeout──► │
|
|
|
|
|
│ ▲ │ │
|
|
|
|
|
│ │ ack received │
|
|
|
|
|
│ │ │ │
|
|
|
|
|
│ └────────────────────────────┘ │
|
|
|
|
|
│ │
|
|
|
|
|
│ WaitingDirectAck ──timeout──► WaitingIndirectAck ──timeout──► │
|
|
|
|
|
│ │ │
|
|
|
|
|
│ indirect ack │
|
|
|
|
|
│ │ │
|
|
|
|
|
│ back to Idle │
|
|
|
|
|
│ │
|
|
|
|
|
│ WaitingIndirectAck ──timeout──► Suspect(target) + start timer │
|
|
|
|
|
│ │
|
|
|
|
|
│ Suspicion timer ──timeout──► DeclareDead(target) │
|
|
|
|
|
└──────────────────────────────────────────────────────────────────────┘
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Probe targets are selected via round-robin over a shuffled member list. Each
|
|
|
|
|
probe cycle picks one target and sends a direct `Ping`. If no `Ack` arrives
|
|
|
|
|
within `probe_timeout` ticks, indirect `PingReq` messages are sent through
|
|
|
|
|
`k` relay nodes. If no indirect ack arrives either, the target enters the
|
|
|
|
|
Suspect state.
|
|
|
|
|
|
|
|
|
|
A ring buffer of the last 16 probe targets is maintained for dashboard
|
|
|
|
|
visualization.
|
|
|
|
|
|
|
|
|
|
## Member States & Incarnation
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
┌─ MemberState ────────────────────────────────────────────────────────────┐
|
|
|
|
|
│ │
|
|
|
|
|
│ Alive ─────► Suspect ─────► Dead │
|
|
|
|
|
│ │
|
|
|
|
|
│ Priority ordering: Dead (2) > Suspect (1) > Alive (0) │
|
|
|
|
|
│ Within the same incarnation, higher-priority state always wins. │
|
|
|
|
|
│ │
|
|
|
|
|
│ Incarnation number: bumped only by the node itself to refute │
|
|
|
|
|
│ suspicion. Higher incarnation unconditionally overrides any state. │
|
|
|
|
|
│ │
|
|
|
|
|
└──────────────────────────────────────────────────────────────────────────┘
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The membership list is a CRDT with merge semantics:
|
|
|
|
|
1. Higher incarnation wins unconditionally.
|
|
|
|
|
2. Same incarnation: higher-priority state wins (`Dead > Suspect > Alive`).
|
|
|
|
|
3. Lower incarnation is silently ignored.
|
|
|
|
|
|
|
|
|
|
When a node receives a `Suspect` about itself, it bumps its incarnation and
|
|
|
|
|
disseminates an `Alive` update — this is self-refutation.
|
|
|
|
|
|
|
|
|
|
## Dissemination Queue
|
|
|
|
|
|
|
|
|
|
Membership updates are not sent in dedicated messages. Instead, they are
|
|
|
|
|
**piggybacked** onto existing protocol messages (Pings, Acks, PingReqs).
|
|
|
|
|
|
|
|
|
|
Each update has a **transmit budget** of `Lambda * ceil(log2(n))` sends, where
|
|
|
|
|
`Lambda` (default 3) is the dissemination multiplier and `n` is the cluster
|
|
|
|
|
size. The budget ensures logarithmic scaling: a 10-node cluster sends each
|
|
|
|
|
update ~12 times; a 1000-node cluster sends it ~30 times.
|
|
|
|
|
|
|
|
|
|
Updates are sorted by priority when piggybacking, so `Dead` notifications
|
|
|
|
|
propagate faster than routine `Alive` heartbeats.
|
|
|
|
|
|
|
|
|
|
See `crates/distribution/DESIGN_NOTES.md` for the full rationale.
|
|
|
|
|
|
|
|
|
|
## Lifeguard Health Multiplier
|
|
|
|
|
|
|
|
|
|
Inspired by the Hashicorp Lifeguard paper, the `HealthMultiplier` tracks
|
|
|
|
|
the local node's network health:
|
|
|
|
|
|
|
|
|
|
- Each successful ack decreases the health score.
|
|
|
|
|
- Each nack/timeout increases it (capped at `max_health_score`).
|
|
|
|
|
- The score produces a multiplier `1 + score` that stretches probe intervals
|
|
|
|
|
and timeouts.
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
// Dynamic suspicion timeout:
|
|
|
|
|
// clamp(base * ceil(log2(n+1)) * multiplier, min, max)
|
|
|
|
|
pub fn dynamic_suspicion_timeout(&self, cluster_size: usize) -> u64
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This prevents unhealthy nodes from generating false accusations: a node
|
|
|
|
|
that can't reach peers slows its own probing rather than suspecting everyone.
|
|
|
|
|
|
|
|
|
|
## Join Protocol
|
|
|
|
|
|
|
|
|
|
A new node contacts seed addresses with `SendJoinRequest`. The receiving
|
|
|
|
|
node adds the newcomer to its member list, enqueues the event for
|
|
|
|
|
dissemination, and responds with a `SendJoinResponse` containing the full
|
|
|
|
|
membership snapshot. The joiner applies the snapshot, learning about all
|
|
|
|
|
existing cluster members in one round-trip.
|
|
|
|
|
|
|
|
|
|
## Where Things Live
|
|
|
|
|
|
|
|
|
|
| Type | File | Role |
|
|
|
|
|
|------|------|------|
|
|
|
|
|
| `SwimProbe` | `swim/probe.rs` | Probe cycle state machine |
|
|
|
|
|
| `SwimConfig` | `swim/probe.rs` | Protocol tuning knobs |
|
|
|
|
|
| `SwimEvent` / `SwimAction` | `swim/probe.rs` | State machine I/O |
|
|
|
|
|
| `MemberList` | `swim/member_list.rs` | Membership CRDT |
|
|
|
|
|
| `MemberEntry` | `swim/member_list.rs` | Single member record |
|
|
|
|
|
| `SwimNode` | `swim/node.rs` | Integrated SWIM facade |
|
|
|
|
|
| `NodeAction` | `swim/node.rs` | High-level network actions |
|
|
|
|
|
| `DisseminationQueue` | `swim/dissemination.rs` | Piggybacked update queue |
|
|
|
|
|
| `HealthMultiplier` | `swim/lifeguard.rs` | Local health tracking |
|
|
|
|
|
| `LifeguardConfig` | `swim/lifeguard.rs` | Lifeguard tuning knobs |
|
|
|
|
|
| `MemberState` | `types.rs` | `Alive` / `Suspect` / `Dead` enum |
|
|
|
|
|
| `NodeRecord` | `types.rs` | Wire-format membership record |
|