mirror of
https://github.com/kvcache-ai/AgentENV.git
synced 2026-11-05 01:17:36 +00:00
[GH-ISSUE #320] OverlayBD full-file cache: entries held open by long-lived sandboxes are never evicted, so a node stops caching new templates #84
Labels
No labels
bug
documentation
enhancement
enhancement
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/AgentENV#84
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @nblintao on GitHub (Sep 25, 2026).
Original GitHub issue: https://github.com/kvcache-ai/AgentENV/issues/320
Originally assigned to: @huajq on GitHub.
Affected component
OverlayBD / image resolution
Problem statement
The full-file cache never evicts an entry that is open. A running sandbox keeps its template's memory-snapshot layers (and its rootfs lower layers) open for its whole lifetime, so the layers of every template that has at least one live sandbox stay pinned, however long ago they were last read.
When the pinned layers reach the pool's eviction watermark (90% of
cacheSizeGB), eviction has nothing it is allowed to evict,is_fullstays set, and every refill is refused withENOSPC. From then on, any template that is not already cached resumes fully on demand: each read goes to the source for that request only, nothing is kept, and concurrent resumes of the same template do not share fetched blocks. Resumes that touch many pages can then exceedenvd.init_timeout_secs, and the create fails.The failure is a cliff rather than a gradual slowdown: templates that are already cached keep working, every new template degrades at once, and the capacity is held by sandboxes that may no longer be doing anything.
Use case
A node that serves many templates with long-lived sandboxes. For example, agent or evaluation workloads that build one template per task, start a burst of sandboxes from it, and keep each sandbox for the duration of the task (tens of minutes to hours). Sandboxes can also outlive their users when a client exits without deleting them, in which case they run until their timeout.
With memory layers of roughly 0.5–1.5 GiB per template and the default
max_size_gb = 100, about 60–90 templates that each have one live sandbox are enough to pin the memory-snapshot cache. We hit this on a bare-metal x86_64 node running about 800 concurrent sandboxes.Current behavior and workarounds
Code paths, at upstream main
04f8786:evictable_cache_ids_by_lruonly returns entries for whichentry_is_busyis false, andentry_is_busyis true wheneveropen_count > 0. Open entries are skipped regardless oflast_access.publish_pressure_lockedkeepsis_fullset whilecurrent_bytesis at or above the watermark.do_refill_block_genericreturnsENOSPCwhileis_fullis set, sotry_preadv2_genericfalls back topread_from_source_genericblock by block, with no caching and no deduplication between concurrent readers.ENOSPCwhile the pool is full, so the layer is never filled either.What we observed on a node in this state (build based on upstream
187f654,storage/unmodified):image-cache/memory-blockssat at 90 GiB of 100 GiB. Almost all of it belonged to templates whose remaining sandboxes were idle (median CPU below 1%).datafile and nometa.bin, while the entries of templates with running sandboxes were fully populated.sandbox failed to become ready ... timed out waiting for envd after 105 probes in 60001 ms. Earlier the same day, the same template had started 32 sandboxes on the same node in about 1.5 s each.Workarounds are to delete idle sandboxes, or to raise
max_size_gb, which only moves the threshold and needs a restart. Neither addresses the pinning.Desired behavior
Acceptance: with a small cache (for example
max_size_gb = 2) filled by open entries that are no longer read, starting sandboxes from a new template populates that template's cache entry, and later resumes of it are served from the cache.Proposed approach
For discussion:
CacheEntry::read_blockreturns a zero-copyBytesinto the entry's mmap without holdingrefill_eviction_barrier; as we read it, that is why open entries cannot be evicted today. Holding a read guard of the barrier for as long as the returnedByteslives (for example as part of its owner), and treating a failedtry_readas a miss, would make eviction wait for in-flight reads.last_access, using block-level eviction that keeps the entry (CacheEntry::evict_all_blocks) instead ofevict_entry, so open files keep refilling afterwards. An idle-age threshold would protect the working set of active sandboxes.Compatibility and operational impact
None of the three changes needs an API, configuration, or on-disk format change. The behavior change is that blocks of idle open entries can be evicted, so a sandbox that touches them again pays a source read. If changing the default is undesirable, (2) could be gated by a setting such as an idle age for open entries, off by default. The rootfs cache (
remote-blocks) uses the same backend and behaves the same way.Alternatives considered
max_size_gbdelays the cliff by a constant factor, takes disk space that sandboxes also need, and requires a restart.FileCachePoolevicts open files by truncating them. The Rust cache deliberately does not, presumably because of the zero-copy read path, so (1) looks like a prerequisite either way.Non-goals
Contribution
I can help test or review it
Pre-submission checklist