package meta_cache import ( "context" "sync/atomic" "testing" "time" "github.com/seaweedfs/seaweedfs/weed/filer" "github.com/seaweedfs/seaweedfs/weed/util" ) func insertCacheEntry(t *testing.T, mc *MetaCache, path util.FullPath) { t.Helper() if err := mc.InsertEntry(context.Background(), &filer.Entry{ FullPath: path, Attr: filer.Attr{ Crtime: time.Unix(1, 0), Mtime: time.Unix(1, 0), Mode: 0100644, FileSize: 1, }, }, 0); err != nil { t.Fatalf("insert %s: %v", path, err) } } // barrier flushes the apply loop: enqueued before it, PurgeDirectoryChildren is // asynchronous, so a synchronous apply-loop call afterward guarantees the purge // has been processed (the loop is FIFO and single-threaded). func barrier(t *testing.T, mc *MetaCache) { t.Helper() if err := mc.AbortDirectoryBuild(context.Background(), util.FullPath("/__barrier__")); err != nil { t.Fatalf("barrier: %v", err) } } // TestPurgeSkippedWhileDirectoryBuilding is the core regression guard: a purge // (idle eviction / kernel Forget / copy-range fallback) that lands while a // directory is being rebuilt must NOT wipe the entries the build just inserted. // Otherwise CompleteDirectoryBuild marks the directory cached over an empty // store, and every file in it vanishes from the mount though it is safe on the // filer. func TestPurgeSkippedWhileDirectoryBuilding(t *testing.T) { mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true}) defer mc.Shutdown() dir := util.FullPath("/dir") if err := mc.BeginDirectoryBuild(context.Background(), dir); err != nil { t.Fatalf("begin build: %v", err) } insertCacheEntry(t, mc, "/dir/a.txt") insertCacheEntry(t, mc, "/dir/b.txt") // A concurrent eviction tries to purge the directory mid-build. It is // enqueued before CompleteDirectoryBuild, so the apply loop processes it // first — while /dir is still building. var resetCalls int32 mc.PurgeDirectoryChildren(dir, func() { atomic.AddInt32(&resetCalls, 1) }) if err := mc.CompleteDirectoryBuild(context.Background(), dir, 0, nil); err != nil { t.Fatalf("complete build: %v", err) } if got := atomic.LoadInt32(&resetCalls); got != 0 { t.Fatalf("resetFn ran %d times during build; purge must be skipped", got) } if !mc.IsDirectoryCached(dir) { t.Fatal("/dir should be cached after build completes") } for _, name := range []string{"/dir/a.txt", "/dir/b.txt"} { if _, _, err := mc.FindEntry(context.Background(), util.FullPath(name)); err != nil { t.Fatalf("%s missing after mid-build purge: %v", name, err) } } } // TestPurgeClearsWhenNotBuilding verifies the eviction path still works off the // apply loop: with no build in flight, the purge resets the cached flag and // wipes the store. func TestPurgeClearsWhenNotBuilding(t *testing.T) { mc, _, _, _ := newTestMetaCache(t, map[util.FullPath]bool{"/": true, "/dir": true}) defer mc.Shutdown() dir := util.FullPath("/dir") insertCacheEntry(t, mc, "/dir/a.txt") var resetCalls int32 mc.PurgeDirectoryChildren(dir, func() { atomic.AddInt32(&resetCalls, 1) }) barrier(t, mc) if got := atomic.LoadInt32(&resetCalls); got != 1 { t.Fatalf("resetFn ran %d times; want 1", got) } if _, _, err := mc.FindEntry(context.Background(), util.FullPath("/dir/a.txt")); err == nil { t.Fatal("/dir/a.txt should have been purged") } }