carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package assetcache
2
3import (
4 "errors"
5 "fmt"
6 "os"
7 "sync"
8 "testing"
9 "time"
10)
11
12// fakeFS is a settable free-space source. Tests using it are not acceptance:
13// the real FS is covered by TestStatfsReportsTheTempDir.
14type fakeFS struct {
15 mu sync.Mutex
16 total, free uint64
17 err error
18}
19
20func (f *fakeFS) Space(string) (uint64, uint64, error) {
21 f.mu.Lock()
22 defer f.mu.Unlock()
23 return f.total, f.free, f.err
24}
25
26func (f *fakeFS) setFree(v uint64) {
27 f.mu.Lock()
28 f.free = v
29 f.mu.Unlock()
30}
31
32const worstReserve = 2*MaxBody + metaReserve
33
34// fillStore opens a Store over a real temporary directory with a fake FS that
35// has plenty of room, and a budget big enough for many worst-case fills.
36func fillStore(t *testing.T) (*Store, *fakeFS, string) {
37 t.Helper()
38 dir := t.TempDir()
39 fs := &fakeFS{total: 64 << 30, free: 64 << 30}
40 cfg := testConfig(dir)
41 cfg.Budget = 4 << 30
42 cfg.FS = fs
43 s, err := Open(cfg)
44 if err != nil {
45 t.Fatal(err)
46 }
47 return s, fs, dir
48}
49
50// TestReserveWorstCaseWithUnknownLength is not acceptance: it uses a fake FS.
51func TestReserveWorstCaseWithUnknownLength(t *testing.T) {
52 s, _, _ := fillStore(t)
53 f, err := s.Reserve(testKey("shop", "/a.css"), -1)
54 if err != nil {
55 t.Fatal(err)
56 }
57 defer f.Abort()
58 if got, want := s.Stats().Reserved, int64(2*MaxBody+64<<10); got != want {
59 t.Fatalf("Reserved = %d, want %d", got, want)
60 }
61}
62
63// TestReserveUsesContentLength is not acceptance: it uses a fake FS.
64func TestReserveUsesContentLength(t *testing.T) {
65 s, _, _ := fillStore(t)
66 f, err := s.Reserve(testKey("shop", "/a.css"), 1000)
67 if err != nil {
68 t.Fatal(err)
69 }
70 defer f.Abort()
71 if got, want := s.Stats().Reserved, int64(2*1000+metaReserve); got != want {
72 t.Fatalf("Reserved = %d, want %d", got, want)
73 }
74}
75
76// TestReserveRefusesTooLarge is not acceptance: it uses a fake FS.
77func TestReserveRefusesTooLarge(t *testing.T) {
78 s, _, dir := fillStore(t)
79 if _, err := s.Reserve(testKey("shop", "/big"), MaxBody+1); !errors.Is(err, ErrTooLarge) {
80 t.Fatalf("err = %v, want ErrTooLarge", err)
81 }
82 f, err := s.Reserve(testKey("shop", "/max"), MaxBody)
83 if err != nil {
84 t.Fatalf("a body of exactly MaxBody: %v", err)
85 }
86 f.Abort()
87 if got := s.Stats().Reserved; got != 0 {
88 t.Fatalf("Reserved = %d after refusals and abort, want 0", got)
89 }
90 if got := listDir(t, dir); len(got) != 0 {
91 t.Fatalf("files left: %v", got)
92 }
93}
94
95// TestReserveRefusesBelowTheFloor is not acceptance: it uses a fake FS. It is
96// spec section 7's sixteen-fill case reduced to its rule: free space minus
97// outstanding reservations minus this one must stay at the 15% floor.
98func TestReserveRefusesBelowTheFloor(t *testing.T) {
99 s, fs, _ := fillStore(t)
100 fs.total = 8 << 30
101 fs.free = 8<<30*15/100 + worstReserve // exactly one reservation above the floor
102 first, err := s.Reserve(testKey("shop", "/a"), -1)
103 if err != nil {
104 t.Fatalf("first reservation: %v", err)
105 }
106 defer first.Abort()
107 if _, err := s.Reserve(testKey("blog", "/b"), -1); !errors.Is(err, ErrBelowFloor) {
108 t.Fatalf("second concurrent reservation: err = %v, want ErrBelowFloor", err)
109 }
110 if got := s.Stats().Reserved; got != worstReserve {
111 t.Fatalf("Reserved = %d after a refusal, want %d", got, int64(worstReserve))
112 }
113 // Once the first is released the room is back.
114 first.Abort()
115 again, err := s.Reserve(testKey("blog", "/b"), -1)
116 if err != nil {
117 t.Fatalf("after abort: %v", err)
118 }
119 again.Abort()
120}
121
122// TestReserveRefusesWhenSomethingElseFillsTheDisk is not acceptance: it uses a
123// fake FS. Free space drops between two calls, as when logs fill the disk.
124func TestReserveRefusesWhenSomethingElseFillsTheDisk(t *testing.T) {
125 s, fs, _ := fillStore(t)
126 fs.total = 8 << 30
127 fs.setFree(4 << 30)
128 f, err := s.Reserve(testKey("shop", "/a"), -1)
129 if err != nil {
130 t.Fatal(err)
131 }
132 defer f.Abort()
133 fs.setFree(8 << 30 * 15 / 100) // the floor itself, nothing to spare
134 if _, err := s.Reserve(testKey("shop", "/b"), 10); !errors.Is(err, ErrBelowFloor) {
135 t.Fatalf("err = %v, want ErrBelowFloor", err)
136 }
137 // The fill in flight is untouched and still writes up to its reservation.
138 if _, err := f.Write([]byte("still fine")); err != nil {
139 t.Fatalf("in-flight write: %v", err)
140 }
141}
142
143// TestReserveRefusesWithAnFSError is not acceptance: it uses a fake FS.
144func TestReserveRefusesWithAnFSError(t *testing.T) {
145 s, fs, _ := fillStore(t)
146 boom := errors.New("statfs failed")
147 fs.err = boom
148 if _, err := s.Reserve(testKey("shop", "/a"), 10); !errors.Is(err, boom) {
149 t.Fatalf("err = %v, want the FS error", err)
150 }
151 if got := s.Stats().Reserved; got != 0 {
152 t.Fatalf("Reserved = %d after an FS error", got)
153 }
154 s.mu.Lock()
155 boxFills := s.fills
156 s.mu.Unlock()
157 if boxFills != 0 {
158 t.Fatalf("fill slots held after an FS error: %d", boxFills)
159 }
160}
161
162// TestReserveRefusesOverBudget is not acceptance: it uses a fake FS.
163func TestReserveRefusesOverBudget(t *testing.T) {
164 dir := t.TempDir()
165 cfg := testConfig(dir)
166 cfg.Budget = 2*500 + metaReserve + 10 // room for one 500-byte fill, not two
167 cfg.FS = &fakeFS{total: 64 << 30, free: 64 << 30}
168 s, err := Open(cfg)
169 if err != nil {
170 t.Fatal(err)
171 }
172 a, err := s.Reserve(testKey("shop", "/a"), 500)
173 if err != nil {
174 t.Fatal(err)
175 }
176 defer a.Abort()
177 if _, err := s.Reserve(testKey("shop", "/b"), 500); !errors.Is(err, ErrNoRoom) {
178 t.Fatalf("err = %v, want ErrNoRoom", err)
179 }
180}
181
182// TestReserveBoundsFillsPerBoxAndPerApp is not acceptance: it uses a fake FS.
183func TestReserveBoundsFillsPerBoxAndPerApp(t *testing.T) {
184 s, _, _ := fillStore(t)
185 var fills []*Fill
186 defer func() {
187 for _, f := range fills {
188 f.Abort()
189 }
190 }()
191 reserve := func(app string, i int) (*Fill, error) {
192 f, err := s.Reserve(testKey(app, fmt.Sprintf("/%d", i)), 100)
193 if err == nil {
194 fills = append(fills, f)
195 }
196 return f, err
197 }
198
199 // Four fills for one app are fine, the fifth is busy.
200 for i := 0; i < maxFillsPerApp; i++ {
201 if _, err := reserve("shop", i); err != nil {
202 t.Fatalf("fill %d for shop: %v", i, err)
203 }
204 }
205 if _, err := reserve("shop", 99); !errors.Is(err, ErrBusy) {
206 t.Fatalf("5th fill for one app: err = %v, want ErrBusy", err)
207 }
208 // Another app is not held up by it.
209 for i := 0; i < maxFillsPerApp; i++ {
210 if _, err := reserve("blog", i); err != nil {
211 t.Fatalf("fill %d for blog: %v", i, err)
212 }
213 }
214 // Fill the box to 16 with further apps, then the 17th is busy.
215 for a := 0; len(fills) < maxFillsPerBox; a++ {
216 if _, err := reserve(fmt.Sprintf("app%d", a), 0); err != nil {
217 t.Fatalf("fill %d: %v", len(fills), err)
218 }
219 }
220 if _, err := reserve("other", 0); !errors.Is(err, ErrBusy) {
221 t.Fatalf("17th fill on the box: err = %v, want ErrBusy", err)
222 }
223 // Releasing one frees a slot for the box and for that app.
224 fills[0].Abort()
225 fills = fills[1:]
226 if _, err := reserve("shop", 100); err != nil {
227 t.Fatalf("after abort: %v", err)
228 }
229}
230
231// TestWritePastTheReservationFails is not acceptance: it uses a fake FS.
232func TestWritePastTheReservationFails(t *testing.T) {
233 s, _, _ := fillStore(t)
234 f, err := s.Reserve(testKey("shop", "/a"), 10)
235 if err != nil {
236 t.Fatal(err)
237 }
238 defer f.Abort()
239 if n, err := f.Write([]byte("0123456789")); err != nil || n != 10 {
240 t.Fatalf("write of exactly the reservation: n=%d err=%v", n, err)
241 }
242 if n, err := f.Write([]byte("x")); !errors.Is(err, ErrTooLarge) || n != 0 {
243 t.Fatalf("write past the reservation: n=%d err=%v, want 0 and ErrTooLarge", n, err)
244 }
245 if _, err := f.Write(nil); !errors.Is(err, ErrTooLarge) {
246 t.Fatalf("a failed fill keeps failing: err = %v", err)
247 }
248}
249
250// TestWritePastMaxBodyFails is not acceptance: it uses a fake FS. With an
251// unknown length the identity half is MaxBody, and the fill stops there.
252func TestWritePastMaxBodyFails(t *testing.T) {
253 s, _, _ := fillStore(t)
254 f, err := s.Reserve(testKey("shop", "/a"), -1)
255 if err != nil {
256 t.Fatal(err)
257 }
258 defer f.Abort()
259 chunk := make([]byte, 1<<20)
260 for i := 0; i < MaxBody>>20; i++ {
261 if _, err := f.Write(chunk); err != nil {
262 t.Fatalf("chunk %d: %v", i, err)
263 }
264 }
265 if _, err := f.Write([]byte("x")); !errors.Is(err, ErrTooLarge) {
266 t.Fatalf("byte past MaxBody: err = %v, want ErrTooLarge", err)
267 }
268}
269
270// TestAbortReleasesEverything is not acceptance: it uses a fake FS.
271func TestAbortReleasesEverything(t *testing.T) {
272 s, _, dir := fillStore(t)
273 f, err := s.Reserve(testKey("shop", "/a"), 100)
274 if err != nil {
275 t.Fatal(err)
276 }
277 if _, err := f.Write([]byte("some bytes")); err != nil {
278 t.Fatal(err)
279 }
280 if len(listDir(t, dir)) == 0 {
281 t.Fatal("expected a temporary while the fill is open")
282 }
283 f.Abort()
284 if got := s.Stats().Reserved; got != 0 {
285 t.Fatalf("Reserved = %d after Abort", got)
286 }
287 if got := listDir(t, dir); len(got) != 0 {
288 t.Fatalf("files left after Abort: %v", got)
289 }
290 s.mu.Lock()
291 boxFills, appFills := s.fills, len(s.fillsByApp)
292 s.mu.Unlock()
293 if boxFills != 0 || appFills != 0 {
294 t.Fatalf("slots after Abort: box %d, apps %d, want 0 and 0", boxFills, appFills)
295 }
296 if _, err := f.Write([]byte("x")); err == nil {
297 t.Fatal("write after Abort succeeded")
298 }
299}
300
301// TestAbortTwiceIsSafe is not acceptance: it uses a fake FS. A second Abort
302// must not release another fill's reservation.
303func TestAbortTwiceIsSafe(t *testing.T) {
304 s, _, _ := fillStore(t)
305 a, err := s.Reserve(testKey("shop", "/a"), 100)
306 if err != nil {
307 t.Fatal(err)
308 }
309 b, err := s.Reserve(testKey("shop", "/b"), 100)
310 if err != nil {
311 t.Fatal(err)
312 }
313 defer b.Abort()
314 a.Abort()
315 a.Abort()
316 if got, want := s.Stats().Reserved, int64(2*100+metaReserve); got != want {
317 t.Fatalf("Reserved = %d, want %d (b's alone)", got, want)
318 }
319}
320
321// TestReserveAndAbortConcurrently is not acceptance: it uses a fake FS. Run
322// with -race: counters must come back to zero.
323func TestReserveAndAbortConcurrently(t *testing.T) {
324 s, _, dir := fillStore(t)
325 var wg sync.WaitGroup
326 for g := 0; g < 8; g++ {
327 wg.Add(1)
328 go func(g int) {
329 defer wg.Done()
330 for i := 0; i < 50; i++ {
331 f, err := s.Reserve(testKey([]string{"shop", "blog"}[g%2], fmt.Sprintf("/%d/%d", g, i)), 64)
332 if err != nil {
333 if !errors.Is(err, ErrBusy) {
334 t.Errorf("unexpected error: %v", err)
335 }
336 continue
337 }
338 if _, err := f.Write([]byte("body")); err != nil {
339 t.Errorf("write: %v", err)
340 }
341 f.Abort()
342 }
343 }(g)
344 }
345 wg.Wait()
346 if got := s.Stats().Reserved; got != 0 {
347 t.Fatalf("Reserved = %d after all aborted", got)
348 }
349 if got := listDir(t, dir); len(got) != 0 {
350 t.Fatalf("files left: %v", got)
351 }
352}
353
354// TestAbortKeepsLeftoverBytesCountedAndRetries is not acceptance: it uses a
355// fake FS. When the temporaries cannot be removed (the directory is not
356// writable), the bytes still on disk stay counted, the fill slot is released,
357// and a second Abort removes the files and releases the bytes.
358func TestAbortKeepsLeftoverBytesCountedAndRetries(t *testing.T) {
359 if os.Geteuid() == 0 {
360 t.Skip("root ignores directory permissions")
361 }
362 s, _, dir := fillStore(t)
363 f, err := s.Reserve(testKey("shop", "/a"), 100)
364 if err != nil {
365 t.Fatal(err)
366 }
367 if _, err := f.Write([]byte("ten bytes!")); err != nil {
368 t.Fatal(err)
369 }
370 if err := os.Chmod(dir, 0o500); err != nil {
371 t.Fatal(err)
372 }
373 t.Cleanup(func() { os.Chmod(dir, 0o700) })
374
375 f.Abort()
376 if got := s.Stats().Reserved; got != 10 {
377 t.Fatalf("Reserved = %d after a failed cleanup, want the 10 bytes still on disk", got)
378 }
379 if got := listDir(t, dir); len(got) != 1 {
380 t.Fatalf("files after a failed cleanup: %v, want the one body temporary", got)
381 }
382 s.mu.Lock()
383 boxFills, appFills := s.fills, len(s.fillsByApp)
384 s.mu.Unlock()
385 if boxFills != 0 || appFills != 0 {
386 t.Fatalf("slots after a failed cleanup: box %d, apps %d, want 0 and 0", boxFills, appFills)
387 }
388 f.Abort() // still failing: nothing changes, nothing is released twice
389 if got := s.Stats().Reserved; got != 10 {
390 t.Fatalf("Reserved = %d after a second failed cleanup, want 10", got)
391 }
392
393 if err := os.Chmod(dir, 0o700); err != nil {
394 t.Fatal(err)
395 }
396 f.Abort()
397 if got := s.Stats().Reserved; got != 0 {
398 t.Fatalf("Reserved = %d after the retry, want 0", got)
399 }
400 if got := listDir(t, dir); len(got) != 0 {
401 t.Fatalf("files after the retry: %v", got)
402 }
403 f.Abort() // and a further Abort is a no-op
404 if got := s.Stats().Reserved; got != 0 {
405 t.Fatalf("Reserved = %d after a third Abort", got)
406 }
407}
408
409// TestReserveResamplesWhenACommitLandsAfterItsSample is not acceptance: it
410// uses a fake FS. A fill commits between another Reserve's free-space sample
411// and its decision. The committed bytes have left the reservation counter
412// and are not in the sample either, so a decision that combined the two
413// would count them nowhere. The numbers make that stale combination pass
414// the floor and a fresh sample fail it.
415func TestReserveResamplesWhenACommitLandsAfterItsSample(t *testing.T) {
416 s, fs, _ := fillStore(t)
417 const total = 8 << 30
418 floor := uint64(total) * floorPercent / 100
419 body := make([]byte, 1<<20)
420 onDisk := uint64(len(body)) + 4096 // what the fake disk loses when a commits
421 start := floor + worstReserve + onDisk/2
422 fs.total = total
423 fs.setFree(start)
424
425 a := fill(t, s, testKey("blog", "/a.css"), body)
426 samples := 0
427 s.afterSample = func() {
428 samples++
429 if samples > 1 {
430 return
431 }
432 if err := a.Commit(Meta{Header: cssHeader("1"), Lifetime: time.Hour, Expires: testNow.Add(time.Hour)}, false); err != nil {
433 t.Errorf("Commit: %v", err)
434 }
435 fs.setFree(start - onDisk)
436 }
437 f, err := s.Reserve(testKey("shop", "/b.css"), -1)
438 if err == nil {
439 f.Abort()
440 }
441 if !errors.Is(err, ErrBelowFloor) {
442 t.Fatalf("Reserve after a commit landed behind its sample: err = %v, want ErrBelowFloor", err)
443 }
444 if samples < 2 {
445 t.Fatalf("free space sampled %d times, want a second sample after the commit", samples)
446 }
447 if st := s.Stats(); st.Reserved != 0 || st.Entries != 1 {
448 t.Fatalf("Stats = %+v, want a's entry and nothing reserved", st)
449 }
450}
451
452// TestReserveDecidesUnderTheLockWhenReleasesKeepComing is not acceptance: it
453// uses a fake FS. Every unlocked sample is overtaken by a release, so after
454// sampleTries of them Reserve samples with the store locked, where nothing
455// can move the counter, and decides on that.
456func TestReserveDecidesUnderTheLockWhenReleasesKeepComing(t *testing.T) {
457 s, _, _ := fillStore(t)
458 var others []*Fill
459 for i := 0; i < sampleTries; i++ {
460 others = append(others, fill(t, s, testKey("blog", fmt.Sprintf("/%d", i)), []byte("x")))
461 }
462 samples := 0
463 s.afterSample = func() {
464 if samples < len(others) {
465 others[samples].Abort()
466 }
467 samples++
468 }
469 f, err := s.Reserve(testKey("shop", "/a.css"), 10)
470 if err != nil {
471 t.Fatalf("Reserve: %v", err)
472 }
473 f.Abort()
474 if samples != sampleTries {
475 t.Fatalf("unlocked samples = %d, want %d and then one under the lock", samples, sampleTries)
476 }
477 if st := s.Stats(); st.Reserved != 0 {
478 t.Fatalf("Reserved = %d after every fill aborted", st.Reserved)
479 }
480}
481