Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Order cache eviction by written files, not directory mtimes

e6f0d8b947d985c8615e1172aef06cee8fd3729e · Keenan Nemetz · 20h ago · parent 973fb809

Order cache eviction by written files, not directory mtimes

Trim sorted namespaces by the newest mtime in the tree, directories included.
A directory's mtime moves when a tree is unpacked, so two namespaces created in
the same breath compared equal and sort.Slice broke the tie arbitrarily. The
runner's verify run on 01M6YSX585GD5BDT45H0FE65AE removed the newer namespace
and left the older one, failing TestTrimDropsTheOldestNamespaceWhole on a
machine where it had passed on ext4.

measure now dates a namespace by its files, falling back to the directory's own
mtime when there is no file, and Trim breaks an exact tie by path so the same
namespace goes on every pass.

TestTrimOrdersByContentNotDirectories fails on the previous implementation with
the same message the runner printed.

ci/cache/cache.go +28−4

@@ -145,6 +145,10 @@ func holdNamespace(root, ns string, exclusive, wait bool) (func(), bool, error)
145 145 // unknown state; a skipped namespace still counts against the cap, so the next pass
146 146 // finds it again.
147 147 //
148 // "Written" is a file: see measure. Namespaces that were written at the same
149 // instant are ordered by path, so two caches of the same age always go in the same
150 // order rather than whichever one a sort happened to move first.
151 //
148 152 // Whole namespaces rather than files: a Go module cache with one file missing is
149 153 // worse than no Go module cache, because the failure is a checksum error deep in a
150 154 // build rather than a download.
@@ -171,7 +175,12 @@ func Trim(root string, maxBytes int64) (int64, error) {
171 175 if total <= maxBytes {
172 176 return 0, nil
173 177 }
174 sort.Slice(all, func(i, j int) bool { return all[i].newest.Before(all[j].newest) })
178 sort.Slice(all, func(i, j int) bool {
179 if all[i].newest.Equal(all[j].newest) {
180 return all[i].path < all[j].path
181 }
182 return all[i].newest.Before(all[j].newest)
183 })
175 184
176 185 var removed int64
177 186 for _, ns := range all {
@@ -269,7 +278,15 @@ func dirs(p string) []string {
269 278 return out
270 279 }
271 280
281 // measure returns a namespace's size and when it was last written. Only files
282 // count as writes: a directory's mtime moves when a tree is unpacked, so counting
283 // it would age a namespace by the act of extracting it rather than by the work it
284 // holds — and two namespaces created in the same breath would tie, after which the
285 // eviction order was whatever the sort did with equal keys. An empty namespace has
286 // no file to date it, so it falls back to its own mtime and can still be aged
287 // against the others.
272 288 func measure(dir string) (size int64, newest time.Time) {
289 var created time.Time
273 290 _ = filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
274 291 if err != nil {
275 292 return nil
@@ -278,14 +295,21 @@ func measure(dir string) (size int64, newest time.Time) {
278 295 if err != nil {
279 296 return nil
280 297 }
298 if d.IsDir() {
299 if p == dir {
300 created = info.ModTime()
301 }
302 return nil
303 }
304 size += info.Size()
281 305 if info.ModTime().After(newest) {
282 306 newest = info.ModTime()
283 307 }
284 if !d.IsDir() {
285 size += info.Size()
286 }
287 308 return nil
288 309 })
310 if newest.IsZero() {
311 newest = created
312 }
289 313 return size, newest
290 314 }
291 315