Skip to content

Cache-server concurrent writers: threaded requests, first-writer-wins verdicts - #89

Merged
davet47 merged 1 commit into
mainfrom
cache-concurrent-writers
Jul 26, 2026
Merged

Cache-server concurrent writers: threaded requests, first-writer-wins verdicts#89
davet47 merged 1 commit into
mainfrom
cache-concurrent-writers

Conversation

@davet47

@davet47 davet47 commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Second item of the v0.5 hosted-store theme (ROADMAP: "Concurrent writers — the cache server is single-threaded-serialised; real teams need atomic verdict/blob writes under concurrency (CAS or equivalent), not politeness").

Threadingpython -m hashloom.cache_server is now a ThreadingHTTPServer:

  • Requests (network IO, JSON parsing, auth) run concurrently; one threading.Lock serialises the single SQLite connection, so every db write stays an atomic single-statement commit — the exact semantics the store's check_same_thread=False comment promises, now enforced by the lock rather than by single-threading. (An explicit lock rather than trusting SQLite serialized mode: sqlite3.threadsafety introspection is unreliable on the 3.10 floor.)
  • daemon_threads = False, so server_close() joins in-flight request threads before serve() closes the store — the shutdown use-after-close that threading would otherwise introduce never exists.
  • Unexpected handler errors now return structured JSON 500s ({"error": {"code": "internal", ...}}) instead of a bare stack trace swallowed by the silenced request log — the _respond discipline, extended to the cache server.

First-writer-wins verdicts — the "CAS or equivalent", resolved by observing that the verification key is the compare:

  • A verdict row is keyed by the verification key (sha256 over contract, impl, test source, toolchain identity, and the transitive dep-hash closure), so two publishes for one key are independent runs of a bit-identical closure — status is always pass (the server rejects the rest) and summaries are empty on green. Last-writer-wins buys nothing.
  • Worse, the old ON CONFLICT DO UPDATE refreshed ran_at (a verdict looking fresher than the run that produced it) and reset stale=0 — which becomes a served-stale-green race the moment cross-graph invalidation (theme item 3) can mark shared rows stale.
  • New CacheStore(SqliteStore) subclass in cache_server.py: ON CONFLICT(key) DO NOTHING. Duplicates are no-ops. Local stores keep their overwrite behavior; the Store protocol and its contract are untouched by construction.

Deferred, documented: WAL journal mode + per-thread connections + bounded busy-retry — the throughput path if a team ever saturates the lock (docs/hosted-store.md item 4 names it).

Tests (4 new in test_remote_store.py, fixtures now exercise CacheStore): deterministic first-writer-wins proof (duplicate leaves summary and ran_at untouched); the soundness pin — a duplicate publish cannot un-stale a row mark_stale just marked; a 16-worker mixed publish/read storm with no 5xx, every key landing exactly once, and one whole winner on the contested key; structured-500 with no Traceback in the body.

Live smoke: 250 parallel requests against the real server process — status codes exactly as designed (100×204, 100×200, 50×403, zero 5xx), 51 rows for 51 keys, one whole winner on the 50-way contested key.

Full suite 243 passed, coverage 90.49% (gate 85), benchmark ≥5x. Wire protocol unchanged — clients need no update.

Next theme items: cross-graph invalidation, then the dependency set in the verification key.

🤖 Generated with Claude Code

…ns verdicts (v0.5 theme, item 2)

ThreadingHTTPServer with one lock-guarded sqlite connection: network IO
and JSON handling run concurrently while db work stays serialised, so
every write remains an atomic single-statement commit. daemon_threads is
False so server_close joins in-flight requests before the store closes.
Verdict publishes are first-writer-wins (CacheStore, ON CONFLICT DO
NOTHING): two publishes for one verification key are re-runs of a
bit-identical closure, so a duplicate must not refresh ran_at (freshness
honesty) or reset stale — closing in advance the race where a duplicate
publish resurrects a row cross-graph invalidation just marked. Handler
errors now return structured JSON 500s, never a bare stack trace. Wire
protocol unchanged; Store protocol and contracts untouched. WAL +
per-thread connections stay the documented throughput follow-up.
@davet47
davet47 merged commit 6369de7 into main Jul 26, 2026
10 checks passed
@davet47
davet47 deleted the cache-concurrent-writers branch July 26, 2026 04:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant