Files
mcr/internal/db/gc_test.go
Kyle Isom 562b69e875 Phase 9: two-phase garbage collection engine
GC engine (internal/gc/): Collector.Run() implements the two-phase
algorithm — Phase 1 finds unreferenced blobs and deletes DB rows in
a single transaction, Phase 2 deletes blob files from storage.
Registry-wide mutex blocks concurrent GC runs. Collector.Reconcile()
scans filesystem for orphaned files with no DB row (crash recovery).

Wired into admin_gc.go: POST /v1/gc now launches the real collector
in a goroutine with gc_started/gc_completed audit events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 20:27:17 -07:00

112 lines
2.9 KiB
Go

package db
import "testing"
func TestFindAndDeleteUnreferencedBlobs(t *testing.T) {
d := openTestDB(t)
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate: %v", err)
}
// Setup: repo, manifest, two blobs. One referenced, one not.
_, err := d.Exec(`INSERT INTO repositories (name) VALUES ('testrepo')`)
if err != nil {
t.Fatalf("insert repo: %v", err)
}
_, err = d.Exec(`INSERT INTO manifests (repository_id, digest, media_type, content, size)
VALUES (1, 'sha256:m1', 'application/vnd.oci.image.manifest.v1+json', '{}', 2)`)
if err != nil {
t.Fatalf("insert manifest: %v", err)
}
// Referenced blob.
_, err = d.Exec(`INSERT INTO blobs (digest, size) VALUES ('sha256:referenced', 100)`)
if err != nil {
t.Fatalf("insert referenced blob: %v", err)
}
_, err = d.Exec(`INSERT INTO manifest_blobs (manifest_id, blob_id) VALUES (1, 1)`)
if err != nil {
t.Fatalf("insert manifest_blob: %v", err)
}
// Unreferenced blob.
_, err = d.Exec(`INSERT INTO blobs (digest, size) VALUES ('sha256:unreferenced', 500)`)
if err != nil {
t.Fatalf("insert unreferenced blob: %v", err)
}
unreferenced, err := d.FindAndDeleteUnreferencedBlobs()
if err != nil {
t.Fatalf("FindAndDeleteUnreferencedBlobs: %v", err)
}
if len(unreferenced) != 1 {
t.Fatalf("unreferenced count: got %d, want 1", len(unreferenced))
}
if unreferenced[0].Digest != "sha256:unreferenced" {
t.Fatalf("unreferenced digest: got %q", unreferenced[0].Digest)
}
if unreferenced[0].Size != 500 {
t.Fatalf("unreferenced size: got %d, want 500", unreferenced[0].Size)
}
// Verify unreferenced blob row was deleted.
exists, err := d.BlobExists("sha256:unreferenced")
if err != nil {
t.Fatalf("BlobExists: %v", err)
}
if exists {
t.Fatal("unreferenced blob should have been deleted from DB")
}
// Verify referenced blob still exists.
exists, err = d.BlobExists("sha256:referenced")
if err != nil {
t.Fatalf("BlobExists: %v", err)
}
if !exists {
t.Fatal("referenced blob should still exist")
}
}
func TestFindAndDeleteUnreferencedBlobsNone(t *testing.T) {
d := openTestDB(t)
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate: %v", err)
}
unreferenced, err := d.FindAndDeleteUnreferencedBlobs()
if err != nil {
t.Fatalf("FindAndDeleteUnreferencedBlobs: %v", err)
}
if unreferenced != nil {
t.Fatalf("expected nil, got %v", unreferenced)
}
}
func TestBlobExistsByDigest(t *testing.T) {
d := openTestDB(t)
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate: %v", err)
}
_, err := d.Exec(`INSERT INTO blobs (digest, size) VALUES ('sha256:exists', 100)`)
if err != nil {
t.Fatalf("insert blob: %v", err)
}
exists, err := d.BlobExistsByDigest("sha256:exists")
if err != nil {
t.Fatalf("BlobExistsByDigest: %v", err)
}
if !exists {
t.Fatal("expected blob to exist")
}
exists, err = d.BlobExistsByDigest("sha256:nope")
if err != nil {
t.Fatalf("BlobExistsByDigest (nope): %v", err)
}
if exists {
t.Fatal("expected blob to not exist")
}
}