fix(push): stop rescanning the whole cache on every publication

The real cause of the 13.3 replay wall. Every cache publication called
`_make_room`, which listed and stat'ed every entry to total the cache size,
and a push publishes one answer per question -- 22,125 on a WordPress
plugin. Measured on the live replay cache: one scan 0.68 s at 12,238
entries, ~70 minutes spent scanning by then, ~2.3 hours to finish the
evidence pass. The engine was idle while the Python process sat at 83% CPU;
the 13/31/51-minute portions were the quadratic growing.

The scan is now remembered, keyed by the cache root's modification stamp:
our own publications and evictions update the tally in place, and any
change another process makes moves the stamp and forces a rescan, so the
size limit stays exact under concurrent pushes. Lock stripes are created up
front so their first use never invalidates the tally. Eviction order and
lease handling are unchanged.

Controls: 61 publications (one a same-key replacement) take exactly one
scan and the tally equals a fresh scan; a second writer on the same root
forces a rescan and the limit holds.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-23 14:12:47 +02:00
co-authored by Claude Opus 5.5
parent 9b47e2c345
commit 7cf256b2a7
2 changed files with 92 additions and 16 deletions
+36
View File
@@ -146,3 +146,39 @@ def test_abandoned_partial_files_count_toward_size_and_are_evicted(tmp_path):
assert cache.publish(key, b"b" * 1000, {}, budget())
assert not abandoned.exists()
assert sum(p.stat().st_size for p in cache.root.rglob("*") if p.is_file()) <= cache.max_bytes
def _real_usage(cache):
"""The cache's size as a fresh scan sees it: the instrument the remembered tally must agree with."""
return sum(child.lstat().st_size for entry in cache.root.iterdir() if entry.is_dir() for child in entry.iterdir())
def test_publications_reuse_one_scan_and_keep_an_exact_tally(tmp_path, monkeypatch):
"""A push stores one answer per question; rescanning the cache on each made the pass quadratic.
On a WordPress plugin, 12,115 entries in, every new answer cost ~50,000 filesystem calls and the evidence
pass never finished its case. The tally must stay exact, not merely cheap.
"""
cache = ArtifactCache(tmp_path / "cache", max_bytes=10**7)
scans = []
real_scan = cache._scan
monkeypatch.setattr(cache, "_scan", lambda b: scans.append(1) or real_scan(b))
for index in range(60):
assert cache.publish(digest_value(f"answer-{index}"), b"rows" * (index + 1), {"kind": "query"}, budget())
assert cache.publish(digest_value("answer-7"), b"replaced", {"kind": "query"}, budget()) # same key, new size
assert len(scans) == 1, f"{len(scans)} full scans for 61 publications"
assert cache._usage is not None and cache._usage[2] == _real_usage(cache)
def test_another_writer_forces_a_rescan_and_the_limit_holds(tmp_path):
"""The tally is only trusted while nothing else touched the cache; the size limit is never approximate."""
root = tmp_path / "cache"
first = ArtifactCache(root, max_bytes=6000)
second = ArtifactCache(root, max_bytes=6000)
for index in range(4):
assert first.publish(digest_value(f"first-{index}"), b"a" * 1000, {}, budget())
for index in range(4):
assert second.publish(digest_value(f"second-{index}"), b"b" * 1000, {}, budget())
assert first.publish(digest_value("first-late"), b"c" * 1000, {}, budget())
assert _real_usage(first) <= 6000
assert first._usage is not None and first._usage[2] == _real_usage(first)