File: key-value-store/keyvaluestore.py

Date: 2026-06-05

Time: 13:16

key-value-store/keyvaluestore.py

Purpose

This file implements a Dynamo-style distributed key-value store as a single-process simulation. It models the core mechanisms from Amazon's Dynamo paper: consistent hashing with virtual nodes, quorum-based reads/writes (N/W/R), vector clocks for conflict detection, hinted handoff for availability during failures, Merkle trees for anti-entropy synchronization, and gossip-based failure detection. It's a teaching implementation — everything runs in-process with no networking — designed to demonstrate how these mechanisms compose into a coherent system.

Key Components

VectorClock

A logical clock mapping nodeid → counter. Immutable-style API: increment, merge, and prune all return new instances. The partial order is defined by dominates (one clock is strictly ahead) and concurrentwith (neither dominates — a conflict). prune(max_entries) caps clock size by keeping only the highest-counter entries, which is Dynamo's approach to bounding clock growth at the cost of some precision.

VersionedValue

A value tagged with its vector clock, wall-clock timestamp, and a tombstone flag. Tombstones are how deletes work in an eventually-consistent system — you can't just remove the key because replicas that haven't seen the delete would re-introduce it during anti-entropy.

MerkleTree

A binary hash tree over sorted key-value pairs. Built from a dict[str, str] snapshot of a node's data. finddifferences compares two trees — if root hashes match, there are zero differences; otherwise it falls back to a key-by-key comparison. The tree structure itself (stored in tree keyed by (depth, start)) is used for construction but not for incremental diffing — the find_differences method is a brute-force fallback rather than a tree walk.

KVNode

A single storage node. Holds a store mapping keys to lists of VersionedValue (multiple concurrent versions can coexist — this is the "siblings" concept from Dynamo). Key methods:

HintedHandoff

A buffer for writes destined for unavailable nodes. When a target node is down, the coordinator stores a hint. When the node recovers (deliver_hints), the buffered writes are replayed. This is a pure availability mechanism — it means the system can accept writes even when some replicas are down.

KVStore (Coordinator)

The top-level coordinator that ties everything together. Constructor parameters n, w, r are the Dynamo quorum knobs:

Key methods:

Patterns

1. Immutable value objects: VectorClock operations return new instances, avoiding aliasing bugs in the distributed setting.

2. Quorum intersection: The W + R > N constraint (not enforced in code but assumed by callers) guarantees that reads and writes overlap on at least one node, ensuring consistency.

3. Consistent hashing with vnodes: 150 virtual nodes per physical node for balanced load distribution. The ring is rebuilt on addnode/removenode.

4. Tombstone-based deletes: Deletes don't remove data; they write a tombstone marker that dominates the live value during reads.

5. Read repair: Opportunistic consistency fix during reads — stale replicas get updated as a side effect of the get path.

6. Separation of coordinator vs. storage: KVStore handles routing/quorum logic; KVNode handles local storage. localput vs. localput_raw cleanly separates "originating a write" from "accepting a replica write."

Dependencies

Imports: Only stdlib — hashlib (consistent hashing + Merkle hashes), random (gossip target selection), dataclasses.

Imported by: testkeyvalue_store.py — the test suite drives all the scenarios (quorum failures, conflicts, anti-entropy, hinted handoff).

Flow

Write path

1. Client calls KVStore.put(key, value, context)

2. Coordinator computes preference list via consistent hash ring

3. First node in list is the coordinator — it increments the vector clock

4. Writes are sent to N target nodes via localputraw

5. If a target is DOWN, a hint is stored and the write goes to a backup node

6. If fewer than W nodes succeed, an exception is raised

7. Returns the new vector clock (client must pass this back as context on the next write to maintain causality)

Read path

1. Client calls KVStore.get(key)

2. Coordinator reads from R nodes via local_get

3. All versions are collected, tombstones filtered, dominated versions removed

4. Deduplicated by vector clock — concurrent versions are returned as siblings

5. Read repair pushes missing versions to stale replicas

6. Returns list of (value, vector_clock) tuples — multiple entries means a conflict the client must resolve

Failure detection

1. rungossipround advances heartbeats, gossips to random peers

2. Compares heartbeat timestamps against suspecttimeout and downtimeout

3. Nodes transition: ALIVE → SUSPECT → DOWN

Invariants

Error Handling

Errors are minimal and exception-based:

Topics to Explore

Beliefs