diff --git a/sandboxd/pool/pool.go b/sandboxd/pool/pool.go index ffda09f1f..8ebbaff33 100644 --- a/sandboxd/pool/pool.go +++ b/sandboxd/pool/pool.go @@ -226,7 +226,7 @@ type pool struct { removed bool // dropped from the desired set while unsettled; swept by refillOnce warm []*types.Sandbox refilling int - claimClones int + claimClones atomic.Int32 // Without a streak gate a node-wide dead cause is retried every tick at full concurrency refillFails int @@ -238,7 +238,7 @@ func newPool(key types.PoolKey) *pool { } func (p *pool) settled() bool { - return !p.building && p.refilling == 0 && p.claimClones == 0 + return !p.building && p.refilling == 0 && p.claimClones.Load() == 0 } // refillGated reports whether a refill may not spawn: backoff wait, or one probe in flight. diff --git a/sandboxd/pool/pool_test.go b/sandboxd/pool/pool_test.go index b1304f4d7..6253b5462 100644 --- a/sandboxd/pool/pool_test.go +++ b/sandboxd/pool/pool_test.go @@ -511,7 +511,7 @@ func TestARemovedPoolKeepsItsGoldenUntilItSettles(t *testing.T) { }{ {"build", func(p *pool) { p.building = true }}, {"refill", func(p *pool) { p.refilling = 1 }}, - {"claim clone", func(p *pool) { p.claimClones = 1 }}, + {"claim clone", func(p *pool) { p.claimClones.Store(1) }}, } { t.Run(tt.name, func(t *testing.T) { m := newTestManager(t, newFakeEngine(), config.PoolSpec{PoolKey: testKey, Warm: 0}) @@ -528,7 +528,8 @@ func TestARemovedPoolKeepsItsGoldenUntilItSettles(t *testing.T) { t.Fatalf("golden deleted while the removed pool's %s was in flight", tt.name) } m.mu.Lock() - p.building, p.refilling, p.claimClones = false, 0, 0 + p.building, p.refilling = false, 0 + p.claimClones.Store(0) m.mu.Unlock() m.refillOnce(t.Context()) for _, path := range []string{g, g + goldenStampSuffix} { diff --git a/sandboxd/pool/template.go b/sandboxd/pool/template.go index 7578e81ae..5570bb2e4 100644 --- a/sandboxd/pool/template.go +++ b/sandboxd/pool/template.go @@ -328,10 +328,14 @@ type goldenResolution struct { source types.PoolKey promoted bool guestEnv bool + held *pool unlock func() } func (g goldenResolution) release() { + if g.held != nil { + g.held.claimClones.Add(-1) + } if g.unlock != nil { g.unlock() } @@ -344,11 +348,11 @@ func (m *Manager) resolveGolden(ctx context.Context, key types.PoolKey, tenant s p, ok := m.activePool(key) if ok && p.goldenDir != "" { dir = p.goldenDir - p.claimClones++ + p.claimClones.Add(1) } m.mu.Unlock() if dir != "" { - return goldenResolution{dir: dir, unlock: func() { m.mu.Lock(); p.claimClones--; m.mu.Unlock() }}, nil + return goldenResolution{dir: dir, held: p}, nil } if key.Net == types.NetEgress { return goldenResolution{}, nil // never resume a live-captured template on the egress lane; cold-boot instead