package nixcache import ( "context" "net/http" "sync" "testing" "time" "tangled.org/core/spindle/db" ) type fakeReservations struct { mu sync.Mutex committed, released []string } func (f *fakeReservations) Commit(_ context.Context, id string) error { f.mu.Lock() defer f.mu.Unlock() f.committed = append(f.committed, id) return nil } func (f *fakeReservations) Release(_ context.Context, id string) error { f.mu.Lock() defer f.mu.Unlock() f.released = append(f.released, id) return nil } func (f *fakeReservations) settled() (committed, released []string) { f.mu.Lock() defer f.mu.Unlock() return append([]string(nil), f.committed...), append([]string(nil), f.released...) } func publishingRow(id, repo, hash string, age time.Duration) db.NixPublishing { cache, _ := CacheName(repo) return db.NixPublishing{ReservationID: id, Cache: cache, StoreHash: hash, CreatedAt: pruneNow.Add(-age)} } func (pt *pruneTest) publishing(rows ...db.NixPublishing) *fakeReservations { res := &fakeReservations{} pt.pruner.Reservations = res pt.pruner.Publishing = rows return res } func wantIDs(t *testing.T, what string, got []string, want ...string) { t.Helper() if len(got) != len(want) || len(want) > 0 && got[0] != want[0] { t.Errorf("%s = %v, want %v", what, got, want) } } func TestResolvePublishingCommitsPresentReleasesOldMissingAndKeepsYoungMissing(t *testing.T) { pt := newPruneTest(t) pt.attic.store(repoA, hash('a')) res := pt.publishing( publishingRow("present", repoA, hash('a'), time.Minute), publishingRow("old", repoA, hash('b'), 2*time.Hour), publishingRow("young", repoA, hash('c'), time.Minute), ) pt.pruner.resolvePublishing(context.Background()) committed, released := res.settled() wantIDs(t, "committed", committed, "present") wantIDs(t, "released", released, "old") if len(pt.pruner.Publishing) != 1 || pt.pruner.Publishing[0].ReservationID != "young" { t.Errorf("kept = %+v, want only the young one", pt.pruner.Publishing) } if len(pt.attic.missingCalls) != 1 { t.Errorf("attic got %d calls, want one per cache", len(pt.attic.missingCalls)) } } func TestResolvePublishingWaitsOutTheLongestLiveToken(t *testing.T) { pt := newPruneTest(t) pt.pruner.Grace = 5 * time.Minute pt.pruner.Grants = liveGrants(3 * time.Hour) res := pt.publishing(publishingRow("missing", repoA, hash('b'), 2*time.Hour)) pt.pruner.resolvePublishing(context.Background()) if _, released := res.settled(); len(released) != 0 || len(pt.pruner.Publishing) != 1 { t.Fatalf("released %v while a token that could still finish the upload is live, want it kept", released) } } func TestResolvePublishingKeepsEverythingWhileAtticIsDownThenSettlesIt(t *testing.T) { pt := newPruneTest(t) cacheA, _ := CacheName(repoA) cacheB, _ := CacheName(repoB) pt.attic.store(repoA, hash('a')) pt.attic.failCache = map[string]int{cacheA: http.StatusBadGateway, cacheB: http.StatusBadGateway} res := pt.publishing( publishingRow("a", repoA, hash('a'), time.Minute), publishingRow("b", repoB, hash('b'), 2*time.Hour), ) pt.pruner.resolvePublishing(context.Background()) if committed, released := res.settled(); len(committed)+len(released) != 0 || len(pt.pruner.Publishing) != 2 { t.Fatalf("settled %v and %v with attic down, kept %d, want nothing settled and both kept", committed, released, len(pt.pruner.Publishing)) } if len(pt.attic.missingCalls) != 1 { t.Errorf("attic got %d calls %v, want the pass to end after the first", len(pt.attic.missingCalls), pt.attic.missingCalls) } pt.attic.failCache = nil pt.pruner.resolvePublishing(context.Background()) committed, released := res.settled() wantIDs(t, "committed", committed, "a") wantIDs(t, "released", released, "b") if len(pt.pruner.Publishing) != 0 { t.Errorf("kept = %+v, want none", pt.pruner.Publishing) } } func TestResolvePublishingKeepsOnlyTheCacheAtticRefuses(t *testing.T) { pt := newPruneTest(t) cacheA, _ := CacheName(repoA) pt.attic.store(repoA, hash('a')) pt.attic.store(repoB, hash('b')) pt.attic.failCache = map[string]int{cacheA: http.StatusForbidden} res := pt.publishing( publishingRow("a", repoA, hash('a'), time.Minute), publishingRow("b", repoB, hash('b'), time.Minute), ) pt.pruner.resolvePublishing(context.Background()) committed, _ := res.settled() wantIDs(t, "committed", committed, "b") if len(pt.pruner.Publishing) != 1 || pt.pruner.Publishing[0].ReservationID != "a" { t.Errorf("kept = %+v, want the refused cache's row", pt.pruner.Publishing) } } func TestStartWithoutAnIntervalSettlesPublishingNowAndAfterGrace(t *testing.T) { pt := newPruneTest(t) pt.pruner.MaxBytes = 1 pt.add(repoA, ownerX, hash('z'), 100, 3*day) pt.attic.store(repoA, hash('a')) pt.pruner.Grace = 300 * time.Millisecond pt.pruner.now = time.Now cache, _ := CacheName(repoA) res := pt.publishing( db.NixPublishing{ReservationID: "present", Cache: cache, StoreHash: hash('a'), CreatedAt: time.Now()}, db.NixPublishing{ReservationID: "missing", Cache: cache, StoreHash: hash('b'), CreatedAt: time.Now()}, ) pt.pruner.Start(t.Context(), 0) waitFor(t, "the present upload to be committed", func() bool { c, _ := res.settled(); return len(c) == 1 }) if _, released := res.settled(); len(released) != 0 { t.Fatalf("released %v before the grace passed", released) } waitFor(t, "the missing upload to be released", func() bool { _, r := res.settled(); return len(r) == 1 }) if len(pt.attic.deletes) != 0 { t.Errorf("delete calls = %v, want no pruning with eviction off", pt.attic.deletes) } } func waitFor(t *testing.T, what string, done func() bool) { t.Helper() for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(10 * time.Millisecond) { if done() { return } } t.Fatalf("timed out waiting for %s", what) }