mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 07:39:19 -05:00
Eviction deleted an object before checking its record still pointed at it. If a refetch had moved the record, that object was queued and a request that read the record earlier could still be opening it. Eviction now clears first, skips the delete if the record moved, and queues the object when the delete fails after a clear. Reclaim queues a failed delete again, which moves it behind the rest, so objects the backend keeps refusing cannot fill every batch. Also drop the clearArtifactCache passthrough and ClearCachedArtifact's unused context, note that only tests call ArtifactPath, and document that max_size does not count objects waiting to be reclaimed.
108 lines
3.4 KiB
Go
108 lines
3.4 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
|
"github.com/git-pkgs/proxy/internal/storage"
|
|
)
|
|
|
|
func storeQueued(t *testing.T, db *database.DB, store storage.Storage, path string) {
|
|
t.Helper()
|
|
if _, _, err := store.Store(context.Background(), path, strings.NewReader("superseded")); err != nil {
|
|
t.Fatalf("storing %s: %v", path, err)
|
|
}
|
|
if err := db.QueuePendingDelete(path); err != nil {
|
|
t.Fatalf("queueing %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func queuedPaths(t *testing.T, db *database.DB) []string {
|
|
t.Helper()
|
|
paths, err := db.GetDuePendingDeletes(time.Now().Add(time.Hour), 100)
|
|
if err != nil {
|
|
t.Fatalf("listing queue: %v", err)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func objectExists(t *testing.T, store storage.Storage, path string) bool {
|
|
t.Helper()
|
|
ok, err := store.Exists(context.Background(), path)
|
|
if err != nil {
|
|
t.Fatalf("checking %s: %v", path, err)
|
|
}
|
|
return ok
|
|
}
|
|
|
|
func TestReclaimStorageWaitsForGracePeriod(t *testing.T) {
|
|
db, store := setupEvictionTest(t)
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
const path = "npm/old/1.0.0/a/old-1.0.0.tgz"
|
|
storeQueued(t, db, store, path)
|
|
|
|
reclaimStorage(context.Background(), db, store, logger, time.Now().Add(-time.Hour))
|
|
if !objectExists(t, store, path) {
|
|
t.Fatal("object deleted before its grace period ended")
|
|
}
|
|
|
|
reclaimStorage(context.Background(), db, store, logger, time.Now().Add(time.Hour))
|
|
if objectExists(t, store, path) {
|
|
t.Error("object survived after its grace period ended")
|
|
}
|
|
if got := queuedPaths(t, db); len(got) != 0 {
|
|
t.Errorf("queue = %v after reclaim, want empty", got)
|
|
}
|
|
}
|
|
|
|
// TestReclaimStorageRequeuesFailedDeletes checks a refused delete goes behind
|
|
// the rest of the queue, so objects the backend keeps refusing cannot hold every
|
|
// slot in a batch.
|
|
func TestReclaimStorageRequeuesFailedDeletes(t *testing.T) {
|
|
db, store := setupEvictionTest(t)
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
const path = "npm/old/1.0.0/a/old-1.0.0.tgz"
|
|
storeQueued(t, db, store, path)
|
|
queued := time.Now().UTC().Add(-2 * time.Hour)
|
|
if _, err := db.Exec(db.Rebind(`UPDATE pending_deletes SET queued_at = ? WHERE path = ?`), queued, path); err != nil {
|
|
t.Fatalf("backdating queue entry: %v", err)
|
|
}
|
|
cutoff := time.Now().Add(-time.Hour)
|
|
|
|
undeletable := &undeletableStorage{Storage: store}
|
|
reclaimStorage(context.Background(), db, undeletable, logger, cutoff)
|
|
|
|
if got := undeletable.deletes.Load(); got != 1 {
|
|
t.Errorf("delete attempts = %d, want 1", got)
|
|
}
|
|
if got := queuedPaths(t, db); !slices.Equal(got, []string{path}) {
|
|
t.Errorf("queue = %v, want the failed path kept for retry", got)
|
|
}
|
|
if due, err := db.GetDuePendingDeletes(cutoff, 100); err != nil || len(due) != 0 {
|
|
t.Errorf("due after a failed delete = %v (err %v), want it moved behind the cutoff", due, err)
|
|
}
|
|
}
|
|
|
|
func TestReclaimStorageStopsWhenContextCanceled(t *testing.T) {
|
|
db, store := setupEvictionTest(t)
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
const path = "npm/old/1.0.0/a/old-1.0.0.tgz"
|
|
storeQueued(t, db, store, path)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
reclaimStorage(ctx, db, store, logger, time.Now().Add(time.Hour))
|
|
|
|
if !objectExists(t, store, path) {
|
|
t.Error("object deleted after the context was canceled")
|
|
}
|
|
if got := queuedPaths(t, db); !slices.Equal(got, []string{path}) {
|
|
t.Errorf("queue = %v, want the path kept", got)
|
|
}
|
|
}
|