Skip to content

0.0.10 review: the cache fixes a pre-release deep review found - #40

Merged
Sunrisepeak merged 4 commits into
mainfrom
review-cache-hardening
Oct 3, 2026
Merged

Sunrisepeak merged 4 commits into
mainfrom
review-cache-hardening

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Owner

Follow-up to #39 (already squash-merged as eaf9ea4, not yet released). A pre-release deep review of the 0.0.10 cache work found four P0 defects that could take a live instance's data or block the session loop, plus their kin. All verified against the code before fixing, all covered by tests, all local gates green.

P0 — fixed

P0-1 A 0.0.10 owner renamed a live 0.0.9 guest's cache aside. rename_dead_instances had no grace branch for a directory without instance.json (only sweep_instances did), so the tick renamed a fresh 0.0.9 leftover on its next 10s pass and the background pass then removed it — the old test asserted exactly this. Rename now gives an undescribed directory the same grace (newest_modified) the sweep does.

P0-2 resetCache deleted the resetting instance's own instance.json — the heartbeat that says the directory is alive; the comment claimed the opposite of what the code did. It survives a reset now, like owner.lease.

P0-3 mcppls.sweepCache and a stale cxxModules/cache ran whole-tree walks on the session's event loop. Minutes of filesystem work (the 64 GiB case) held every request and the lease renewal behind it — a blocked owner loses its lease to a new instance. The sweep is now prepared on the loop (prepare_sweep), run on a thread as plain data (run_prepared_sweep), and answered through a deferred_answer event (the exportBundle F18 pattern); a stale report answers the last snapshot while a report-only pass refreshes it off the loop (S3-5.7-8, S3-5.8-10).

P0-4 An unreadable process identity read as a dead lease owner. owner_gone treated process_identity == nullopt (OpenProcess access-denied on Windows, a failed ps on macOS) as gone, so a live owner's lease could be taken over. Only a definite answer releases a lease early: a different incarnation, or a pid process_alive says is gone; lease_owner_gone is exported and unit-tested.

P1 — fixed

  • single-flight for every cache pass with coalescing (startup/engine-start/command used to race on the same tree)
  • generationStartedAt_ resets when clangd exits (staleCommands was unreachable after any crash)
  • a heartbeat ahead of the clock (NTP step, wake from sleep) is alive within the window
  • parse_bytes consumes its input whole (1.5G silently meant 1G, 12abcG meant 12G); a budget that does not parse is logged
  • cache.maxBytes's summary now says what it does (reporting; eviction answers to cache.totalBytes)
  • staleCommands/CLI --prune count what left the disk (tree_remove), not what they attempted
  • a dry run changes nothing remembered (it wrote lastSweep, so a preview read as a sweep)
  • a multi-root sweep answers the sum over roots (the last root's answer overwrote the others')

P2 — fixed

report refresh renders prompts once from one facts object; enforce_budget walks each workspace once (was two full walks); the card's Open logs opens the logs directory (was the cache root); the receipt reads human sizes (sizeText); ps runs under LC_ALL=C on macOS; KINDS has a static_assert and a bounds guard.

Verification

  • server suites dev + release: pass (new cases: rename grace, future heartbeat, strict parse_bytes, lease_owner_gone)
  • validate.py: 296 rules, 0 failures (new rules S3-5.7-8, S3-5.8-9..12 with evidence)
  • extension: 114 units, E2E 40/0/1 in source and VSIX mode
  • cross builds: x86_64-windows-gnu and aarch64-macos compile
  • during verification the E2E caught one regression of this very PR (a nlohmann brace-initialisation wrapping the prompt facts in an array crashed cxxModules/cache) — fixed before push

Four would-take-live-data defects and their kin, all found by the
pre-release review of PR #39, all fixed with tests:

- the lease tick renamed a 0.0.9 guest's instance directory aside with no
  grace at all (only sweep_instances had the grace check), so a 0.0.10
  owner took a live 0.0.9 guest's cache within a tick; rename_dead_instances
  now gives an undescribed directory the same grace the sweep does (P0-1)
- reset_cache deleted the resetting instance's own instance.json -- the
  heartbeat that says it is alive -- leaving a rebuilding guest reapable
  for up to a renewal interval; the file survives a reset like owner.lease (P0-2)
- mcppls.sweepCache and a stale cxxModules/cache walked the whole cache tree
  on the session's event loop: minutes of filesystem work held every request
  and the lease's own renewal behind it (a blocked owner loses its lease);
  the sweep is prepared on the loop, run on a thread, answered by an event
  (the exportBundle pattern), and a stale report answers from the last
  snapshot while a refresh runs off the loop (S3-5.7-8, S3-5.8-10)
- owner_gone read an unreadable process identity (another user's process on
  Windows, a failed ps on macOS) as a dead owner and let a live lease be
  taken over; only a definite answer -- another incarnation, or a pid the
  platform says is gone -- releases a lease early (P0-4)
- every cache pass, background or interactive, is single-flight with
  coalescing (a crash-looping engine used to start one full pass per restart)
- generationStartedAt_ resets when clangd exits, so staleCommands sweeps
  are possible again after a crash instead of never
- a heartbeat ahead of this clock (NTP step, waking from sleep) is alive
  within the window; parse_bytes consumes its input whole ('1.5G' silently
  meant 1G) and a budget that does not parse is logged, not silent
- what a sweep counts freed is what left the disk (staleCommands counted
  attempted bytes into files; the CLI --prune did the same)
- a dry run changes nothing the server remembers (it used to write
  lastSweep, so a preview read as a sweep that happened)
- a multi-root sweep answers with the sum over its roots (the last root's
  answer used to overwrite the others')
- the card's Open logs link opens the logs directory (it opened the cache
  root); the sweep receipt reads sizes the way a person does; ps(1) runs
  under LC_ALL=C on macOS so a lease reads the same spelling it was written
  with; KINDS carries a static_assert and a bounds guard

Tests: the renamed-aside test now covers fresh-undescribed (kept), stale-
described and past-grace (renamed); a heartbeat in the future is alive
within the window; parse_bytes rejects partial input; lease_owner_gone is
exported and covered; the card test pins the logs argument; the receipt
test pins the human-readable size. Server suites dev and release, validate
(296 rules), 114 extension units, E2E 40/0/1 in source and VSIX mode, and
the windows/macos cross builds all pass.
sweepPendingReportOnly_ never reset, so a coalesced report-only refresh
always retriggered as a removal pass -- safe (every pass refreshes the
report too) but not what the latch said. It starts neutral now and resets
when the batch is consumed.
The real-use check of the verify window: the table's bars were a touch
short, so the bar column read as a strip tucked at the table's end
instead of carrying its share of the width band the footer sets.
14 cells now (was 12) -- the class rows and the total row read as one
even block, the right edge falling where the footer's does.
The person's pick from the verify window: 13 cells for the en table, and
the two languages adapted separately. The cells now come from the columns
the card itself shows -- the labels and the total row's 'bytes / limit'
-- so the en table lands on 13 and the zh one, its words narrower around
the same Latin sizes, runs its bars a cell or two longer: both tables
close on the same right edge as the footer, one scale a card (the rows,
the total row and the preparation line share it, nothing jitters as
numbers change width). Clamped to 12..20 so no card ever renders an
absurd strip. A test holds the property: en 13, zh longer, both tables
within one column of the same band.
@Sunrisepeak
Sunrisepeak merged commit 6893a4e into main Oct 3, 2026
47 checks passed
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