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>
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcr/internal/gc"
|
||||
)
|
||||
|
||||
// GCLastRun records the result of the last GC run.
|
||||
@@ -17,9 +22,11 @@ type GCLastRun struct {
|
||||
|
||||
// GCState tracks the current state of garbage collection.
|
||||
type GCState struct {
|
||||
mu sync.Mutex
|
||||
Running bool `json:"running"`
|
||||
LastRun *GCLastRun `json:"last_run,omitempty"`
|
||||
mu sync.Mutex
|
||||
Running bool `json:"running"`
|
||||
LastRun *GCLastRun `json:"last_run,omitempty"`
|
||||
Collector *gc.Collector
|
||||
AuditFn AuditFunc
|
||||
}
|
||||
|
||||
type gcStatusResponse struct {
|
||||
@@ -43,10 +50,51 @@ func AdminTriggerGCHandler(state *GCState) http.HandlerFunc {
|
||||
state.Running = true
|
||||
state.mu.Unlock()
|
||||
|
||||
// GC engine is Phase 9 -- for now, just mark as running and return.
|
||||
// The actual GC goroutine will be wired up in Phase 9.
|
||||
gcID := uuid.New().String()
|
||||
|
||||
// Run GC asynchronously.
|
||||
go func() {
|
||||
startedAt := time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
if state.AuditFn != nil {
|
||||
state.AuditFn("gc_started", "", "", "", "", map[string]string{
|
||||
"gc_id": gcID,
|
||||
})
|
||||
}
|
||||
|
||||
var result *gc.Result
|
||||
var gcErr error
|
||||
if state.Collector != nil {
|
||||
result, gcErr = state.Collector.Run(context.Background())
|
||||
}
|
||||
|
||||
completedAt := time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
state.mu.Lock()
|
||||
state.Running = false
|
||||
lastRun := &GCLastRun{
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: completedAt,
|
||||
}
|
||||
if result != nil {
|
||||
lastRun.BlobsRemoved = result.BlobsRemoved
|
||||
lastRun.BytesFreed = result.BytesFreed
|
||||
}
|
||||
state.LastRun = lastRun
|
||||
state.mu.Unlock()
|
||||
|
||||
if state.AuditFn != nil && gcErr == nil {
|
||||
details := map[string]string{
|
||||
"gc_id": gcID,
|
||||
}
|
||||
if result != nil {
|
||||
details["blobs_removed"] = fmt.Sprintf("%d", result.BlobsRemoved)
|
||||
details["bytes_freed"] = fmt.Sprintf("%d", result.BytesFreed)
|
||||
}
|
||||
state.AuditFn("gc_completed", "", "", "", "", details)
|
||||
}
|
||||
}()
|
||||
|
||||
writeJSON(w, http.StatusAccepted, gcTriggerResponse{ID: gcID})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user