Fix cached-downloader bugs and implement periodic download_cache cleanup rather than waiting for the next download - #1196
Fix cached-downloader bugs and implement periodic download_cache cleanup rather than waiting for the next download#1196geofffranks wants to merge 4 commits into
Conversation
This reverts commit ce41050.
This reverts commit 1e91e88.
Adds a rep/cachecleanup.Runner that calls executor.Client.ReclaimCacheSpace once a minute, threaded through CachedDownloader.MakeRoom, so the cache downloader's LRU eviction runs proactively instead of only on-demand during a download. Also fixes three related bugs found while building this: DownloadCachedDependencies losing late-arriving successful mounts on error, unused OldEntries never being deleted from disk, and a failed tar extraction leaving a stale ExpandedDirectoryPath that blocked retry. FileCache.MakeRoom takes the package lock since it's now reachable concurrently with normal container create/download activity. ai-assisted=yes TNZ-127673
CI's lint-repo/sync-package-specs check regenerates packages/rep/spec and packages/rep_windows/spec from the gosub dependency graph and failed because the new rep/cachecleanup package (added for the periodic cache reclamation runner) wasn't listed alongside its sibling packages. ai-assisted=yes TNZ-127673
|
Some comments from claude, that might be worth considering. Comment 2 — RecoverState + periodic runner startup race Location: cacheddownloader.go / new cachecleanup/runner.go RecoverState is called during executor startup (initializer_garden.go:149). The new periodic runner starts as part of the executor's ifrit Members group, which launches goroutines concurrently. If the cleanup runner fires its first MakeRoom() tick before RecoverState finishes, makeRoom iterates over a partially-initialized Entries map (or an empty one from a prior state load that isn't done yet). makeRoom already holds lock when it runs, and RecoverState modifies c.cache.Entries without acquiring lock (it runs before the runner goroutines start). If the runner's first tick happens during or just after RecoverState completes but before the caller of RecoverState signals readiness, the timing window is narrow but real. Recommend ensuring the cleanup runner's ifrit.Runner is not added to the group until after RecoverState returns — or have the runner skip its first tick until an explicit "ready" signal is received. Comment 3 — MakeRoom() with size=0 is a no-op when cache is within limit Location: file_cache.go:385 / periodic runner The periodic call passes size=0, so makeRoom only evicts when usedSpace > maxSizeInBytes. In normal operation (cache within its configured limit), every 1-minute tick is a full lock acquisition + usedSpace scan with zero effect. This isn't harmful at current fleet sizes, but the PR description says "faster pruning" — which implies entries are being pruned more eagerly. In practice this only helps when the cache is perpetually over the limit, which shouldn't happen in steady state if the limit is configured correctly. If the goal is to proactively free entries that haven't been accessed recently (e.g., entries from apps that no longer run), the eviction criterion should also include an access-time threshold (TTL), not just size pressure. Otherwise the "periodic cleanup" only acts as a size-limit enforcer on a slower polling cycle, which the existing download-triggered makeRoom already handles. Comment 4 — Package-level lock increases contention surface with new goroutine Location: file_cache.go:18 var lock = &sync.Mutex{} lock is a package-level variable, not a field on FileCache. All FileCache instances in the process share the same mutex. Adding a continuously-running goroutine that acquires lock every 60 seconds means any download operation during that window must wait. At current frequencies this is negligible, but it's worth noting if FileCache is ever instantiated more than once per process (e.g., in tests or in a future multi-tenant executor). A per-instance mutex on FileCache would eliminate the coupling entirely. Comment 5 — OldEntries cleanup in CloseDirectory: verify file deletion is safe under concurrent reads Location: file_cache.go:CloseDirectory The fix deletes files from disk when an OldEntries entry's inUse count reaches zero. On Linux, unlink/RemoveAll on a file that has open file descriptors is safe — the file content remains accessible to existing readers until their descriptors are closed, and only the directory entry is removed. If this code runs on Windows (the PR also includes rep_windows spec changes), the behavior is different: Windows will return an error if the file is open. Confirm that the deletion path in CloseDirectory is either Linux-only or handles the Windows-open-file case explicitly. |
|
Thanks -
The reviewer's mental model of the startup sequence is wrong. Tracing the actual call chain:
RecoverState completes before Initialize() returns. The ifrit group containing the cleanup runner doesn't start until after Initialize() returns. There is no timing window — RecoverState and the runner's first tick cannot overlap. The sequence is strictly serial: RecoverState → return Members → construct group → start group → first tick (≥60s later).
The goal isn't to proactively free things that aren't recent, it's to proactively free things that are over the limit and wouldn't be cleaned up until the next LRP/Task download. Don't want to introduce new behaviors to how the eviction logic happens, just when it happens.
Not worried about this, we won't be making it happen multiple times per node or more frequently. the current contention happens only if downloading a new asset for a new LRP or Task is happening at the exact moment this is checking. Since those paths already called makeRoom, it's not like they see any new time from this (locking/calculating isn't the bottleneck, cleaning is).
This would only affect Windows, but I don't want to introduce new behaviors to how the eviction logic happens, just when it happens. We've been using this logic for many years without running into issues with this. |
Summary
Fixes 4 issues:
Backward Compatibility
Breaking Change? no