@@ -25,41 +25,50 @@ func Register[S any](o *Orchestrator, m Module[S]) {
2525
2626// Run executes one GC cycle:
2727//
28- // 1. For each module: TryLock → ReadDB → Unlock (skip if busy).
29- // 2. Each module's Resolve analyses its typed snapshot, with other modules'
30- // snapshots available as map[string]any for cross-module analysis.
31- // 3. For each snapshotted module: TryLock → Collect → Unlock (skip if busy).
32- // Collect is called even with empty targets so modules can run housekeeping
33- // (e.g. stale temp cleanup).
28+ // 1. TryLock all modules; skip those whose lock is busy.
29+ // 2. ReadDB each locked module to build a snapshot.
30+ // 3. Resolve deletion targets per module. Each module's Resolve receives
31+ // all other snapshots (typed as any) for cross-module analysis — e.g.,
32+ // image GC checks UsedBlobIDs from the VM snapshot to protect active blobs.
33+ // 4. Collect targets for each snapshotted module.
34+ // 5. Unlock all (deferred).
3435//
35- // Step 3 re-acquires the lock rather than holding it from step 1 to keep
36- // contention minimal. commitAndRecord validates blob existence under lock
37- // before writing the index, so a deletion racing with a commit is caught
38- // there and the pull retries.
36+ // All locks are held for the entire cycle so that the snapshot, resolve, and
37+ // collect phases see a consistent view. GC runs infrequently and executes
38+ // fast, so the extended lock hold is acceptable.
3939func (o * Orchestrator ) Run (ctx context.Context ) error {
4040 logger := log .WithFunc ("gc.Run" )
4141
42- // Phase 1: collect each module's snapshot under lock .
43- snapshots := make ( map [ string ] any , len ( o . modules ))
42+ // Acquire all locks up front; hold until GC finishes .
43+ var locked [] runner
4444 for _ , m := range o .modules {
4545 ok , err := m .getLocker ().TryLock (ctx )
4646 if err != nil || ! ok {
4747 logger .Warnf (ctx , "skip %s: lock busy" , m .getName ())
4848 continue
4949 }
50- snap , readErr := m .readSnapshot (ctx )
51- m .getLocker ().Unlock (ctx ) //nolint:errcheck
52- if readErr != nil {
53- logger .Warnf (ctx , "snapshot %s: %v" , m .getName (), readErr )
50+ locked = append (locked , m )
51+ }
52+ defer func () {
53+ for _ , m := range locked {
54+ m .getLocker ().Unlock (ctx ) //nolint:errcheck
55+ }
56+ }()
57+
58+ // Phase 1: snapshot all locked modules.
59+ snapshots := make (map [string ]any , len (locked ))
60+ for _ , m := range locked {
61+ snap , err := m .readSnapshot (ctx )
62+ if err != nil {
63+ logger .Warnf (ctx , "snapshot %s: %v" , m .getName (), err )
5464 continue
5565 }
5666 snapshots [m .getName ()] = snap
5767 }
5868
59- // Phase 2: resolve deletion targets per module (no locks held).
60- // Each module sees its own snapshot typed, others as any.
69+ // Phase 2: resolve deletion targets (cross-module via snapshots).
6170 targets := make (map [string ][]string )
62- for _ , m := range o . modules {
71+ for _ , m := range locked {
6372 snap , ok := snapshots [m .getName ()]
6473 if ! ok {
6574 continue
@@ -69,23 +78,15 @@ func (o *Orchestrator) Run(ctx context.Context) error {
6978 }
7079 }
7180
72- // Phase 3: collect under lock for every snapshotted module.
73- // Collect is called even with empty targets so modules can do housekeeping.
81+ // Phase 3: collect.
7482 var errs []string
75- for _ , m := range o . modules {
83+ for _ , m := range locked {
7684 if _ , snapshotted := snapshots [m .getName ()]; ! snapshotted {
7785 continue
7886 }
79- ids := targets [m .getName ()] // nil if no targets — Collect handles this
80- ok , err := m .getLocker ().TryLock (ctx )
81- if err != nil || ! ok {
82- logger .Warnf (ctx , "skip collect %s: lock busy" , m .getName ())
83- continue
84- }
85- collectErr := m .collect (ctx , ids )
86- m .getLocker ().Unlock (ctx ) //nolint:errcheck
87- if collectErr != nil {
88- errs = append (errs , fmt .Sprintf ("%s: %v" , m .getName (), collectErr ))
87+ ids := targets [m .getName ()]
88+ if err := m .collect (ctx , ids ); err != nil {
89+ errs = append (errs , fmt .Sprintf ("%s: %v" , m .getName (), err ))
8990 }
9091 }
9192 if len (errs ) > 0 {
0 commit comments