mount: test peer unlinks of a hard link name held open

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris LuandClaude Opus 5.5 committed 2026-11-13 02:51:33 -08:00
1 parent ee1de6d533
commit df02ea508e
1 file changed
+134
+134
View File
@@ -0,0 +1,134 @@
//go:build !freebsd
package mount
import (
"context"
"testing"
"github.com/seaweedfs/go-fuse/v2/fuse"
"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
)
// peerUnlink removes name on the filer the way another mount does and
// delivers the delete event this mount's subscription receives.
func peerUnlink(t *testing.T, wfs *WFS, fake *hardLinkFiler, name string) {
t.Helper()
old := fake.stored(name)
if _, err := fake.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{Directory: "/", Name: name}); err != nil {
t.Fatalf("peer delete %s: %v", name, err)
}
fake.mu.Lock()
fake.stamp++
ts := fake.stamp
fake.mu.Unlock()
del := &filer_pb.SubscribeMetadataResponse{Directory: "/", TsNs: ts, EventNotification: &filer_pb.EventNotification{OldEntry: old}}
if err := wfs.metaCache.ApplyMetadataResponse(context.Background(), del, meta_cache.SubscriberMetadataResponseApplyOptions); err != nil {
t.Fatalf("apply peer delete: %v", err)
}
wfs.metaCache.WaitForEntryInvalidations()
}
// Another mount removes one of the two names of a file open here. The handle
// keeps publishing through the other name, over the link count and ctime the
// filer stored there, and the removed name stays removed.
func TestPeerUnlinkedAliasKeepsOpenHandlePublishing(t *testing.T) {
for _, removed := range []string{"tmp", "final"} {
for _, dirty := range []bool{false, true} {
t.Run(removed+map[bool]string{false: "/clean", true: "/dirty"}[dirty], func(t *testing.T) {
wfs, fake, fh, inode := openLinkTestFile(t)
linkInRoot(t, wfs, inode, "final")
chunks := 2
if dirty {
fh.UpdateEntry(func(e *filer_pb.Entry) {
e.Chunks = append(e.Chunks, linkHandleChunk(9, 100))
e.Attributes.FileSize = 200
})
fh.dirtyMetadata = true
chunks = 3
}
survivor := map[string]string{"tmp": "final", "final": "tmp"}[removed]
peerUnlink(t, wfs, fake, removed)
if fh.isDeleted {
t.Fatal("a peer unlinking one of two names marked the open handle deleted")
}
unlinked := fake.stored(survivor).Attributes
if code := appendAndFlush(t, wfs, fh, 2); code != fuse.OK {
t.Fatalf("flush after the peer unlink: %v", code)
}
wantStored(t, fake, survivor, chunks, 1)
if got := fake.stored(survivor).Attributes; got.Ctime != unlinked.Ctime || got.CtimeNs != unlinked.CtimeNs {
t.Fatalf("the flush moved the ctime from the unlink's %d.%09d back to %d.%09d", unlinked.Ctime, unlinked.CtimeNs, got.Ctime, got.CtimeNs)
}
if fake.stored(removed) != nil {
t.Fatalf("the flush recreated %s", removed)
}
})
}
}
}
// The surviving name was found by a lookup, not linked by this mount.
func TestPeerUnlinkedAliasKeepsLookedUpSurvivor(t *testing.T) {
wfs, fake, fh, inode := openLinkTestFile(t)
linkInRoot(t, wfs, inode, "final")
wfs.inodeToPath.RemovePath("/final", nil)
wfs.inodeToPath.Lookup("/final", 1, false, true, inode, true)
peerUnlink(t, wfs, fake, "tmp")
if fh.isDeleted {
t.Fatal("a peer unlinking the open name of a file with a looked-up second name marked the handle deleted")
}
if code := appendAndFlush(t, wfs, fh, 2); code != fuse.OK {
t.Fatalf("flush after the peer unlink: %v", code)
}
wantStored(t, fake, "final", 2, 1)
if fake.stored("tmp") != nil {
t.Fatal("the flush recreated tmp")
}
}
// With no other name known here, the handle stops rather than recreating the
// removed one.
func TestPeerUnlinkedAliasWithUnknownSurvivorStops(t *testing.T) {
wfs, fake, fh, inode := openLinkTestFile(t)
linkInRoot(t, wfs, inode, "final")
wfs.inodeToPath.RemovePath("/final", nil)
peerUnlink(t, wfs, fake, "tmp")
if !fh.isDeleted {
t.Fatal("a handle with no known surviving name was left publishing")
}
if code := appendAndFlush(t, wfs, fh, 2); code != fuse.OK {
t.Fatalf("flush: %v", code)
}
if fake.stored("tmp") != nil {
t.Fatal("the flush recreated tmp")
}
}
// A peer removes one name, then this mount unlinks the last one.
func TestPeerThenLocalUnlinkKeepsHandleDeleted(t *testing.T) {
wfs, fake, fh, inode := openLinkTestFile(t)
linkInRoot(t, wfs, inode, "final")
peerUnlink(t, wfs, fake, "final")
unlinkInRoot(t, wfs, "tmp")
if !fh.isDeleted {
t.Fatal("the handle of a file with no names left is not marked deleted")
}
if code := appendAndFlush(t, wfs, fh, 2); code != fuse.OK {
t.Fatalf("flush after the last unlink: %v", code)
}
for _, name := range []string{"tmp", "final"} {
if fake.stored(name) != nil {
t.Fatalf("the flush recreated %s", name)
}
}
}
func TestPeerUnlinkLastNameKeepsHandleDeleted(t *testing.T) {
wfs, fake, fh, _ := openLinkTestFile(t)
peerUnlink(t, wfs, fake, "tmp")
if !fh.isDeleted {
t.Fatal("a peer unlinking the only name did not mark the handle deleted")
}
}