vfs/disk: Fix EvictDiskVisibilityAndRecreateSafety flake
This commit was merged in pull request #20.
This commit is contained in:
@@ -4,6 +4,9 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- 'CONTRIBUTING.md'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-and-test:
|
check-and-test:
|
||||||
|
|||||||
+15
-5
@@ -248,15 +248,25 @@ func (d *DiskFS) calculateSizeAndPopulateIndex() {
|
|||||||
|
|
||||||
// insertBatch populates info/LRU under lock for a bounded batch (follows maxEvictBatch pattern for short critical sections).
|
// insertBatch populates info/LRU under lock for a bounded batch (follows maxEvictBatch pattern for short critical sections).
|
||||||
// Size is incremented here only for files actually added (prevents double-count vs. concurrent Create during window).
|
// Size is incremented here only for files actually added (prevents double-count vs. concurrent Create during window).
|
||||||
|
// Fail-closed: re-stat each path under d.mu and skip if the file is gone. Create does not wait on
|
||||||
|
// initDone, so a file the scanner observed can be Evict/Delete'd (info + os.Remove) before this
|
||||||
|
// insert runs. Inserting without a live-file check would resurrect the key in d.info and make
|
||||||
|
// Stat succeed while os.Stat fails.
|
||||||
func (d *DiskFS) insertBatch(batch []discoveredFile) {
|
func (d *DiskFS) insertBatch(batch []discoveredFile) {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
for _, df := range batch {
|
for _, df := range batch {
|
||||||
if _, exists := d.info[df.key]; !exists {
|
if _, exists := d.info[df.key]; exists {
|
||||||
fi := vfs.NewFileInfoFromOS(df.osInfo, df.key)
|
continue
|
||||||
d.info[df.key] = fi
|
|
||||||
d.LRU.Add(df.key, fi)
|
|
||||||
d.size += df.size
|
|
||||||
}
|
}
|
||||||
|
path := d.pathForKey(df.key)
|
||||||
|
st, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fi := vfs.NewFileInfoFromOS(st, df.key)
|
||||||
|
d.info[df.key] = fi
|
||||||
|
d.LRU.Add(df.key, fi)
|
||||||
|
d.size += st.Size()
|
||||||
}
|
}
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-41
@@ -371,7 +371,8 @@ func testKey(i int) string {
|
|||||||
// artifacts for victims are immediately gone (no resurrection via lazy discovery in Stat/Open),
|
// artifacts for victims are immediately gone (no resurrection via lazy discovery in Stat/Open),
|
||||||
// and that recreating the same key produces independent content that is not subject to any
|
// and that recreating the same key produces independent content that is not subject to any
|
||||||
// stale eviction unlinks. This exercises the coordinated WLock remove path for DiskFS.
|
// stale eviction unlinks. This exercises the coordinated WLock remove path for DiskFS.
|
||||||
// Uses tolerant checks suitable for raw DiskFS lazy discovery + bg size.
|
// Create does not wait on initDone, so this also covers insertBatch racing with eviction:
|
||||||
|
// gone files must not be re-indexed (Stat present / disk missing).
|
||||||
func TestDiskFS_EvictDiskVisibilityAndRecreateSafety(t *testing.T) {
|
func TestDiskFS_EvictDiskVisibilityAndRecreateSafety(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
td := t.TempDir()
|
td := t.TempDir()
|
||||||
@@ -400,47 +401,21 @@ func TestDiskFS_EvictDiskVisibilityAndRecreateSafety(t *testing.T) {
|
|||||||
_ = d.EvictBySize(1024*1024, true)
|
_ = d.EvictBySize(1024*1024, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consistency check: never have a key absent from Stat but with a file on disk (would indicate
|
// Drain bg population so insertBatch cannot still be in flight when we audit.
|
||||||
// either resurrection risk or orphan). If Stat succeeds, file should exist.
|
_ = d.Size()
|
||||||
// A few retries tolerate the documented lazy discovery + eviction coordination windows under
|
|
||||||
// artificial "force massive eviction then immediate audit" load (especially visible under -race).
|
// Consistency: Stat success iff the file exists on disk. insertBatch must not resurrect
|
||||||
for attempt := 0; attempt < 3; attempt++ {
|
// keys whose backing files were already evicted.
|
||||||
bad := false
|
for _, k := range created {
|
||||||
for _, k := range created {
|
p := d.pathForKey(k)
|
||||||
p := d.pathForKey(k)
|
_, statErr := d.Stat(k)
|
||||||
_, statErr := d.Stat(k)
|
_, diskErr := os.Stat(p)
|
||||||
_, diskErr := os.Stat(p)
|
if statErr != nil {
|
||||||
if statErr != nil {
|
if !os.IsNotExist(diskErr) {
|
||||||
if !os.IsNotExist(diskErr) {
|
t.Errorf("key %s absent via Stat but file lingers on disk at %s (resurrection risk)", k, p)
|
||||||
bad = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if diskErr != nil {
|
|
||||||
bad = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !bad {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if attempt < 2 {
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
} else {
|
|
||||||
// On final attempt, report the last observed state for the keys
|
|
||||||
for _, k := range created {
|
|
||||||
p := d.pathForKey(k)
|
|
||||||
_, statErr := d.Stat(k)
|
|
||||||
_, diskErr := os.Stat(p)
|
|
||||||
if statErr != nil {
|
|
||||||
if !os.IsNotExist(diskErr) {
|
|
||||||
t.Errorf("key %s absent via Stat but file lingers on disk at %s (resurrection risk)", k, p)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if diskErr != nil {
|
|
||||||
t.Errorf("key %s present via Stat but missing on disk: %v", k, diskErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if diskErr != nil {
|
||||||
|
t.Errorf("key %s present via Stat but missing on disk: %v", k, diskErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,6 +439,64 @@ func TestDiskFS_EvictDiskVisibilityAndRecreateSafety(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDiskFS_InsertBatchSkipsGoneFiles is the fail-closed contract for bg/lazy index
|
||||||
|
// insert: a discoveredFile whose path was removed (evicted) must not be re-inserted
|
||||||
|
// into d.info. That resurrection is what made Stat succeed while os.Stat failed.
|
||||||
|
func TestDiskFS_InsertBatchSkipsGoneFiles(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
td := t.TempDir()
|
||||||
|
d, err := New(td, 10*1024*1024, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_ = d.Size() // finish constructor scan so it cannot also index these keys
|
||||||
|
|
||||||
|
liveKey := "live"
|
||||||
|
goneKey := "gone"
|
||||||
|
writeKey := func(key, body string) os.FileInfo {
|
||||||
|
t.Helper()
|
||||||
|
p := d.pathForKey(key)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(p, []byte(body), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
st, err := os.Stat(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
liveInfo := writeKey(liveKey, "still-here")
|
||||||
|
goneInfo := writeKey(goneKey, "about-to-vanish")
|
||||||
|
if err := os.Remove(d.pathForKey(goneKey)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.insertBatch([]discoveredFile{
|
||||||
|
{key: liveKey, size: liveInfo.Size(), osInfo: liveInfo},
|
||||||
|
{key: goneKey, size: goneInfo.Size(), osInfo: goneInfo},
|
||||||
|
})
|
||||||
|
|
||||||
|
d.mu.RLock()
|
||||||
|
_, liveExists := d.info[liveKey]
|
||||||
|
_, goneExists := d.info[goneKey]
|
||||||
|
d.mu.RUnlock()
|
||||||
|
if !liveExists {
|
||||||
|
t.Errorf("insertBatch skipped live key %s", liveKey)
|
||||||
|
}
|
||||||
|
if goneExists {
|
||||||
|
t.Errorf("insertBatch resurrected gone key %s", goneKey)
|
||||||
|
}
|
||||||
|
if _, err := d.Stat(goneKey); err == nil {
|
||||||
|
t.Errorf("Stat succeeded for gone key %s", goneKey)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(d.pathForKey(liveKey)); err != nil {
|
||||||
|
t.Errorf("live key %s missing on disk: %v", liveKey, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestDiskFS_EvictBoundedLargeN exercises the maxEvictBatch early-break logic (Idea #2)
|
// TestDiskFS_EvictBoundedLargeN exercises the maxEvictBatch early-break logic (Idea #2)
|
||||||
// under a map size >> batch limit. Forces repeated eviction rounds via GC-style pressure
|
// under a map size >> batch limit. Forces repeated eviction rounds via GC-style pressure
|
||||||
// and asserts progress + consistency (no resurrection/orphans). Covers bounded collection
|
// and asserts progress + consistency (no resurrection/orphans). Covers bounded collection
|
||||||
|
|||||||
Reference in New Issue
Block a user