atelet: add filecache eviction (EvictUnused) - #1515
Closed
Dmitry Berkovich (dberkov) wants to merge 3 commits into
Closed
atelet: add filecache eviction (EvictUnused)#1515Dmitry Berkovich (dberkov) wants to merge 3 commits into
Dmitry Berkovich (dberkov) wants to merge 3 commits into
Conversation
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.
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. |
Collaborator
Author
|
Consolidated into #1517. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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;FreedBytesis credited only after eachRemoveAllsucceeds, so the stats never overreport reclaimed space (a failed removal leaves a.rm-*dir forSweepDebrisand shows up in the returned error instead).retireEntry): inside the key's singleflight +hitMuexclusive — 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 aGetFileTothat joins a retire just loops and refetches (thelinkRetriesloop from atelet: add filecache singleflight retrieval (GetFileTo) #1513).Retiredcounts namespace removals (irreversible at rename time),FreedBytesphysical reclamation,PendingBytesretired-but-still-consumer-linked bytes the kernel frees later, plus per-reason skip counts.hitMuand the singleflight — hits and fetches never wait on a multi-GBRemoveAll;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); directretireEntryveto and retire→sweep handoff.🤖 Generated with Claude Code