This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / spindle / engine / cache_prune_test.go
4.9 kB 152 lines
1package engine 2 3import ( 4 "context" 5 "errors" 6 "slices" 7 "testing" 8 "time" 9) 10 11func TestPruneCachesExpiryPolicies(t *testing.T) { 12 ctx := context.Background() 13 now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC) 14 type spec struct { 15 id string 16 state string 17 age time.Duration 18 } 19 tests := []struct { 20 name string 21 retention time.Duration 22 pendingMax time.Duration 23 entries []spec 24 wantPruned int 25 wantSurvive []string 26 }{ 27 { 28 name: "ready entries expire by last use", 29 retention: time.Hour, 30 pendingMax: 15 * time.Minute, 31 entries: []spec{{"old", "ready", 2 * time.Hour}, {"fresh", "ready", 30 * time.Minute}}, 32 wantPruned: 1, 33 wantSurvive: []string{"fresh"}, 34 }, 35 { 36 name: "zero retention keeps ready entries", 37 retention: 0, 38 pendingMax: time.Hour, 39 entries: []spec{{"ready", "ready", 24 * time.Hour}, {"pending", "pending", 2 * time.Hour}}, 40 wantPruned: 1, 41 wantSurvive: []string{"ready"}, 42 }, 43 { 44 name: "pending entries expire by age", 45 retention: time.Hour, 46 pendingMax: time.Hour, 47 entries: []spec{{"old", "pending", 2 * time.Hour}, {"fresh", "pending", 10 * time.Minute}}, 48 wantPruned: 1, 49 wantSurvive: []string{"fresh"}, 50 }, 51 } 52 for _, tt := range tests { 53 t.Run(tt.name, func(t *testing.T) { 54 d := newCacheTestDB(t) 55 store := &fakeStorage{objects: map[string][]byte{}} 56 keys := map[string]string{} 57 for _, e := range tt.entries { 58 entry := cacheTestEntry(e.id, "did:plc:repo", "microvm", "deps", "hash", e.state, now.Add(-e.age)) 59 insertCacheTestEntry(t, d, entry) 60 store.objects[entry.StorageKey] = []byte(e.id) 61 keys[e.id] = entry.StorageKey 62 } 63 64 pruned, err := PruneCaches(ctx, d, store, now, tt.retention, tt.pendingMax, 10) 65 if err != nil { 66 t.Fatalf("PruneCaches: %v", err) 67 } 68 if pruned != tt.wantPruned { 69 t.Fatalf("pruned %d entries, want %d", pruned, tt.wantPruned) 70 } 71 for _, e := range tt.entries { 72 survives := slices.Contains(tt.wantSurvive, e.id) 73 if store.has(keys[e.id]) != survives || cacheTestEntryExists(t, d, e.id) != survives { 74 t.Fatalf("entry %q survived = %v, want %v", e.id, !survives, survives) 75 } 76 } 77 }) 78 } 79} 80 81func TestPruneCachesSkipsEntryRefreshedAfterScan(t *testing.T) { 82 ctx := context.Background() 83 d := newCacheTestDB(t) 84 now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC) 85 first := cacheTestEntry("first", "did:plc:repo", "microvm", "deps", "first", "ready", now.Add(-3*time.Hour)) 86 refreshed := cacheTestEntry("refreshed", "did:plc:repo", "microvm", "deps", "refreshed", "ready", now.Add(-2*time.Hour)) 87 insertCacheTestEntry(t, d, first) 88 insertCacheTestEntry(t, d, refreshed) 89 store := &fakeStorage{objects: map[string][]byte{ 90 first.StorageKey: []byte("first"), 91 refreshed.StorageKey: []byte("refreshed"), 92 }} 93 store.onDelete = func(key string) { 94 if key != first.StorageKey { 95 return 96 } 97 if err := d.TouchCacheEntry(ctx, refreshed.ID, now); err != nil { 98 t.Fatalf("TouchCacheEntry: %v", err) 99 } 100 } 101 102 pruned, err := PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10) 103 if err != nil { 104 t.Fatalf("PruneCaches: %v", err) 105 } 106 if pruned != 1 { 107 t.Fatalf("pruned %d entries, want 1", pruned) 108 } 109 if !store.has(refreshed.StorageKey) || !cacheTestEntryExists(t, d, refreshed.ID) { 110 t.Fatal("entry refreshed after expiry scan was pruned") 111 } 112} 113 114func TestPruneCachesDeleteFailureRemainsRetryable(t *testing.T) { 115 for _, initialState := range []string{"ready", "deleting"} { 116 t.Run(initialState, func(t *testing.T) { 117 ctx := context.Background() 118 d := newCacheTestDB(t) 119 now := time.Date(2026, 5, 6, 7, 8, 9, 0, time.UTC) 120 entry := cacheTestEntry("retry", "did:plc:repo", "microvm", "deps", "hash", initialState, now.Add(-2*time.Hour)) 121 insertCacheTestEntry(t, d, entry) 122 deleteErr := errors.New("delete failed") 123 store := &fakeStorage{ 124 objects: map[string][]byte{entry.StorageKey: []byte("archive")}, 125 deleteErr: deleteErr, 126 } 127 128 pruned, err := PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10) 129 if !errors.Is(err, deleteErr) { 130 t.Fatalf("first PruneCaches error = %v, want %v", err, deleteErr) 131 } 132 if pruned != 0 { 133 t.Fatalf("first prune count = %d, want 0", pruned) 134 } 135 state, _, _ := cacheTestEntryState(t, d, entry.ID) 136 if state != initialState { 137 t.Fatalf("state after delete failure = %q, want %q", state, initialState) 138 } 139 if !store.has(entry.StorageKey) { 140 t.Fatal("failed delete removed object") 141 } 142 143 pruned, err = PruneCaches(ctx, d, store, now, time.Hour, time.Hour, 10) 144 if err != nil { 145 t.Fatalf("retry PruneCaches: %v", err) 146 } 147 if pruned != 1 || store.has(entry.StorageKey) || cacheTestEntryExists(t, d, entry.ID) { 148 t.Fatalf("retry result = pruned %d, object %v, metadata %v", pruned, store.has(entry.StorageKey), cacheTestEntryExists(t, d, entry.ID)) 149 } 150 }) 151 } 152}