Skip to content

fix(dig-node): sweep modules-cache after reshare-warm promote_into_cache land (#2053) - #172

Merged
MichaelTaylor3d merged 2 commits into
mainfrom
harden/2053-reshare-warm-sweep
Aug 3, 2026
Merged

fix(dig-node): sweep modules-cache after reshare-warm promote_into_cache land (#2053)#172
MichaelTaylor3d merged 2 commits into
mainfrom
harden/2053-reshare-warm-sweep

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

Closes dig_ecosystem#2053. Completes the "every on-demand land path sweeps" <cache>/modules disk-bound invariant (the #1934 / #2041 arc) — the reshare-warm land was the last land site without a tier-aware size-cap sweep.

Why

Surfaced by the #2041 loop-security audit: promote_into_cache (crates/dig-node-core/src/seams/dig_peer/module_reshare.rs) lands a whole capsule into <cache>/modules (atomic write-then-rename) with NO subsequent size-cap sweep, unlike cache_fetch_and_cache (#1934) and the read-path §21 sync (#2041). A remotely-triggered read fires the reshare warm (reader → holder, #1576), so repeated warms could grow the modules cache past the cap with the tier-0 loop idle — the same unbounded-growth class #2041 closed on the read path, still open on this one. Self-driven (the reshare flywheel), so lower severity than the read-path residual, but it left the SPEC's "every on-demand land" claim untrue.

Lock-context audit (the load-bearing step)

The reshare-warm land runs in CapsuleWarmer::warm() — a standalone seam reached via spawn_capsule_warm → a fire-and-forget tokio::spawn. It holds no cache_lock (no Node handle, no guard across the land). So the sweep uses the async Node::evict_modules_if_needed().await (takes cache_lock fresh) — mirroring #2041's sync_module_and_bound. The locked core evict_modules_locked() would evict without serialization here; the async variant under a held lock would deadlock — neither hazard applies since no lock is held. Injected via the existing tier0_live::ModulesCacheEvictor seam (the same NodeModulesEvictor the tier-0 loop uses) — no new, driftable evictor.

Tier: a reshare-warm module is untagged (never mark_tier0_land), so module_tier returns the protected Tier1Demand default (#2015 fail-safe). The sweep sacrifices Tier0Precache inventory first and the just-warmed module survives its own sweep — treated as its real (demand) tier, neither wrongly protected nor wrongly sacrificed. No tier-tag change needed.

What changed

  • module_reshare.rsCapsuleWarmer gains an evictor seam; warm() runs the sweep after a successful Held land.
  • tier0_live.rsModulesCacheEvictor / NodeModulesEvictor exposed pub(crate) + NodeModulesEvictor::new; test-only NoopModulesEvictor.
  • download.rs / peer.rs — wire NodeModulesEvictor::new(node) through wire_capsule_reshare.
  • SPEC.md §3.4 — reshare-warm added to the on-demand land paths + the untagged→Tier1Demand note.
  • Narrowed CapsuleWarmer::new + wire_capsule_reshare pubpub(crate) (both crate-internal only — verified no external consumer; required to inject the crate-private evictor without a private_interfaces clippy error).

How verified (TDD)

  • reshare_warm_land_bounds_modules_cache_evicting_the_tier0_victim (lib.rs) — real Node + real NodeModulesEvictor + mock transport: a real warm lands module A past the cap, the pre-seeded oversized tier-0 victim B is evicted, A survives.
  • a_successful_reshare_warm_land_sweeps_the_modules_cache_once (module_reshare.rs, spy) — exactly one sweep on Held.
  • a_refused_reshare_warm_does_not_sweep_the_modules_cache (module_reshare.rs, negative control) — zero sweeps on refusal.
  • Non-vacuous: disabling the sweep call → the end-to-end + spy tests FAIL (B survives; sweeps≠1); the refusal control still passes. Restored → all pass.
  • cargo test -p dig-node-core --lib656 passed, 0 failed; fmt + clippy -D warnings clean; build OK.

Version

root [workspace.package].version 0.93.3 → 0.93.4 (patch, fix:); Cargo.lock regenerated.


Generated by Claude Code

claude added 2 commits August 3, 2026 15:56
Co-Authored-By: Claude <noreply@anthropic.com>
…che land (#2053)

The read-triggered reshare-warm whole-capsule promotion
(CapsuleWarmer::warm -> promote_into_cache) landed a whole .dig into
<cache>/modules without running the tier-aware size-cap sweep, so the
disk bound held only while the tier-0 precache loop (#1934) or a
read-path sync (#2041) happened to run. This closes the last unbounded
on-demand land path: warm() now runs the sweep after a SUCCESSFUL
(Held) land only.

Lock context: a reshare warm holds NO cache_lock (the warmer is a
standalone seam with no Node guard across the land), so it drives the
async Node::evict_modules_if_needed (takes cache_lock fresh), never the
locked core. The sweep reuses the SAME ModulesCacheEvictor seam the
tier-0 loop uses, so both paths bound the cache through one impl.

A reshare-warm module is untagged -> module_tier returns the protected
Tier1Demand default, so the sweep sacrifices Tier0Precache inventory
first and the just-warmed module survives its own sweep.

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d merged commit 8b2d0bf into main Aug 3, 2026
15 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the harden/2053-reshare-warm-sweep branch August 3, 2026 16:50
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.

2 participants