mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-10 08:09:17 -05:00
194 lines
7.1 KiB
Go
194 lines
7.1 KiB
Go
|
|
package server
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"database/sql"
|
||
|
|
"io"
|
||
|
|
"log/slog"
|
||
|
|
"slices"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/git-pkgs/proxy/internal/database"
|
||
|
|
"github.com/git-pkgs/proxy/internal/storage"
|
||
|
|
)
|
||
|
|
|
||
|
|
// seedMetadata caches a 500-byte metadata document last used at
|
||
|
|
// accessedAt and returns its storage path.
|
||
|
|
func seedMetadata(t *testing.T, ctx context.Context, db *database.DB, store storage.Storage, key string, accessedAt time.Time) string {
|
||
|
|
t.Helper()
|
||
|
|
path := "_metadata/golang/" + key + "/metadata"
|
||
|
|
size, _, err := store.Store(ctx, path, strings.NewReader(strings.Repeat("m", 500)))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("storing metadata: %v", err)
|
||
|
|
}
|
||
|
|
if err := db.UpsertMetadataCache(&database.MetadataCacheEntry{
|
||
|
|
Ecosystem: "golang",
|
||
|
|
Name: key,
|
||
|
|
StoragePath: path,
|
||
|
|
Size: sql.NullInt64{Int64: size, Valid: true},
|
||
|
|
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
||
|
|
}); err != nil {
|
||
|
|
t.Fatalf("upserting metadata: %v", err)
|
||
|
|
}
|
||
|
|
query := db.Rebind(`UPDATE metadata_cache SET last_accessed_at = ? WHERE ecosystem = ? AND name = ?`)
|
||
|
|
if _, err := db.Exec(query, accessedAt.UTC(), "golang", key); err != nil {
|
||
|
|
t.Fatalf("setting last access: %v", err)
|
||
|
|
}
|
||
|
|
return path
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestEvictLRU_CountsAndEvictsMetadata(t *testing.T) {
|
||
|
|
db, store := setupEvictionTest(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||
|
|
now := time.Now()
|
||
|
|
|
||
|
|
oldTile := seedMetadata(t, ctx, db, store, "tile-old", now.Add(-3*time.Hour))
|
||
|
|
seedArtifact(t, ctx, db, store, "mid-pkg", 500, now.Add(-2*time.Hour))
|
||
|
|
newTile := seedMetadata(t, ctx, db, store, "tile-new", now)
|
||
|
|
|
||
|
|
// Artifacts alone are within the limit; with metadata the cache is
|
||
|
|
// 1500 bytes, so the least recently used entry has to go, and that is
|
||
|
|
// the old metadata document.
|
||
|
|
evictLRU(ctx, db, store, logger, 1100)
|
||
|
|
|
||
|
|
if entry, _ := db.GetMetadataCache("golang", "tile-old"); entry != nil {
|
||
|
|
t.Error("least recently used metadata row was kept")
|
||
|
|
}
|
||
|
|
if ok, _ := store.Exists(ctx, oldTile); ok {
|
||
|
|
t.Error("least recently used metadata blob was kept")
|
||
|
|
}
|
||
|
|
if entry, _ := db.GetMetadataCache("golang", "tile-new"); entry == nil {
|
||
|
|
t.Error("recently used metadata row was evicted")
|
||
|
|
}
|
||
|
|
if ok, _ := store.Exists(ctx, newTile); !ok {
|
||
|
|
t.Error("recently used metadata blob was deleted")
|
||
|
|
}
|
||
|
|
art, err := db.GetArtifact("pkg:npm/mid-pkg@1.0.0", "mid-pkg-1.0.0.tgz")
|
||
|
|
if err != nil || art == nil || !art.StoragePath.Valid {
|
||
|
|
t.Errorf("artifact used after the old metadata was evicted: %+v, %v", art, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestEvictLRU_EvictsArtifactBeforeNewerMetadata(t *testing.T) {
|
||
|
|
db, store := setupEvictionTest(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||
|
|
now := time.Now()
|
||
|
|
|
||
|
|
seedArtifact(t, ctx, db, store, "old-pkg", 500, now.Add(-3*time.Hour))
|
||
|
|
seedMetadata(t, ctx, db, store, "tile", now)
|
||
|
|
|
||
|
|
evictLRU(ctx, db, store, logger, 600)
|
||
|
|
|
||
|
|
art, err := db.GetArtifact("pkg:npm/old-pkg@1.0.0", "old-pkg-1.0.0.tgz")
|
||
|
|
if err != nil || art == nil || art.StoragePath.Valid {
|
||
|
|
t.Errorf("older artifact kept: %+v, %v", art, err)
|
||
|
|
}
|
||
|
|
if entry, _ := db.GetMetadataCache("golang", "tile"); entry == nil {
|
||
|
|
t.Error("newer metadata evicted although evicting the artifact was enough")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestEvictBatch_KeepsMetadataUsedSinceRead evicts from a row read before a
|
||
|
|
// request served the document again. The row and its blob stay, and nothing
|
||
|
|
// is counted as freed.
|
||
|
|
func TestEvictBatch_KeepsMetadataUsedSinceRead(t *testing.T) {
|
||
|
|
db, store := setupEvictionTest(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||
|
|
path := seedMetadata(t, ctx, db, store, "tile", time.Now().Add(-time.Hour))
|
||
|
|
|
||
|
|
stale, err := db.GetLeastRecentlyUsedCacheEntries(evictionBatch)
|
||
|
|
if err != nil || len(stale) != 1 {
|
||
|
|
t.Fatalf("reading LRU rows: %v, %v", stale, err)
|
||
|
|
}
|
||
|
|
if err := db.RecordMetadataHit("golang", "tile"); err != nil {
|
||
|
|
t.Fatalf("recording hit: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
cleared, freed := evictBatch(ctx, db, store, logger, stale, 0, 500)
|
||
|
|
if cleared != 0 || freed != 0 {
|
||
|
|
t.Errorf("cleared %d records freeing %d bytes, want nothing", cleared, freed)
|
||
|
|
}
|
||
|
|
if entry, _ := db.GetMetadataCache("golang", "tile"); entry == nil {
|
||
|
|
t.Error("metadata row served since it was read was evicted")
|
||
|
|
}
|
||
|
|
if ok, _ := store.Exists(ctx, path); !ok {
|
||
|
|
t.Error("metadata blob served since it was read was deleted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestEvictLRU_MetadataLeavesNothingQueued checks that a completed eviction
|
||
|
|
// takes the path it queued with the row's delete off the queue again.
|
||
|
|
func TestEvictLRU_MetadataLeavesNothingQueued(t *testing.T) {
|
||
|
|
db, store := setupEvictionTest(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||
|
|
path := seedMetadata(t, ctx, db, store, "tile", time.Now().Add(-time.Hour))
|
||
|
|
|
||
|
|
evictLRU(ctx, db, store, logger, 0)
|
||
|
|
|
||
|
|
if ok, _ := store.Exists(ctx, path); ok {
|
||
|
|
t.Error("metadata blob kept")
|
||
|
|
}
|
||
|
|
if got := queuedPaths(t, db); len(got) != 0 {
|
||
|
|
t.Errorf("queue = %v, want it empty after the delete", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestEvictMetadata_InterruptedBeforeDeleteIsReclaimed stops an eviction
|
||
|
|
// after the row is gone but before the blob is deleted, as a crash would. The
|
||
|
|
// path is queued, so reclaim still deletes the blob.
|
||
|
|
func TestEvictMetadata_InterruptedBeforeDeleteIsReclaimed(t *testing.T) {
|
||
|
|
db, store := setupEvictionTest(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||
|
|
path := seedMetadata(t, ctx, db, store, "tile", time.Now().Add(-time.Hour))
|
||
|
|
|
||
|
|
entries, err := db.GetLeastRecentlyUsedCacheEntries(evictionBatch)
|
||
|
|
if err != nil || len(entries) != 1 {
|
||
|
|
t.Fatalf("reading LRU rows: %v, %v", entries, err)
|
||
|
|
}
|
||
|
|
e := entries[0]
|
||
|
|
if ok, err := db.EvictMetadataCache(e.Key, e.Name, path, e.LastAccessedAt); err != nil || !ok {
|
||
|
|
t.Fatalf("EvictMetadataCache = %v, %v", ok, err)
|
||
|
|
}
|
||
|
|
if got := queuedPaths(t, db); !slices.Equal(got, []string{path}) {
|
||
|
|
t.Fatalf("queue = %v, want the evicted path", got)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Nothing left for a restarted eviction pass to retry; reclaim takes it.
|
||
|
|
if again, _ := db.GetLeastRecentlyUsedCacheEntries(evictionBatch); len(again) != 0 {
|
||
|
|
t.Errorf("LRU after interruption = %+v, want nothing", again)
|
||
|
|
}
|
||
|
|
reclaimDue(ctx, db, store, logger, time.Now().Add(time.Hour), time.Minute)
|
||
|
|
if objectExists(t, store, path) {
|
||
|
|
t.Error("blob still stored after reclaim")
|
||
|
|
}
|
||
|
|
if got := queuedPaths(t, db); len(got) != 0 {
|
||
|
|
t.Errorf("queue = %v, want it drained", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestBackfillMetadataAccessDrainsInBatches(t *testing.T) {
|
||
|
|
db, _ := setupEvictionTest(t)
|
||
|
|
for _, name := range []string{"a", "b", "c", "d", "e"} {
|
||
|
|
query := db.Rebind(`INSERT INTO metadata_cache (ecosystem, name, storage_path, size, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`)
|
||
|
|
if _, err := db.Exec(query, "npm", name, "_metadata/npm/"+name+"/metadata", 1, time.Now(), time.Now()); err != nil {
|
||
|
|
t.Fatalf("inserting: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
backfillMetadataAccess(context.Background(), db, slog.New(slog.NewTextHandler(io.Discard, nil)), 2)
|
||
|
|
|
||
|
|
var left int
|
||
|
|
if err := db.Get(&left, `SELECT COUNT(*) FROM metadata_cache WHERE last_accessed_at IS NULL`); err != nil {
|
||
|
|
t.Fatalf("counting: %v", err)
|
||
|
|
}
|
||
|
|
if left != 0 {
|
||
|
|
t.Errorf("%d rows left without an access time", left)
|
||
|
|
}
|
||
|
|
}
|