vfs/disk: Harden pending-window test against empty-dir race
Empty-dir disk init closes initDone almost immediately, so TestDiskTierSignalMixedPendingReady can observe DiskTierReady=0 then a heartbeat that already saw ready. Register a per-root init hold so the test can keep the pending window open without slowing production. Link: #36
This commit is contained in:
+39
-2
@@ -45,6 +45,25 @@ type DiskFS struct {
|
||||
initCloseOnce sync.Once
|
||||
startupEvict func(vfs.VFS, uint) uint // passed to New (via gc.GetGCAlgorithm); invoked as last step of bg init if over cap (no post-ctor race)
|
||||
metrics *metrics.Metrics
|
||||
// initHold, if non-nil, is received on before closing initDone (test pending-window hold).
|
||||
initHold <-chan struct{}
|
||||
}
|
||||
|
||||
// initHolds is a per-root registry of optional init holds (root path -> <-chan struct{}).
|
||||
// Tests call RegisterInitHold before New so that instance copies the channel and waits
|
||||
// before closing initDone; production never registers, so init is unchanged.
|
||||
var initHolds sync.Map
|
||||
|
||||
// RegisterInitHold registers a channel that DiskFS.New for this root copies onto that
|
||||
// instance. calculateSizeAndPopulateIndex receives on it before closing initDone, so
|
||||
// tests can observe the pending-attach window. Other DiskFS roots are unaffected.
|
||||
func RegisterInitHold(root string, ch <-chan struct{}) {
|
||||
initHolds.Store(root, ch)
|
||||
}
|
||||
|
||||
// ClearInitHold removes a previously registered hold for root.
|
||||
func ClearInitHold(root string) {
|
||||
initHolds.Delete(root)
|
||||
}
|
||||
|
||||
// shardPath converts a Steam cache key to a sharded directory path to reduce inode pressure
|
||||
@@ -129,6 +148,12 @@ func New(root string, capacity int64, evict func(vfs.VFS, uint) uint) (*DiskFS,
|
||||
startupEvict: evict,
|
||||
}
|
||||
|
||||
if v, ok := initHolds.Load(root); ok {
|
||||
if ch, ok := v.(<-chan struct{}); ok {
|
||||
d.initHold = ch
|
||||
}
|
||||
}
|
||||
|
||||
d.initDone = make(chan struct{})
|
||||
// Launch heavy population asynchronously so New returns fast (scans millions of files without blocking ctor or using O(N) temp RAM).
|
||||
// The initDone barrier ensures first Size() and subsequent ops (including late tier attach) see fully populated + post-eviction state.
|
||||
@@ -152,7 +177,7 @@ func (d *DiskFS) calculateSizeAndPopulateIndex() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Logger.Error().Interface("recovered_panic", r).Msg("calculateSizeAndPopulateIndex panicked; ensuring initDone closed to unblock Size waiters and prevent hang")
|
||||
}
|
||||
d.initCloseOnce.Do(func() { close(d.initDone) })
|
||||
d.closeInitDone()
|
||||
}()
|
||||
|
||||
tstart := time.Now()
|
||||
@@ -243,7 +268,19 @@ func (d *DiskFS) calculateSizeAndPopulateIndex() {
|
||||
|
||||
// Signal readiness: Size() and callers (late tier attach + Evict*) now see correct populated + post-eviction state.
|
||||
// Use Once (recover path also uses it) to guarantee exactly one close even under panic.
|
||||
d.initCloseOnce.Do(func() { close(d.initDone) })
|
||||
d.closeInitDone()
|
||||
}
|
||||
|
||||
// closeInitDone receives on a copied test hold (if any) then closes initDone once.
|
||||
// Both the normal end of calculateSizeAndPopulateIndex and the panic-recovery defer
|
||||
// call this so Size() waiters unblock in either path.
|
||||
func (d *DiskFS) closeInitDone() {
|
||||
d.initCloseOnce.Do(func() {
|
||||
if d.initHold != nil {
|
||||
<-d.initHold
|
||||
}
|
||||
close(d.initDone)
|
||||
})
|
||||
}
|
||||
|
||||
// insertBatch populates info/LRU under lock for a bounded batch (follows maxEvictBatch pattern for short critical sections).
|
||||
|
||||
@@ -662,3 +662,57 @@ func TestDiskFS_NewMkdirError(t *testing.T) {
|
||||
t.Errorf("expected mkdir failure error for file-as-dir, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDiskFS_InitHoldBlocksOnlyRegisteredRoot covers the per-root init hold:
|
||||
// Size() stays blocked while the hold is open, and a DiskFS on a different root
|
||||
// does not wait on that hold.
|
||||
func TestDiskFS_InitHoldBlocksOnlyRegisteredRoot(t *testing.T) {
|
||||
td := t.TempDir()
|
||||
hold := make(chan struct{})
|
||||
var holdOnce sync.Once
|
||||
closeHold := func() { holdOnce.Do(func() { close(hold) }) }
|
||||
RegisterInitHold(td, hold)
|
||||
t.Cleanup(func() {
|
||||
closeHold()
|
||||
ClearInitHold(td)
|
||||
})
|
||||
|
||||
d, err := New(td, 10*1024*1024, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
blocked := make(chan struct{})
|
||||
go func() {
|
||||
_ = d.Size()
|
||||
close(blocked)
|
||||
}()
|
||||
select {
|
||||
case <-blocked:
|
||||
t.Fatal("Size returned while init hold still open")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
|
||||
td2 := t.TempDir()
|
||||
d2, err := New(td2, 10*1024*1024, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
other := make(chan struct{})
|
||||
go func() {
|
||||
_ = d2.Size()
|
||||
close(other)
|
||||
}()
|
||||
select {
|
||||
case <-other:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("unrelated DiskFS Size hung; init hold leaked across roots")
|
||||
}
|
||||
|
||||
closeHold()
|
||||
select {
|
||||
case <-blocked:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Size did not return after init hold released")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user