Repository navigation
docs: describe the sandbox asset cache, and assert its size instrument in the metrics suite - #2792
Merged
Dmitry Berkovich (dberkov) merged 1 commit intoOct 19, 2026
Conversation
Dmitry Berkovich (dberkov)
force-pushed
the
asset-cache-docs
branch
5 times, most recently
from
October 18, 2026 03:43
bbc491c to
8a5f426
Compare
Dmitry Berkovich (dberkov)
force-pushed
the
asset-cache-docs
branch
3 times, most recently
from
October 18, 2026 21:35
3211edd to
c6d8c2a
Compare
Dmitry Berkovich (dberkov)
marked this pull request as draft
October 18, 2026 21:39
Dmitry Berkovich (dberkov)
force-pushed
the
asset-cache-docs
branch
2 times, most recently
from
October 18, 2026 21:44
4b02fa4 to
cda1273
Compare
igooch
pushed a commit
to igooch/substrate
that referenced
this pull request
Oct 19, 2026
…g its default runtime (agent-substrate#2756) Step 5 of agent-substrate#2675: the sandbox asset cache gets an eviction pass with a budget, and each worker's default runtime is kept in the cache by links under the worker's own directory. **Commit 1, filecache.** A pass never evicts an entry a consumer still hard-links. A link is how a consumer declares an entry in use; retiring such an entry frees nothing while the link lasts and makes the next caller download a second copy beside it. Linked entries are skipped and counted as `SkippedLinked`, and every retired entry frees its full size, so the stats need no `PendingBytes`. The listing runs lock-free, so the retire step re-checks the links under the hit lock as it re-checks the last-use clock, and a link gained since the listing vetoes the entry even when the hit's best-effort touch did not land. The contract is that eviction never takes what a caller holds and its only cost is a re-download. **Commit 2, atelet.** *Design.* The cache has one protection rule, the link rule above, and both things that must survive a pass use it the same way. An actor protects its runtime through the links in its own directory for as long as it lives on the node. A worker protects its default runtime through links in its own directory for as long as the pod exists. There is no root set the pass consults, no reference count across pods, no prune, and nothing to reconcile after a restart: the directory of a pod still on the node keeps protecting, and the startup sweep that already removes the directories of pods that are gone releases their links with them. Several pods of one pool each hold their own links to the same entries; the filesystem counts them, and the entry becomes evictable when the last pod's directory goes. *Directory layout.* Under each worker pod's directory, `<base path>/workers/<pod UID>/`, which the syncer already owns for the pod's sandbox runtimes file: ``` workers/<pod UID>/ sandbox-runtimes.json default-runtime/ <hash>/ one per default runtime the file advertises, by bundle hash gvisor/runsc … hard links into the cache, made by the staging call Run uses ``` *Lifecycle.* The syncer owns the directories and the prewarmer only fills them. Once a pod's file is written and registered, the syncer makes `default-runtime/` match what the file names as defaults: the directory of a default the file no longer names is removed, which unlinks that runtime, and an empty directory is created for each default that lacks one. Then it hands the prewarmer one item per default, the hash and its directory. The hand-over comes after the build on purpose: a build that fails, as a permanent runsc probe failure does, leaves the file and so the directories as they were, with the previous default still linked and still advertised. The prewarm starts late by only a little: for a gVisor default the build's probe has already fetched the release, so the prewarm is links and the pause image pull, and for a micro-VM default the build takes milliseconds. The prewarm job checks that its directory exists, stages into it with a variant of the staging call that creates no directories, pulls the pause image beside it, and on an error checks again: a directory gone, after a later sync or the pod's removal, means the item was superseded, and it ends without a retry and without recreating anything. A directory the syncer already filled costs a stat per asset, so a re-sync with an unchanged default downloads nothing and leaves the links alone. A failed download retries with the existing backoff and gives up after eight attempts; the directory stays, empty or partial, until the next sync, and a Run placed on that default meanwhile fetches on demand and shares the download. One queue and one worker drain the items in order, so defaults download one at a time across the node and never compete for its bandwidth. *Why the old default is not kept during the download.* By the time the directories change, the file names the new default and the worker has registered it, so the scheduler places nothing new on the old one. Actors already running on it hold their own links, and restores of snapshots taken on it go through the on-demand path like every non-default version. Keeping its links would only hold cache space. *GC loop.* `sandbox_asset_gc.go` adds `--sandbox-asset-cache-gc-period` (5m, 0 disables), `--sandbox-asset-cache-max-bytes` (0 means unbounded: no pass runs, warned at startup) and `--sandbox-asset-cache-gc-dry-run`. A pass that finds the cache over budget asks the store to free down to 90% of it, oldest first. Same shape as the image GC: immediate first pass, serialized passes, panic recovery, shortfall warnings that back off to a periodic reminder. *Metrics.* `ate.sandboxassetcache.size` (observable up/down counter, bytes), `ate.sandboxassetcache.evictions` and `ate.sandboxassetcache.gc.passes` by `ate.sandboxassetcache.gc.outcome` (`complete`, `shortfall`, `skipped`); a pass within budget records nothing, and a dry run records its pass but no evictions. Registry group and entries added, `hack/verify/metrics.sh` passes, rows and a paragraph in `docs/observability.md`. **What a shortfall means.** Everything left is linked by an actor, linked by a worker as its default, younger than the min age, or busy. The size then measures what the node's actors and defaults need, not waste; the budget is too small or the node hosts too many runtimes. **Not in this PR.** A shared GC driver for the image and asset caches, which would also give the image GC the metrics it lacks; filed as a follow-up once this lands. Step 6, the summary doc, which agent-substrate#2792 carries and which will be rewritten for this layout. **Tests.** filecache: a linked file, a partly linked tree and a fully linked tree all survive a pass; unlinked trees still go oldest first; an entry linked between the listing and the retire, without a touch, is vetoed at retire time. atelet: a prewarm links the runtime into its directory and an eviction pass keeps the entry; a second prewarm of the same directory fetches nothing; removing the pod directory makes the entry evictable; a job whose directory is gone before it runs, or whose pod goes while it stages, fetches nothing, recreates nothing, and is not retried; the sync hands the default over only after the build, removes a retired default's directory, keeps the current one and its contents across a re-sync, hands nothing over and touches no directory when the build fails, and the pod's removal takes the directories; the end-to-end informer test asserts the link under the worker's directory; retry, pause-image and attempt-timeout cases as before. GC: target table, flag validation, quiet, skipped, complete and shortfall passes with the metrics checked through a manual reader, the size callback, immediate first pass, ticking, panic recovery. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…t in the metrics suite Nothing described the sandbox asset cache end to end: what is on disk, how an actor gets its assets, how a node decides which runtimes it needs, what eviction keeps and why, and what the metrics mean. Add docs/sandbox-asset-cache.md with that, link it from the README, the architecture page and the observability guide, and reword the one-line summaries that named a pin directory. The page describes the current design: each worker's default runtime is linked under the worker's own directory, in a directory per default the worker's file names that the syncer makes and removes and that goes with the pod, so the on-disk table, the syncer section, the eviction rule and the runbook follow that layout and a subsection gives its lifecycle. It also says that no eviction pass runs without a budget, and that the WorkerPool, not the ActorTemplate, decides which runtime an actor gets. ate_sandboxassetcache_size joins the platform metric prefixes the e2e metrics suite asserts, so every kind lane proves that atelet registers the instrument, its callback walks a real cache directory, and the OTLP path carries it. Only the size qualifies: it is observable and has a series from the first export, while the eviction and pass counters emit only once a pass had work to do, which never happens without a budget.
Dmitry Berkovich (dberkov)
force-pushed
the
asset-cache-docs
branch
from
October 19, 2026 04:19
cda1273 to
0053165
Compare
Dmitry Berkovich (dberkov)
marked this pull request as ready for review
October 19, 2026 04:23
Dmitry Berkovich (dberkov)
requested review from
Michelle Au (msau42) and
Zoe Zhao (zoez7)
October 19, 2026 04:23
Zoe Zhao (zoez7)
approved these changes
Oct 19, 2026
Zoe Zhao (zoez7)
enabled auto-merge
October 19, 2026 18:21
auto-merge was automatically disabled
October 19, 2026 18:48
Pull request was closed
Collaborator
Author
|
googlebot I fixed it. |
Dmitry Berkovich (dberkov)
enabled auto-merge
October 19, 2026 18:55
Max Thompson (thompsonmax)
pushed a commit
to thompsonmax/substrate
that referenced
this pull request
Oct 19, 2026
…etric family (agent-substrate#2889) Follow-up to agent-substrate#2756, so that agent-substrate#2796 can give the snapshot cache a budget without a third copy of the eviction loop. **What moves.** The sandbox asset cache's eviction loop, flags, instruments, target formula and shortfall backoff move from `cmd/atelet/sandbox_asset_gc.go` into a package, `cmd/atelet/internal/filecachegc`, written against the filecache store. The loop's behavior is unchanged: immediate first pass, serialized passes, panic recovery, dry run, budget with a 90% low water, shortfall warnings that back off to a periodic reminder. **How a cache uses it.** - `filecachegc.NewFlags("sandbox-asset-cache", "sandbox asset cache")` registers `--sandbox-asset-cache-gc-period`, `--sandbox-asset-cache-max-bytes` and `--sandbox-asset-cache-gc-dry-run`, so the asset cache's flags are the same as before. The snapshot cache will call it once with its own prefix. - `filecachegc.NewInstruments(meter)` creates the instruments once per process. - `filecachegc.Start(ctx, name, store, flags, instruments)` reports the cache's size, warns once when there is no budget, and runs the loop when there is a budget and a period. **One metric family.** `ate.filecache.size`, `ate.filecache.evictions` and `ate.filecache.gc.passes`, labeled by `ate.filecache.name` (today `sandbox_asset`) and, for passes, `ate.filecache.gc.outcome`, replace `ate.sandboxassetcache.size`, `.evictions` and `.gc.passes`. The caches run on one store and one loop, so one set of instruments describes them, and "which cache is over budget on this node" is one query grouped by name. The registry, `ateattr` and the observability guide are updated. The name is `filecache` rather than something broader because the family is about the filecache store; the image cache, which measures layers against volume watermarks, is not a member and keeps its loop. **Tests.** The asset GC's tests move into the package unchanged in what they prove, plus one that puts two stores on the same instruments and asserts two size series under their names. **Docs and e2e.** The sandbox asset cache page from agent-substrate#2792 and the e2e metrics suite follow the rename in the last commit: the suite asserts `ate_filecache_size`, and the page's metrics table and runbook use the new names and say where the label comes from. **Follow-up.** agent-substrate#2796 adopts the driver for the snapshot cache and adds the `snapshot` label value. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
The page describes #2756's final design: the default runtime is linked under the worker's own directory, in a directory per default the worker's file names, made and removed by the syncer and gone with the pod, so the on-disk table, syncer section, eviction rule, and runbook follow that layout and a subsection gives its lifecycle; and with no budget set atelet starts no eviction pass.
Step 6 of #2675, in its small form: the e2e under eviction pressure was dropped as too much machinery for what it proves, and this is what remained worth doing.
Metrics suite.
ate_sandboxassetcache_sizejoinsPlatformMetricPrefixes, soTestPlatformMetricsEmittedproves on every kind lane that atelet registers the instrument, its callback walks a real cache directory without error, and the OTLP path to the collector carries it. Only size qualifies: it is an observable instrument read on every collection, so it has a series from the first export on any install. The eviction and pass counters emit only once a pass had work to do, which never happens with the default unbounded budget.Doc.
docs/sandbox-asset-cache.md, for the operator or engineer who meets the cache for the first time: what lives under/var/lib/ateand why it must share a mount; how an actor gets its assets, from the runtime the request names through the index, the cache and the hard links, and what Run, Restore and Checkpoint each do; how the worker sandbox syncer decides which runtimes a node needs, prewarms and pins the defaults, and prunes; the eviction rules and the six flags; the three metrics and what a shortfall means; a runbook for a node over budget; and what is deliberately left out. Linked from the README's doc list, the node supervisor section of the architecture page, and the metrics paragraph of the observability guide.The page describes the request as naming the runtime the worker registered, which is #2788's behavior rather than this stack's. It documents the state once both land rather than the interim.
🤖 Generated with Claude Code