Skip to content

atelet: add filecache eviction (EvictUnused) - #1515

Closed
Dmitry Berkovich (dberkov) wants to merge 3 commits into
agent-substrate:mainfrom
dberkov:filecache-evict
Closed

atelet: add filecache eviction (EvictUnused)#1515
Dmitry Berkovich (dberkov) wants to merge 3 commits into
agent-substrate:mainfrom
dberkov:filecache-evict

Conversation

@dberkov

Copy link
Copy Markdown
Collaborator

Third slice of the node-local artifact cache (#690), stacked on #1512 and #1513 — fork branches can't serve as PR bases, so this targets main; review only the top commit (atelet: add filecache eviction). I'll rebase as the stack merges.

Completes the M1 library core with pressure-driven eviction:

  • EvictUnused(ctx, targetBytes, dryRun) — lock-free candidate snapshot, min-age gate, ordering unlinked-first then LRU, and a stop-at-target victim loop. Selection tracks expected bytes; FreedBytes is credited only after each RemoveAll succeeds, so the stats never overreport reclaimed space (a failed removal leaves a .rm-* dir for SweepDebris and shows up in the returned error instead).
  • Two-phase retire (retireEntry): inside the key's singleflight + hitMu exclusive — re-stat, veto if the last-use clock moved (a hit landed) — then rename to a uniquely-suffixed .rm-*. Sharing the singleflight group with fetches means a retire that overlaps an in-flight fetch joins it and skips harmlessly, and a GetFileTo that joins a retire just loops and refetches (the linkRetries loop from atelet: add filecache singleflight retrieval (GetFileTo) #1513).
  • Stats vocabulary: Retired counts namespace removals (irreversible at rename time), FreedBytes physical reclamation, PendingBytes retired-but-still-consumer-linked bytes the kernel frees later, plus per-reason skip counts.
  • Physical deletion runs after all retires, outside hitMu and the singleflight — hits and fetches never wait on a multi-GB RemoveAll; evictMu (which serializes passes) intentionally stays held.

Tests: LRU order + stop-at-target; min-age veto; unlinked preferred over older linked; the core safety property (evicting a linked entry leaves the consumer's file readable, next get refetches); dry run touches nothing; removal-failure accounting (Retired=1, FreedBytes=0, err != nil); direct retireEntry veto and retire→sweep handoff.

🤖 Generated with Claude Code

Introduce cmd/atelet/internal/filecache, the foundation of a node-local
artifact cache: opaque entry keys (content-addressed sha256 and
immutable-URI forms), the entries/tmp on-disk layout, a startup sweep
for crash debris (unfinished fetches, interrupted evictions), and byte
accounting for a GC budget.

Golden snapshot restores download their files per actor with no reuse,
and sandbox-asset fetches race concurrent downloads of the same asset;
this package is the shared cache that will back both paths. Retrieval
(singleflight fetch, atomic publication, hardlink-out) and eviction
build on this skeleton in follow-up changes.
GetFileTo materializes a cached artifact at a destination path via hard
link, fetching it on a miss. Concurrent callers for one key share a
single fetch (singleflight), and the fetch runs detached from the
callers' contexts bounded by the store's fetch timeout, so one canceled
caller never aborts a download other callers are waiting on. There is
no negative caching: a failed fetch reaches every waiting caller and
the next call starts fresh.

A fetch lands in tmp/, must produce a regular file, is made read-only
(0444) so a consumer's in-place write fails loudly instead of
corrupting the shared copy, and is published with one atomic rename. A
hit links out and touches the entry's last-use clock under a shared
lock that eviction will hold exclusively, closing the hit-vs-evict
window. Destinations must not exist and must be absolute paths on the
cache's mount; cross-filesystem destinations fail with a dedicated
error rather than a silent copy.
EvictUnused frees cache space least-recently-used first until a byte
target is met, with a two-phase retire: inside the key's singleflight
and the hit lock, a victim is re-verified (a moved last-use clock or an
in-flight fetch vetoes) and renamed to a .rm-* dir, making it invisible
to lookups; the slow physical deletion runs after all retires, outside
the locks the hot path contends, so hits and fetches never wait on it.

Entries younger than the store's min age are never touched, covering
the window between publication and a consumer's first link. Entries
whose data a consumer still hard-links may be retired but count as
pending rather than freed bytes - the kernel returns that space when
the last consumer link goes - so eviction can only ever cost a
re-download, never break a consumer. FreedBytes is credited per entry
only after its physical removal succeeds; a failed removal leaves the
bytes in a .rm-* dir for the startup sweep and out of the freed count.
@dberkov

Copy link
Copy Markdown
Collaborator Author

Closing in favor of a single consolidated PR carrying the whole M1 library as reviewable commits — the fork-based stacking made per-PR diffs and CI awkward. Replacement PR link to follow.

@dberkov

Copy link
Copy Markdown
Collaborator Author

Consolidated into #1517.

@dberkov
Dmitry Berkovich (dberkov) deleted the filecache-evict branch September 5, 2026 03:15
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