Clock Discipline & Peer Sync
weave intermediate 6 min read
ELI5
Clock discipline is like a group of musicians tuning to the same A440 before playing: each musician (peer) listens to the group, calculates how far their pitch (clock) drifts, and adjusts. If any musician is more than 5 semitones off (5 ms), they’re flagged as out-of-sync and their notes arrive in the wrong bar.
Technical Deep Dive
ClockStatus State Machine
stateDiagram-v2[*] --> OutOfSync: ClockDiscipline newOutOfSync --> Synchronized: sync_with_peer, abs_offset <= 1000µsOutOfSync --> Drifting: sync_with_peer, 1000 < abs_offset <= 5000µsSynchronized --> Drifting: subsequent sync shows 1000 < abs_offset <= 5000µsDrifting --> Synchronized: sync brings abs_offset back <= 1000µsDrifting --> OutOfSync: abs_offset > 5000µsSynchronized --> OutOfSync: abs_offset > 5000µsSource: mesh-node/src/convergence.rs lines 44–66.
Sync Sequence
sequenceDiagramparticipant LocalNodeparticipant ClockDisciplineparticipant RemotePeer
RemotePeer->>LocalNode: heartbeat(peer_time_us)LocalNode->>ClockDiscipline: sync_with_peer(peer_id, peer_time_us)ClockDiscipline->>ClockDiscipline: offset = peer_time_us - local_time_us (as i32)ClockDiscipline->>ClockDiscipline: peer_time_us.insert(peer_id, peer_time_us)ClockDiscipline->>ClockDiscipline: offset_us.insert(peer_id, offset)ClockDiscipline->>ClockDiscipline: status = match abs(offset)ClockDiscipline-->>LocalNode: (status updated)LocalNode->>ClockDiscipline: consensus_time()ClockDiscipline-->>LocalNode: median(peer_time_us values)LocalNode->>ClockDiscipline: adjust_local_time(consensus)ClockStatus Thresholds
| Status | Condition | Implication |
|---|---|---|
Synchronized | abs_offset <= 1000 µs (±1 ms) | Peer within Theorem 1 budget |
Drifting | 1000 < abs_offset <= 5000 µs | Warning: delivery guarantee degrades |
OutOfSync | abs_offset > 5000 µs (> ±5 ms) | Peer should be quarantined or excluded from fast path |
Consensus Time Algorithm
consensus_time() computes the median of all peer_time_us values. If no peers have been synced, it returns local_time_us as a fallback. Median is robust to a single Byzantine peer reporting an extreme timestamp.
within_tolerance() Guard
pub fn within_tolerance(&self, tolerance_ms: i32) -> bool { let tolerance_us = tolerance_ms * 1000; self.offset_us.values().all(|&offset| offset.abs() <= tolerance_us)}Default WeaveConfig passes clock_tolerance_ms: 1. A single peer offset of 2 ms would cause within_tolerance(1) to return false, which should trigger message rejection at the LCB layer.
Relationship to Theorem 1
Theorem 1 (≤8 ms P99) is conditional on clock discipline holding within ±1 ms. If clock_tolerance_ms = 1 but actual offsets reach ±5 ms, the effective delivery bound degrades to ±18 ms. The ClockDiscipline struct provides the measurement machinery; enforcement is the caller’s responsibility.
Key Terms
- ClockDiscipline → Per-node clock synchronisation state: tracks offsets from each peer, computes consensus, reports sync status
- ClockStatus → Enum:
Synchronized(±1 ms),Drifting(±5 ms),OutOfSync(> ±5 ms) - offset_us →
BTreeMap<PeerID, i32>of microsecond offsets per peer;> 0means peer is ahead - consensus_time → Median of
peer_time_usvalues; resistant to one Byzantine peer spoofing time - within_tolerance → All-peers guard; returns
falseif any peer’s offset exceeds the configured ms tolerance
Q&A
Q: Why is ClockStatus set on every sync_with_peer call rather than tracking a history?
A: The status reflects the most recent sync sample, not a moving average. This is a design simplification — it means a single bad measurement can flip status. Production deployments should wrap ClockDiscipline with an EMA on the offset, similar to LatencyMetric in network.rs.
Q: What does adjust_local_time() do and is it safe to call repeatedly?
A: It sets local_time_us to the provided value — a hard override. It does not apply gradual slewing (NTP-style adjtime). Calling it with the consensus time on every tick would cause jumps; callers should implement their own slewing logic.
Q: Does ClockDiscipline need one instance per peer or one per node?
A: One per local node. The peer_time_us and offset_us maps hold one entry per remote peer. The single status field reflects the worst sync state across all peers (last-write-wins per sync call).
Examples
From convergence.rs tests (lines 282–305):
sync_with_peer(peer, 1000500): offset = +500 µs →Synchronized✓sync_with_peer(peer, 1003000): offset = +3000 µs →Drifting✓sync_with_peer(peer, 1010000): offset = +10000 µs →OutOfSync✓
For the ±1 ms default tolerance: any production environment achieving ±500 µs (LAN with NTP) stays firmly in Synchronized. A WAN peer with ±3 ms offset triggers Drifting but not rejection unless within_tolerance(1) is called by the application.
neighbors on the map
- LCB DAG State Machine debugging why a message is stuck in the DAG and never delivered to the application
- Spanning Tree Election & Broadcast debugging why the broadcast root keeps changing unexpectedly under topology churn
- Hybrid Logical Clock deciding ordering between concurrent operations
- FNP Lamport Clocks & Causal Ordering understanding causal ordering in distributed systems
- LORE Causality Graph & Decision Visualization understanding how decisions relate to each other