Repository navigation
Conversation
The cache grows forever since nothing ever calls Cache.Expire. Call it after a successful cut with a one week timeout. Opening an entry refreshes its mtime, so this only removes entries no cut has read in that time, and a failure to expire is logged rather than failing the cut. Expire now also skips entries that disappear while it runs, so two expiries over the same cache don't fail on each other's removals. Fixes canonical#261
lczyk
left a comment
There was a problem hiding this comment.
hi. cheers for the pr. :) let me kick things of by doing a first-pass review.
| err = os.Remove(filepath.Join(digestKindDir, finfo.Name())) | ||
| if err != nil { | ||
| if err != nil && !os.IsNotExist(err) { | ||
| return fmt.Errorf("cannot expire cache entry: %v", err) |
There was a problem hiding this comment.
if an entry can't be removed it stops the rest of expiry. e.g. a stale non-empty dir that sorts first gives cannot expire cache entry: ... directory not empty and every stale file after it stays unreclaimed.
There was a problem hiding this comment.
Good catch. It now carries on past an entry it can't remove and returns the first error once it has tried everything, so one bad entry doesn't keep the rest around (791b62b). With the change from the other thread, directories aren't considered at all anymore.
| if finfo.ModTime().After(expired) { | ||
| continue | ||
| } | ||
| err = os.Remove(filepath.Join(digestKindDir, finfo.Name())) |
There was a problem hiding this comment.
this removes anything old under sha*/, not just what chisel wrote. we know exactly what files we wrote so maybe its better to be more selective here(?)
There was a problem hiding this comment.
Agreed. It only looks at regular files named like a digest of that kind now, plus <digest>.tmp and tmp.N, the leftovers of a write that didn't finish, since those are ours too and nothing else ever cleans them up. Anything else under sha*/ is left alone (791b62b).
| // Reading an entry refreshes its timestamp, so this only drops what no | ||
| // cut has used for a while. Failing to clean up is not a failed cut. | ||
| cc := &cache.Cache{Dir: cacheDir} | ||
| if err := cc.Expire(cacheExpiry); err != nil { |
There was a problem hiding this comment.
note. this is the first time cut touches files outside of --root. kinda obvious but also could be detrimental for some use cases (e.g. pre-warming cache). i would personally very much like something like CHISEL_CACHE_EXPIRY to e.g. control the expiry time and CHISEL_CACHE_EXPIRY=0 to disable (and i guess for now safest is to ignore others aka ignore anything thats not positive integer or 0).
There was a problem hiding this comment.
Makes sense, added CHISEL_CACHE_EXPIRY in a6f1417: a number of days, 7 when unset, and 0 disables it. For any other value I read "ignore" as "don't act on it": it logs a warning and skips expiry instead of falling back to 7 days, so someone who sets e.g. off to protect a pre-warmed cache doesn't lose it. Happy to fall back to the default instead if you prefer.
It's also mentioned in chisel help cut. I can add it to chisel-docs once the name and behaviour are settled.
| return fmt.Errorf("cannot list cache directory: %v", err) | ||
| } | ||
| for _, entry := range entries { | ||
| // Entries may vanish while listing, e.g. renamed from .tmp by |
There was a problem hiding this comment.
small race: Open does os.Open and then Chtimes. thich gives a timing window for a race from a parallel process between the two and can lead to the cut dying with cannot update cached file timestamp. likely around us window, and i have not been able to reproduce it, but its technically there and so noting it here as part of my ongoing attempt to make chisel more concurency-friendly.
There was a problem hiding this comment.
Right, the open file is still fine to read if the entry gets removed between os.Open and Chtimes, so Open now ignores ENOENT from Chtimes. It also closes the file on the other errors, which it didn't before (c053a58). I didn't add a test for this one, I couldn't hit that window without adding a hook to Open just for it.
| c.Assert(err, IsNil) | ||
| } | ||
|
|
||
| // Expiries running at the same time remove entries the others have |
There was a problem hiding this comment.
this is probabilistic since a slow runner can schedule gorutines differently.
There was a problem hiding this comment.
Yeah. Replaced it with TestCacheExpireVanishedEntry, which removes the entry itself between listing and stat, and between stat and remove, so nothing depends on scheduling (791b62b).
| } | ||
|
|
||
| // Reading an entry refreshes its timestamp, so this only drops what no | ||
| // cut has used for a while. Failing to clean up is not a failed cut. |
There was a problem hiding this comment.
| // cut has used for a while. Failing to clean up is not a failed cut. | |
| // Reading an entry refreshes its timestamp, so this only drops what no | |
| // cut has used for a while. |
nit: second sentence restates the logf below.
There was a problem hiding this comment.
Done. The comment moved into the shared expireCache helper with the call, so it now says "no command" (a6f1417).
| } | ||
|
|
||
| // cacheExpiry is how long a cache entry can go unused before a cut removes it. | ||
| const cacheExpiry = 7 * 24 * time.Hour |
There was a problem hiding this comment.
hmmm... find / info / debug check-release-archives fill the cache too and never expire it. if we decide to expire the cache should we have that expiry hook in every command which touches it...?
There was a problem hiding this comment.
Good point for debug check-release-archives: it fetches every package of the release, so it now calls the same helper once it has them (a6f1417). find and info only go through FetchRelease, which keeps one releases/<name> directory per release and replaces it in place, so they never add to sha*/ and there's nothing for them to expire.
Expire removed anything old under the digest directories, and the first entry it could not remove stopped the whole run. It now only looks at regular files named like a digest of that kind, or like the temporary files of an unfinished write, and returns the first failure after trying every entry. The concurrent expiry test depended on scheduling. It is replaced by a test that removes an entry between listing and removal on purpose.
Open refreshes the timestamp of an entry after opening it. If another process expires the entry in between, Chtimes fails and so did the fetch, although the open file is still readable. Ignore that case, and close the file on the other failures.
CHISEL_CACHE_EXPIRY sets how many days a cache entry can go unused, 7 by default, and 0 disables expiry. Any other value logs a warning and leaves the cache alone, since deleting is the one thing that can't be undone. debug check-release-archives fetches every package of a release, so it now expires the cache too. find and info only refresh the release directory, which is replaced in place and does not grow. The command tests point XDG_CACHE_HOME at a temporary directory, so they never expire the cache of whoever runs them.
|
Thanks for the review! I pushed three commits for it and replied inline:
Tested with |
Fixes #261.
Nothing calls
Cache.Expire, so the cache only grows. Commands that fill the cache now expire it once they are done with it:cutafter a successful cut, anddebug check-release-archivesafter fetching the packages.Cache.Openalready refreshes an entry's mtime, so only entries nothing has read for a while go away. A failed expiry is logged as a warning.CHISEL_CACHE_EXPIRYsets the expiry in days, 7 by default, and0disables it. Any other value logs a warning and leaves the cache alone.Expireonly removes files chisel wrote: entries named after their digest, and temporary files left by unfinished writes. It carries on past an entry it can't remove, and skips entries that disappear while it runs, so concurrent expiries don't fail on each other.Openno longer fails when an entry is expired right after it was opened.Tested with
go test ./...(and-race -count=30for the cache and cmd packages), golangci-lint, and thetests/cache-expiryspread task in an ubuntu:24.04 container: it passes on this branch and fails on main.