LCB DAG State Machine
weave intermediate 7 min read
ELI5
A DAG state machine is a bucket-sort line in a parcel depot: each parcel has a list of parcels it depends on arriving first. When a dependency clears, the dependent parcel slides into the “ready to dispatch” belt. DAGState is that depot — delivery_queue is the belt.
Technical Deep Dive
State Transitions
stateDiagram-v2[*] --> UnknownUnknown --> Pending: add_message (parents > 0)Unknown --> Ready: add_message (parents = 0)Pending --> Ready: satisfy_dependency() → dependencies_remaining = 0Ready --> Delivered: deliver_message()Delivered --> [*]add_message Path (Algorithm 1, lines 1–10)
sequenceDiagramparticipant Callerparticipant DAGStateparticipant DAGNode
Caller->>DAGState: add_message(msg, received_at)DAGState->>DAGState: check messages.contains_key(msg_id)alt duplicate DAGState-->>Caller: Err("Duplicate message")else new DAGState->>DAGNode: new(msg, received_at) Note right of DAGNode: dependencies_remaining = parents.len() DAGState->>DAGState: messages.insert(msg_id, node) alt parents.len() == 0 DAGState->>DAGState: delivery_queue.push_back(msg_id) end DAGState->>DAGState: lamport_clock = max(lamport_clock, msg.lamport) + 1 DAGState-->>Caller: Ok(())endSource: mesh-node/src/proto.rs lines 148–175.
deliver_message Cascade
When deliver_message(msg_id) is called:
- Node is marked
delivered = true. - Every other node whose
parentscontainsmsg_idcallssatisfy_dependency(). - Any node that reaches
dependencies_remaining == 0is pushed ontodelivery_queue.
This cascade is O(n) in the number of messages currently in the DAG — acceptable for typical collaboration sessions but a performance consideration for very large, deep chains (see weave-005).
Lamport Clock Update Rule
lamport_clock = max(lamport_clock, msg.lamport) + 1Applied inside add_message. The clock is used for causal ordering tiebreaking; it does not replace the physical clock used by convergence.rs for the 8 ms delivery guarantee.
Key API
| Method | Returns | Contract |
|---|---|---|
add_message(msg, ts) | Result<(), String> | Idempotent reject on duplicate |
deliver_message(id) | Result<LCBMessage, String> | Error if not ready |
next_delivery() | Option<LCBMessage> | Pops queue, delivers head |
get_deliverable() | Vec<MsgID> | All nodes with ready_to_deliver() true |
size() / delivered_count() | usize | Diagnostic counters |
Key Terms
- DAGNode → Wrapper around
LCBMessage; tracksreceived_at,deliveredflag, anddependencies_remaining - delivery_queue →
VecDeque<MsgID>; FIFO buffer of messages cleared for application delivery - dependencies_remaining → Counter initialised to
parents.len(); decremented bysatisfy_dependency()each time a parent is delivered - satisfy_dependency → Decrements
dependencies_remaining; called bydeliver_messageon all child nodes of a newly delivered node - Algorithm 1 → The LCB core loop specified in
lib.rsdoc comment;DAGStateis its direct implementation
Q&A
Q: What happens if a parent message never arrives (permanent network partition)?
A: dependencies_remaining stays > 0 indefinitely and the child never enters delivery_queue. LC5 (Liveness, ≤ 300 ms bound) requires that the parent arrives within max_delivery_ms; the transport layer is responsible for triggering a timeout and delivering via alternate route. DAGState itself has no timeout logic — it trusts the caller.
Q: Can next_delivery() return None even when messages exist?
A: Yes — if every message in the DAG has at least one undelivered parent. Callers should check delivery_queue.is_empty() before polling to avoid busy-looping.
Q: Why use BTreeMap instead of HashMap for messages?
A: BTreeMap<MsgID, DAGNode> gives deterministic iteration order (important for the deliver_message cascade loop at lines 194–201), which prevents any ordering-dependent bugs across different runtime environments.
Examples
Three messages arrive in network order B, A, C where A is the root, B depends on A, and C depends on B:
add_message(B)→dependencies_remaining = 1, not queued.add_message(A)→dependencies_remaining = 0, queued immediately.add_message(C)→dependencies_remaining = 1, not queued.next_delivery()pops A, callsdeliver_message(A), cascades to B →dependencies_remaining = 0, B queued.next_delivery()pops B, cascades to C → C queued.next_delivery()pops C.
Result: delivered order A → B → C regardless of arrival order.
neighbors on the map
- LCBMessage & DAGNode Schema adding a new field to LCBMessage and need to update all serialisation sites
- FNP Lamport Clocks & Causal Ordering understanding causal ordering in distributed systems
- LORE Causality Graph & Decision Visualization understanding how decisions relate to each other
- Prompt-DAG Scheduler designing a graph.json for a new repo
- CRDT Operation Message adding a new operation type