In-Memory Key-Value Store: TTL, Transactions, Snapshots
Build an in-memory key-value store in four stages, each extending the last. The final version supports get/set/delete, per-key TTL against an injected clock, nested BEGIN/ROLLBACK/COMMIT, and snapshot/restore.
1Core store
Implement set(key, value), get(key, default) and delete(key) -> bool (True if the key existed).
2TTL
set accepts an optional ttl in seconds. Expired keys read as missing; ttl(key) returns remaining seconds, None for no expiry, and raises KeyError for a missing key. Time comes from an injected clock so tests are deterministic.
3Nested transactions
Add begin(), rollback() and commit(). Transactions nest: an inner commit merges into its parent only; rollback discards the innermost layer. Deletes inside a transaction must shadow committed values. Commit/rollback with no open transaction raises.
4Snapshot / restore
snapshot() returns a deep copy of live committed state; restore(snap) replaces the store, dropping entries that expired since. Both refuse to run while a transaction is open.
Model each transaction as a write-overlay dict on a stack, with a sentinel for deletes. Reads walk the stack top-down then fall through to committed data, commit is a merge into the parent layer, rollback is a pop. Store absolute expiry times, not TTLs, so overlays and snapshots stay correct as the clock moves.
Try it first.
tests.py · 13 passing
t = [0.0]
kv = KVStore(clock=lambda: t[0])
kv.set("a", 1)
assert kv.get("a") == 1 and kv.get("missing") is None
assert kv.delete("a") is True and kv.delete("a") is False
# TTL
kv.set("s", "tok", ttl=10)
t[0] = 9.0
assert kv.get("s") == "tok" and kv.ttl("s") == 1.0
t[0] = 10.0
assert kv.get("s") is None
try:
kv.ttl("s"); assert False
except KeyError:
pass
# nested transactions
kv.set("x", 1)
kv.begin(); kv.set("x", 2)
kv.begin(); kv.delete("x")
assert kv.get("x") is None
kv.rollback()
assert kv.get("x") == 2
kv.begin(); kv.set("y", 9); kv.commit() # inner commit merges into outer only
assert kv.get("y") == 9
kv.rollback() # outer rollback discards both
assert kv.get("x") == 1 and kv.get("y") is None
kv.begin(); kv.set("x", 5); kv.commit()
assert kv.get("x") == 5
try:
kv.commit(); assert False
except RuntimeError:
pass
# TTL inside a transaction is evaluated at read time
kv.begin(); kv.set("e", 1, ttl=1); t[0] = 11.5
assert kv.get("e") is None
kv.commit()
# snapshot / restore, deep-copied and expiry-aware
kv.set("list", [1, 2]); kv.set("short", 1, ttl=2)
snap = kv.snapshot()
kv.get("list").append(3); kv.delete("x")
assert kv.get("list") == [1, 2, 3]
kv.restore(snap)
assert kv.get("list") == [1, 2] and kv.get("x") == 5 and kv.get("short") == 1
t[0] = 20.0
kv.restore(snap)
assert kv.get("short") is None and kv.get("x") == 5
kv.begin()
try:
kv.snapshot(); assert False
except RuntimeError:
passHow would you make this durable and concurrent? Append-only log of committed writes plus periodic snapshots (Redis AOF + RDB); for concurrency use MVCC (each transaction reads a version, conflicts detected at commit) rather than one global lock; expire actively with a sampled sweep, not only lazily on read.