Edge Config Store: TTLs and Point-in-Time Reads
MediumAI-assisted coding · AI-Assisted Feature Work
Edge Config Store: TTLs and Point-in-Time Reads
You've joined the platform team that owns the edge config store: a small versioned key-value store that CDN edge nodes read feature flags, service URLs and sessions from. Every write is kept as a version, which today only powers the history debugging endpoint. There is one bug ticket and two feature tickets in your queue.
Codebase
| File | What it does |
|---|---|
kvstore/api.py | run(operations) entry point and the CommandRouter that dispatches commands |
kvstore/store.py | KeyValueStore: the behaviour of every command |
kvstore/storage.py | VersionedStorage: per-key histories plus an LRU read cache |
kvstore/history.py | KeyHistory: append-only versions of one key with binary-search lookup |
kvstore/versions.py | Version dataclass (value, write time, tombstone flag, reserved expires_at) |
kvstore/keys.py | Key / value / prefix validation and prefix matching |
kvstore/batch.py | Validation for mset / mget |
kvstore/errors.py | StoreError and error codes |
kvstore/config.py | Limits and cache size |
tests/ | Existing tests (one currently fails) |
How your code is run
run(operations: list[list]) -> list runs the operations in order against a fresh store and returns one result per operation. Every operation is [name, ts, ...args]; ts is an integer number of seconds and never decreases (several operations may share a ts). Errors are returned as "error: <code>" strings, never raised.
| Operation | Arguments after ts | Returns | Errors |
|---|---|---|---|
set | key, value | true | invalid key, invalid value |
get | key | the value, or null if the key is not live | invalid key |
delete | key | true if a live key was deleted, else false (nothing recorded) | invalid key |
scan | prefix | [[key, value], ...] for live keys starting with prefix | invalid key (non-string prefix) |
size | — | number of live keys | |
history | key | every write and delete as [written_ts, value], oldest first (null value = delete) | invalid key |
mset | [[key, value], ...] | true; all-or-nothing (one bad pair writes nothing) | invalid key, invalid value, invalid arguments |
mget | [key, ...] | list of values / null | invalid key, invalid arguments |
- Keys are case-sensitive, non-empty strings of at most 200 characters with no whitespace. Values are strings (empty allowed). Arguments are validated in the order listed.
- A key is live if its latest write is a
set(not a delete). - Multiple writes at the same
tsare applied in operation order; the last one wins. - Unknown operation →
"error: unknown operation". Wrong number of arguments or non-integerts→"error: invalid arguments".
Worked example
[["set", 1, "user:42:name", "Ada"],
["set", 2, "user:7:name", "Grace"],
["set", 3, "flags:dark_mode", "on"],
["get", 4, "user:7:name"],
["delete", 5, "flags:dark_mode"],
["delete", 6, "flags:dark_mode"],
["size", 7],
["history", 8, "flags:dark_mode"]]returns [true, true, true, "Grace", true, false, 2, [[3, "on"], [5, null]]].
Part 1 — scan returns the wrong rows (EDGE-311)
Goal: make scan match its contract.
Edge nodes call scan to load a namespace of config at boot. Two reports from the edge team: "the rows come back in a strange order, so our diff-based sync thinks everything changed", and "scan("user:") returned our legacy User: and USER: keys too".
Rules
scanreturns only live keys that start withprefix, compared case-sensitively. The empty prefix matches every key.- Rows are ordered by key, ascending, using plain string comparison (so
"Zulu" < "alpha").
tests/test_store.py::test_scan_is_ordered_by_key currently fails.
Part 2 — Key expiry (TTL) (KV-88)
Goal: sessions and temporary flags should expire on their own.
| Operation | Arguments after ts | Returns | Errors |
|---|---|---|---|
set | key, value, ttl (new optional 3rd argument) | true | invalid key, invalid value, invalid ttl |
expire | key, ttl | true if the key is live, else false (no effect) | invalid key, invalid ttl |
ttl | key | seconds remaining, -1 if live with no expiry, null if not live | invalid key |
Rules
ttlmust be an integer>= 1(not a boolean), otherwiseinvalid ttland nothing is written.- A key set at
tswithttlis live forts <= t < ts + ttl. Att = ts + ttlit is gone (exclusive boundary). The remaining seconds reported byttlat timetists + ttl - t. setwithout a ttl writes a value that never expires (it replaces any earlier expiry).expiregives a live key the new expirynow + ttl, replacing any earlier one (it may shorten it).- An expired key behaves exactly like a deleted one for
get,mget,scan,size,delete(returnsfalse),expire(returnsfalse) andttl(returnsnull), until it is set again. historyis unchanged: rows are still[written_ts, value];expireand expiry itself add no rows.
Example
[["set", 100, "session:abc", "u-42", 30],
["get", 129, "session:abc"], ["ttl", 129, "session:abc"],
["get", 130, "session:abc"], ["ttl", 130, "session:abc"]]returns [true, "u-42", 1, null, null].
Part 3 — Point-in-time reads (EDGE-340)
Goal: let incident responders ask "what did the edge see at time T?".
| Operation | Arguments after ts | Returns | Errors (in order) |
|---|---|---|---|
get_at | key, at | what get would have returned at time at | invalid key, invalid timestamp |
scan_at | prefix, at | what scan would have returned at time at | invalid key, invalid timestamp |
Rules
atmust be an integer (not a boolean) withat <= ts, otherwiseinvalid timestamp.atmay be earlier than any write (the answer is thennull/[]).- The answer reflects every write made at or before
at(including writes made at exactlyat), deletes, and expiry as ofat: a version that had expired byatyieldsnull; it never falls back to an older value. - An
expireissued at timeEnever changes what a read at a time beforeEreturns. - Historical reads must not change the result of any later
get,scan,sizeorttl. - All Part 1 and Part 2 rules apply to
scan_at/get_at(ordering, case-sensitive prefixes, exclusive expiry).
Example
[["set", 10, "feature:checkout", "v1"],
["set", 20, "feature:checkout", "v2"],
["delete", 30, "feature:checkout"],
["get_at", 40, "feature:checkout", 15], ["get_at", 40, "feature:checkout", 20],
["get_at", 40, "feature:checkout", 35], ["get_at", 40, "feature:checkout", 5]]returns [true, true, true, "v1", "v2", null, null].
Notes
history.pyandbatch.pyare known-good. TheVersion.expires_atfield was added ahead of Part 2 but has never been exercised.- You may use the AI assistant freely; verify what it gives you.
- Later parts must not break earlier ones; hidden tests re-check earlier behaviour, including randomised workloads that mix every command.
Example 1
- Input:
- [["set",1,"user:42:name","Ada"],["set",2,"user:7:name","Grace"],["set",3,"flags:dark_mode","on"],["get",4,"user:7:name"],["delete",5,"flags:dark_mode"],["delete",6,"flags:dark_mode"],["size",7],["history",8,"flags:dark_mode"]]
- Output:
- [true,true,true,"Grace",true,false,2,[[3,"on"],[5,null]]]
- Explanation
- The second delete returns false because the key is no longer live. history lists every write and delete as [ts, value], with null for a delete.
Example 2
- Input:
- [["set",100,"session:abc","u-42",30],["get",129,"session:abc"],["ttl",129,"session:abc"],["get",130,"session:abc"],["ttl",130,"session:abc"]]
- Output:
- [true,"u-42",1,null,null]
- Explanation
- A key set at 100 with ttl 30 is live for 100 ≤ t < 130. At 129 one second remains; at 130 it is gone.
Example 3
- Input:
- [["set",10,"feature:checkout","v1"],["set",20,"feature:checkout","v2"],["delete",30,"feature:checkout"],["get_at",40,"feature:checkout",15],["get_at",40,"feature:checkout",20],["get_at",40,"feature:checkout",35],["get_at",40,"feature:checkout",5]]
- Output:
- [true,true,true,"v1","v2",null,null]
- Explanation
- get_at returns what get would have returned at the given time: v1 at 15, v2 at 20, nothing after the delete at 30, and nothing before the first write.
Guidelines
- Python 3 standard library only
- Keys and values are strings; keys are case-sensitive
- Timestamps are integer seconds and never decrease from one operation to the next
- Every operation returns exactly one value; errors are returned as "error: <code>" strings