Vector clocks with siblings.
Compare and merge vector clocks, keep concurrent writes as siblings instead of losing one, and let a write that saw both collapse them.
Two people edit the same shopping cart on two replicas during a partition. Last-write-wins keeps one cart and silently drops the other (Day 4, section 1.2). A Dynamo-style store does better: it tags every version with a vector clock, and when two versions are concurrent it keeps both as siblings until a later write, made by someone who saw both, replaces them (sections 3.1 to 3.3).
A clock is a plain dict from node id to counter, e.g. {"A": 2, "B": 1}. A node that is missing counts as 0, so {"A": 1} and {"A": 1, "B": 0} are the same clock.
Implement two functions and a class:
| Name | Returns |
|---|---|
compare(a, b) |
"before" (a happened before b), "after", "equal" or "concurrent" |
merge(a, b) |
a new clock with the larger counter for every node |
Replica(node_id) |
one replica of a key-value store |
replica.get(key) |
(values, context): every sibling's value in the order stored, and the merge of their clocks. An unknown key gives ([], {}) |
replica.put(key, value, context=None) |
writes on this replica and returns the new version's clock |
replica.receive(key, value, clock) |
applies a version replicated from another node. True if it was kept, False if it was ignored |
replica.versions(key) |
[(value, clock), ...]: what this replica sends to its peers |
Rules the tests check:
putbuilds its clock from thecontextthe client read: copy it and add 1 to this replica's own entry. With no context (None), the base is the merge of every sibling this replica holds, so a blind write replaces them all (the lesson's "fresh write").putandreceivestore a version the same way. If any sibling is"after"or"equal"to the new version, the new one is stale or a duplicate: ignore it. Otherwise drop every sibling that is"before"it, then append it. Concurrent siblings stay.- Never mutate a clock you were given, and don't let callers mutate yours: return copies.
- Keys are independent.
Picking a winner is easy and loses data. Keeping both versions pushes the decision to whoever knows how to merge them: for a cart, the union of items. Amazon's Dynamo paper describes exactly this: "the client must perform the reconciliation" of divergent versions on the next read.
Write your code, then run 3 example tests and 9 hidden tests. Each run is limited to 8 seconds: an infinite loop gets stopped and Python restarts, your code stays.