March Docs

Swim

Swim: the SWIM failure-detection probe loop over a Membership CRDT (P1 L3).

This is the pure protocol state machine: each transition takes the current state + the clock + any injected randomness (the member to probe, the helper set) and returns the next state plus the I/O Actions the runtime layer must perform (send a ping, ask for an indirect ping, gossip a status change). The socket/timer glue that picks random members, sends frames, and fires timers lives in the runtime layer (same split as NetKernel/ClusterConn) — keeping the detection logic deterministic and unit-testable.

One protocol period probes one target:

  1. begin_period(target) -> SendPing(target) (direct ping)
  2. no ack by ack_timeout_ms -> SendPingReq(h, target)… (k-way indirect)
  3. no ack by period end -> mark Suspect + Gossip, arm suspect timer
  4. suspect timer expires -> mark Dead + Gossip

A node refutes a false suspicion by bumping its own incarnation and re-announcing Alive (incarnation-ordered CRDT merge in Membership).

Types

typeConfigConfig = { ack_timeout_ms : Int, suspect_timeout_ms : Int, indirect_k : Int }#

Protocol tunables (durations in ms). suspect_timeout_ms must exceed the worst-case GC/scheduler pause or a paused-but-healthy node is evicted.

typeProbeProbe = { target : String, sent_at : Int, indirect_sent : Bool, acked : Bool }#

The in-flight probe for the current protocol period.

typeStateState = {#
typeActionAction =#

What the runtime I/O layer must perform this transition.

Functions

fnbegin_periodbegin_period(s : State, now : Int, target : String) : (State, Action)#

Begin a protocol period by directly pinging [target] (a random Alive member the caller injects), stamped [now]. Replaces any in-flight probe, without suspecting its target. Returns the new state and SendPing(target).

Examples

let s = Swim.make("me", 0, members, Swim.config(500, 5000, 3))
Swim.begin_period(s, 1000, "n2")
-- (s with probe {target: "n2", sent_at: 1000}, SendPing("n2"))
fnconfigconfig(ack_timeout_ms : Int, suspect_timeout_ms : Int, indirect_k : Int) : Config#

Protocol tunables, durations in ms: [indirect_k] is the number of helpers asked to ping indirectly.

fnend_periodend_period(s : State, now : Int) : (State, List(Action))#

End the protocol period and clear the probe. If the probe was never acked, its target is marked Suspect at its current incarnation (0 if unknown, which also adds it to the view), its suspect timer is armed for now + suspect_timeout_ms, and the Suspect is returned as one Gossip. With no probe, or an acked one, returns Nil actions.

fnescalate_indirectescalate_indirect(s : State, now : Int, helpers : List(String)) : (State, List(Action))#

Escalate an overdue direct ping to k-way indirect ping-req through [helpers] (random Alive members other than the target, injected by the caller). When the probe is unacked, not yet escalated, and at least ack_timeout_ms old at [now], emits one SendPingReq(helper, target) per helper and marks the probe so it escalates at most once. Otherwise (no probe, acked, already escalated, or not yet overdue) returns the state unchanged and Nil. An empty [helpers] still marks the probe escalated.

fnexpire_suspectsexpire_suspects(s : State, now : Int) : (State, List(Action))#

Promote every node whose suspect timer has expired (deadline <= [now]) to Dead at its current incarnation, disarm its timer, and return one Gossip per promotion. Expired timers are visited in unspecified order.

fnmakemake(me : String, incarnation : Int, members : Members, config : Config) : State#

A fresh SWIM state for node [me] at [incarnation] over the member view [members]: no probe in flight and no suspect timers armed.

fnon_ackon_ack(s : State, from : String) : State#

Record an ack from [from]: when it matches the in-flight probe's target the probe is marked acked (it stays in place until end_period). An ack from any other node, or with no probe in flight, leaves the state unchanged.

fnon_gossipon_gossip(s : State, member : Member) : State#

Fold a gossiped member update into the view (CRDT merge). If the resulting winner for that node is Alive, its pending suspect timer is cancelled, so a higher-incarnation Alive refutes a local Suspect. Produces no actions.

fnrefuterefute(s : State) : (State, Option(Action))#

If our own entry in the view is Suspect or Dead, refute it: bump our incarnation to one above both our own and the accusation's, record ourselves Alive at it, and return the Alive as Some(Gossip(...)). Returns None (state unchanged) when we are Alive or absent from our own view.

fnsuspect_membersuspect_member(s : State, now : Int, id : String) : (State, List(Action))#

Suspect [id] now at its current incarnation and arm its suspect timer (now + suspect_timeout_ms), returning the Suspect as one Gossip. This is what a closed connection means to the node service: a hint, not a verdict. A member that is already Suspect or Dead, an unknown id, or [id] equal to ourselves is left alone (Nil actions).