~james/core

core/spindle/engine/cache_evict_test.go -rw-r--r-- 4.9 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 14 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package engine

import (
	"context"
	"slices"
	"testing"
	"time"

	"tangled.org/core/spindle/db"
	"tangled.org/core/spindle/nixcache"
	"tangled.org/core/spindle/quota"
)

func readyCharged(t *testing.T, d *db.DB, qs *db.QuotaStore, entry db.CacheEntry, size int64) {
	t.Helper()
	insertCacheTestEntry(t, d, entry)
	if _, err := d.MarkCacheEntryReady(context.Background(), entry.ID, size, qs, entry.LastUsedAt); err != nil {
		t.Fatal(err)
	}
}

func TestCacheEntriesKindSkipsPinnedAndTouchedEntries(t *testing.T) {
	ctx := context.Background()
	d := newCacheTestDB(t)
	qs := db.NewQuotaStore(d, nil)
	base := time.Now().Add(-time.Hour)
	for i, id := range []string{"touched", "restoring", "cold"} {
		readyCharged(t, d, qs, cacheTestEntry(id, "did:plc:repo", "microvm", id, "h", "pending", base.Add(time.Duration(i)*time.Minute)), 10)
	}
	readyCharged(t, d, qs, cacheTestEntry("other-repo", "did:plc:other", "microvm", "k", "h", "pending", base), 10)
	if pinned, err := d.BeginCacheRestore(ctx, "restoring"); err != nil || !pinned {
		t.Fatal(pinned, err)
	}
	store := &fakeStorage{objects: map[string][]byte{"objects/touched": []byte("t"), "objects/cold": []byte("c")}}
	kind := CacheEntries(d, store)

	listed, err := kind.Evictable(ctx, quota.Subject{Scope: quota.ScopeRepo, DID: "did:plc:repo"}, 10)
	if err != nil || len(listed) != 2 || listed[0].Key != "touched" || listed[1].Key != "cold" {
		t.Fatalf("listed %v, %v; want the repo's unpinned entries oldest first", listed, err)
	}
	owned, err := kind.Evictable(ctx, quota.Subject{Scope: quota.ScopeUser, DID: "did:plc:owner"}, 10)
	if err != nil || len(owned) != 3 {
		t.Fatalf("owner listed %v, %v; want both repos' unpinned entries", owned, err)
	}
	if err := d.TouchCacheEntry(ctx, "touched", time.Now()); err != nil {
		t.Fatal(err)
	}

	gone, err := kind.Evict(ctx, listed)
	if err != nil || len(gone) != 1 || gone[0].Key != "cold" {
		t.Fatalf("Evict = %v, %v; want only cold", gone, err)
	}
	if !cacheTestEntryExists(t, d, "touched") || !store.has("objects/touched") || !genericCharged(t, d, "touched") {
		t.Fatal("evicted an entry restored after it was listed")
	}
	if cacheTestEntryExists(t, d, "cold") || store.has("objects/cold") || genericCharged(t, d, "cold") {
		t.Fatal("cold entry left its row, object or charge")
	}
}

type fakeAttic struct{ deleted []string }

func (a *fakeAttic) DeletePaths(_ context.Context, _ string, hashes []string) ([]string, error) {
	a.deleted = append(a.deleted, hashes...)
	return hashes, nil
}

func (a *fakeAttic) MissingPaths(context.Context, string, []string) ([]string, error) {
	return nil, nil
}

func TestEvictorTrimsNixPathsAndCacheEntriesInOneOrder(t *testing.T) {
	ctx := context.Background()
	d := newCacheTestDB(t)
	qs := db.NewQuotaStore(d, quota.Defaults{quota.ScopeRepo: {quota.ResourceCacheStorageBytes: 30}})
	id := quota.Identity{OwnerDID: "did:plc:owner", RepoDID: "did:plc:repo"}
	now := time.Now()
	nixPath := func(hash string, ago time.Duration) {
		t.Helper()
		res, err := qs.Reserve(ctx, quota.ReserveRequest{
			Kind: quota.KindNixCache, Key: quota.NixCacheKey("cache", hash), Identity: id,
			Resources: quota.Resources{quota.ResourceCacheStorageBytes: 10},
			NixObject: &quota.NixObject{Cache: "cache", StoreHash: hash, StorePath: "/nix/store/" + hash + "-x", NarSize: 10},
		})
		if err != nil || !res.Allowed {
			t.Fatal(res, err)
		}
		if err := qs.Commit(ctx, res.ID); err != nil {
			t.Fatal(err)
		}
		if _, err := d.Exec(`update nix_objects set last_used_at = ? where store_hash = ?`, now.Add(-ago).Unix(), hash); err != nil {
			t.Fatal(err)
		}
	}
	nixPath("nix-oldest", 5*time.Hour)
	readyCharged(t, d, qs, cacheTestEntry("generic-old", id.RepoDID, "microvm", "a", "h", "pending", now.Add(-4*time.Hour)), 10)
	nixPath("nix-new", 2*time.Hour)
	readyCharged(t, d, qs, cacheTestEntry("generic-new", id.RepoDID, "microvm", "b", "h", "pending", now.Add(-time.Hour)), 10)

	attic := &fakeAttic{}
	store := &fakeStorage{objects: map[string][]byte{"objects/generic-old": []byte("o"), "objects/generic-new": []byte("n")}}
	evictor := &quota.Evictor{
		Usage:  qs,
		Limits: qs,
		Kinds: []quota.CacheKind{
			CacheEntries(d, store),
			&nixcache.Pruner{Index: qs, Attic: attic},
		},
	}
	if n, err := evictor.Evict(ctx); err != nil || n != 1 {
		t.Fatalf("Evict = %d, %v; want 1 (40 bytes down to 30)", n, err)
	}
	if !slices.Equal(attic.deleted, []string{"nix-oldest"}) || !cacheTestEntryExists(t, d, "generic-old") {
		t.Fatalf("attic deleted %v, want only the oldest path of either kind", attic.deleted)
	}

	if err := qs.SetLimit(ctx, id.RepoDID, quota.ResourceCacheStorageBytes, 15); err != nil {
		t.Fatal(err)
	}
	if n, err := evictor.Evict(ctx); err != nil || n != 2 {
		t.Fatalf("Evict = %d, %v; want 2 (30 bytes down to 10)", n, err)
	}
	if !slices.Equal(attic.deleted, []string{"nix-oldest", "nix-new"}) || cacheTestEntryExists(t, d, "generic-old") || !cacheTestEntryExists(t, d, "generic-new") {
		t.Fatalf("attic deleted %v; want the next two oldest gone and generic-new kept", attic.deleted)
	}
}