Skip to content

cppa-cursor-browser: lock the summary-cache fingerprint read-compare-write so threaded workers cannot serve a stale summary #154

Description

@clean6378-max-it

Sprint Item

is-7 of 13 — Medium severity — land first Wednesday (PR 1 of 4)

Block Relations

Blocked by
Blocks is-9 (soft) — if this merges first, its entry goes into the same [0.2.1] changelog section rather than needing an amendment
Repo/day Independent of is-1–is-6 (local-ci, Mon–Tue) and is-11–is-13 (CCB, Thu)

Calendar Day

Wednesday, July 29, 2026 (PR 1 of 4) — the fix lands before the release that carries it.

Planned Effort

3 story points (Medium) — sprint item is-7

Problem

Eval Test 13 (Thread Safety) flags services/summary_cache.py as the concurrency gap.

fingerprint_workspace_storage (lines 68-107) reads filesystem mtimes via _file_mtime_ns and a ThreadPoolExecutor without any module-level lock, and every cache accessor performs the fingerprint compare and the disk write as separate, unguarded steps.

list_workspace_projects (services/workspace_listing.py lines 77-99) is the clearest case — three unguarded steps:

  1. compute a fingerprint
  2. call get_cached_projects
  3. rebuild on miss, then call set_cached_projects

The same pattern repeats in:

  • services/workspace_tabs.py (lines 535-551)
  • services/workspace_db.py (lines 448-461)
  • services/workspace_context.py (lines 191-220)

Under the multi-threaded WSGI deployment documented in DEPLOYMENT.md, two concurrent requests can compute different fingerprints for the same logical storage state, and one thread can serve or persist a summary keyed to a stale fingerprint.

This is a live bug, which is why it is scheduled this week while two higher-severity structural findings on this same repository are deferred past July.

Goal

One PR serialising the fingerprint compute → cache read-compare → cache write sequence under a single module-level lock, for all four cache types, with a concurrency regression test.

Scope

Part A — the lock

  • Add _summary_cache_lock = threading.Lock() in services/summary_cache.py, following the pattern in utils/workspace_path.py lines 15-18.

Part B — prefer helpers over scattered acquires

  • Wrap the read-compare-write sequence in new helper functions such as get_or_build_cached_projects, rather than sprinkling acquire/release calls across the four call sites.
  • Guard get_cached_*, set_cached_*, _read_cache_file and _write_cache_file (lines 128-153).

Part C — what the race actually is

_write_cache_file already uses atomic temp-and-replace. The race is fingerprint staleness between compute and compare, not the write. Do not re-solve the write.

Part D — coverage of all four cache types

  • projects
  • composer map
  • invalid-workspace aliases
  • tab summaries

Part E — regression test

  • tests/test_summary_cache_concurrency.py exercising concurrent fingerprint and cache access.

Out of scope

  • The search-pipeline duplication (~70% structural, Medium-High) — deferred past July. It is frozen behind search_index.py importing three private underscore-prefixed helpers out of search.py and must be unpicked in the right order. Do not start it here.
  • assign_composer_workspace's nine keyword-only parameters (Medium-High) — also deferred; API shape, not a defect.

Acceptance Criteria

  • A single module-level lock in services/summary_cache.py serialises fingerprint computation, cache read-compare, and cache write for each cache type: projects, composer map, invalid-workspace aliases, and tab summaries.
  • Concurrent requests under threaded WSGI cannot return a cache hit whose stored fingerprint was computed before a storage mutation visible to the serving thread.
  • A regression test, for example tests/test_summary_cache_concurrency.py, exercises concurrent fingerprint and cache access and passes in CI.
  • The search-pipeline duplication is not refactored in this PR.
  • Tests pass in CI.
  • PR approved by at least 1 reviewer.

Verification

# from the cppa-cursor-browser checkout root
.\.venv\Scripts\Activate.ps1
pytest -q tests/test_summary_cache_concurrency.py
pytest -q
  • Confirm the new test fails against the pre-fix code — a concurrency test that passes either way proves nothing.
  • Run the app under a threaded WSGI server, hit the workspace listing from several clients while mutating workspace storage, and confirm no stale summary is served.

References

  • Sprint: Week 31 (Jul 27–31, 2026) — Chen item is-7 (3 pt, Medium)
  • Week 31 weekly plan — cppa-cursor-browser
  • Week 31 issue descriptions — cppa-cursor-browser, summary-cache fingerprint lock
  • Eval finding: Test 13, Thread Safety
  • Files: services/summary_cache.py:68-107,128-153; services/workspace_listing.py:77-99; services/workspace_tabs.py:535-551; services/workspace_db.py:448-461; services/workspace_context.py:191-220; utils/workspace_path.py:15-18; DEPLOYMENT.md
  • Companion: Wednesday is-8 release workflow, is-9 changelog backfill, is-10 v0.2.1 tag
  • Success definition: "SRI and CSP and proven-safe rendering, typed boundary, programmatic error codes, multi-worker honesty" — v0.2.1

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions