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_test.go +92
| @@ -131,6 +131,98 @@ func TestTrimDropsTheOldestNamespaceWhole(t *testing.T) { | |||
| 131 | 131 | } | |
| 132 | 132 | } | |
| 133 | 133 | ||
| 134 | // A namespace is aged by the work it holds, not by when its tree was unpacked. | ||
| 135 | // Directories are created when a cache is extracted, so counting their mtimes let a | ||
| 136 | // two-hour-old cache outlive a one-hour-old one. That is the shape of the failure | ||
| 137 | // that made this test pass on ext4 and fail on a runner. | ||
| 138 | func TestTrimOrdersByContentNotDirectories(t *testing.T) { | ||
| 139 | t.Parallel() | ||
| 140 | root := t.TempDir() | ||
| 141 | |||
| 142 | mk := func(repo, image string, content, dirs time.Time) string { | ||
| 143 | ns := Namespace(root, repo, "direct", image) | ||
| 144 | if err := os.MkdirAll(ns, 0o755); err != nil { | ||
| 145 | t.Fatal(err) | ||
| 146 | } | ||
| 147 | p := filepath.Join(ns, "a") | ||
| 148 | if err := os.WriteFile(p, make([]byte, 1000), 0o644); err != nil { | ||
| 149 | t.Fatal(err) | ||
| 150 | } | ||
| 151 | if err := os.Chtimes(p, content, content); err != nil { | ||
| 152 | t.Fatal(err) | ||
| 153 | } | ||
| 154 | if err := os.Chtimes(ns, dirs, dirs); err != nil { | ||
| 155 | t.Fatal(err) | ||
| 156 | } | ||
| 157 | return ns | ||
| 158 | } | ||
| 159 | |||
| 160 | now := time.Now() | ||
| 161 | old := mk("old", "ref-a", now.Add(-2*time.Hour), now) | ||
| 162 | fresh := mk("fresh", "ref-b", now.Add(-time.Hour), now.Add(-time.Hour)) | ||
| 163 | |||
| 164 | if removed, err := Trim(root, 1500); err != nil || removed != 1000 { | ||
| 165 | t.Fatalf("trim = %d, %v; want 1000 bytes", removed, err) | ||
| 166 | } | ||
| 167 | if _, err := os.Stat(old); !os.IsNotExist(err) { | ||
| 168 | t.Fatalf("the older content survived because its directory was touched later: %v", err) | ||
| 169 | } | ||
| 170 | if _, err := os.Stat(fresh); err != nil { | ||
| 171 | t.Fatalf("the newer content was removed: %v", err) | ||
| 172 | } | ||
| 173 | } | ||
| 174 | |||
| 175 | // Two namespaces written at the same instant must not be ordered by chance: the same | ||
| 176 | // one goes on every pass, so a runner sharing a directory does not evict a different | ||
| 177 | // cache depending on which way the sort fell. | ||
| 178 | func TestTrimBreaksATieDeterministically(t *testing.T) { | ||
| 179 | t.Parallel() | ||
| 180 | when := time.Now().Add(-time.Hour).Truncate(time.Second) | ||
| 181 | victim := "" | ||
| 182 | for run := 0; run < 20; run++ { | ||
| 183 | root := t.TempDir() | ||
| 184 | var built []string | ||
| 185 | for _, image := range []string{"ref-a", "ref-b"} { | ||
| 186 | ns := Namespace(root, "demo", "direct", image) | ||
| 187 | if err := os.MkdirAll(ns, 0o755); err != nil { | ||
| 188 | t.Fatal(err) | ||
| 189 | } | ||
| 190 | p := filepath.Join(ns, "a") | ||
| 191 | if err := os.WriteFile(p, make([]byte, 1000), 0o644); err != nil { | ||
| 192 | t.Fatal(err) | ||
| 193 | } | ||
| 194 | for _, path := range []string{p, ns} { | ||
| 195 | if err := os.Chtimes(path, when, when); err != nil { | ||
| 196 | t.Fatal(err) | ||
| 197 | } | ||
| 198 | } | ||
| 199 | built = append(built, ns) | ||
| 200 | } | ||
| 201 | if removed, err := Trim(root, 1500); err != nil || removed != 1000 { | ||
| 202 | t.Fatalf("run %d: trim = %d, %v; want 1000 bytes", run, removed, err) | ||
| 203 | } | ||
| 204 | gone := "" | ||
| 205 | for _, ns := range built { | ||
| 206 | if _, err := os.Stat(ns); os.IsNotExist(err) { | ||
| 207 | gone = ns | ||
| 208 | } | ||
| 209 | } | ||
| 210 | if gone == "" { | ||
| 211 | t.Fatalf("run %d: nothing was removed", run) | ||
| 212 | } | ||
| 213 | if victim == "" { | ||
| 214 | victim = gone | ||
| 215 | continue | ||
| 216 | } | ||
| 217 | // t.TempDir makes a new root per run, so compare the namespace that went, | ||
| 218 | // not where it was: the same name must be evicted every time. | ||
| 219 | if filepath.Base(gone) != filepath.Base(victim) { | ||
| 220 | t.Fatalf("run %d evicted %s, the first run evicted %s", | ||
| 221 | run, filepath.Base(gone), filepath.Base(victim)) | ||
| 222 | } | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 134 | 226 | // Clear is what --no-cache uses: the directory stays mounted, but nothing survives. | |
| 135 | 227 | func TestClearRemovesEverything(t *testing.T) { | |
| 136 | 228 | t.Parallel() | |