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

  1. Lamport clock. A wrapper over a u64 counter exposing:
    • tick() — bump the counter, return the new value.
    • send() -> u64 — equivalent to tick: bump, then return the stamp for the outgoing message.
    • recv(incoming: u64)self = max(self, incoming) + 1.
    • value() -> u64.
  2. 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) — pointwise vc[k] = max(vc[k], incoming[k]) for every key k in incoming, then bump the receiver's own counter.
    • partial_cmp(other) -> {Less, Equal, Greater, Concurrent}. Pure function over the two maps.
  3. 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_jumpsrecv(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_ticksrecv(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 recv bump the receiver's own counter after the merge rather than before?
  • Why is the "Concurrent" outcome of partial_cmp necessary; what goes wrong if you collapse it into Equal or Less?
  • 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)?