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)
}