db-16 step 01 — Logical clocks
Goal
Implement Lamport and vector clocks as first-class types in all three languages, with identical semantics under a small, well-defined API.
Tasks
- Lamport clock. A wrapper over a
u64counter exposing:tick()— bump the counter, return the new value.send() -> u64— equivalent totick: bump, then return the stamp for the outgoing message.recv(incoming: u64)—self = max(self, incoming) + 1.value() -> u64.
- Vector clock. A wrapper over
Map<u32, u64>exposing:tick(self_id)—vc[self_id] += 1.send(self_id) -> Self— bump own counter, return a clone of the full VC (the snapshot that gets stamped onto a message).recv(self_id, incoming: &Self)— pointwisevc[k] = max(vc[k], incoming[k])for every keykinincoming, then bump the receiver's own counter.partial_cmp(other) -> {Less, Equal, Greater, Concurrent}. Pure function over the two maps.
- Wire serialization for the VC. Entries on the wire MUST be
sorted ascending by
node_id. This is non-negotiable — it is the single biggest source of byte-diff bugs across languages.
Acceptance
Inline unit tests in each language:
lamport_tick_monotonic— three ticks produce 1, 2, 3.lamport_recv_jumps—recv(10)after value 3 yields 11.vc_partial_order_less—{0:1}<{0:1, 1:1}.vc_partial_order_concurrent—{0:2, 1:0}and{0:0, 1:2}are concurrent.vc_recv_merges_then_ticks—recv(self=1, {0:5, 1:0})from initial{1:2}yields{0:5, 1:3}.vc_serialize_sorted— the bytes are identical no matter what order entries were inserted in the map.
All six green in Rust, Go, and C++.
Discussion prompts
- Why does
recvbump the receiver's own counter after the merge rather than before? - Why is the "Concurrent" outcome of
partial_cmpnecessary; what goes wrong if you collapse it intoEqualorLess? - For a system with one million nodes, is a map-keyed VC still practical? What data structures replace it (hint: interval tree clocks, dotted version vectors)?