Files
seaweedfs/weed/mount/weedfs_remote_rename_test.go
T
Chris Lu fd493f2547 mount: refuse to attach opens to a draining file handle (#12219)
* mount: refuse to attach opens to a draining file handle

With the writeback cache, releasing the last open of a file leaves the
shared FileHandle in the map at counter zero while an async flush
drains it; the worker removes the map entries once the flush commits.
AcquireHandle waits for a pending flush and then attaches, but the two
steps are not atomic: a release can slip in between, drop the counter
to zero, and queue the drain while the opener goes on to mint a new
file-handle id on the draining handle. When the drain completes,
RemoveFileHandle deletes every fh id for the inode -- including the one
just minted. The kernel still holds that id, so its next FUSE_READ
gets ENOENT from GetHandle; the failed read leaves the folio not
uptodate and the kernel surfaces EIO to userspace. This was the flaky
TestWritebackCacheConcurrentMixedOps failure.

AcquireFileHandle now refuses to attach when the shared handle has
reached a zero counter and reports it as draining. AcquireHandle and
the Create path retry the whole wait-resolve-attach sequence; the next
pass blocks on the pending flush (flag registration and the counter
drop share pendingAsyncFlushMu) and builds a fresh handle from the
post-flush entry. The DLM-failure release in AcquireHandle also goes
through ReleaseHandle so it cannot strand a counter-zero handle
outside the drain bookkeeping.

Removes the refreshEntryForRead retry from #12206, which worked around
the symptom by refetching the entry after the fh id was already gone.

Test: TestAcquireFileHandleRefusesDrainingHandle covers the refusal
and fresh-handle path; TestWritebackCacheConcurrentMixedOps and an
amplified concurrent-overwrite repro run clean.

* mount: keep refresh-on-error retry for stale-chunk reads
2026-10-29 16:06:57 -07:00

123 lines
4.8 KiB
Go

package mount
import (
"testing"
"time"
"github.com/seaweedfs/go-fuse/v2/fuse"
"github.com/seaweedfs/seaweedfs/weed/mount/meta_cache"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// The filer sends one event per moved entry, so children follow their parent.
func TestRemoteRenameMovesInodePaths(t *testing.T) {
dir := util.FullPath("/images")
wfs := newPagingWFS(t, dir, []string{"f.jpg"}, 0)
now := time.Now().Unix()
subInode := wfs.inodeToPath.Lookup(dir.Child("sub"), now, true, false, 0, true)
childInode := wfs.inodeToPath.Lookup(dir.Child("sub").Child("f.jpg"), now, false, false, 0, true)
wfs.onEntryInvalidation(meta_cache.EntryInvalidation{
Path: dir.Child("sub"),
RenamedTo: dir.Child("moved"),
WasDirectory: true,
})
wfs.onEntryInvalidation(meta_cache.EntryInvalidation{
Path: dir.Child("sub").Child("f.jpg"),
RenamedTo: dir.Child("moved").Child("f.jpg"),
})
if got, _ := wfs.inodeToPath.GetPath(subInode); got != dir.Child("moved") {
t.Errorf("renamed directory resolves to %s, want %s", got, dir.Child("moved"))
}
if got, _ := wfs.inodeToPath.GetPath(childInode); got != dir.Child("moved").Child("f.jpg") {
t.Errorf("child resolves to %s, want %s", got, dir.Child("moved").Child("f.jpg"))
}
if wfs.inodeToPath.HasPath(dir.Child("sub")) {
t.Error("pre-rename directory path still in the table")
}
}
// The handle path and the table move are two halves of one rename. Doing the
// move twice would unlink what the first one just put at the target, leaving
// the renamed inode with no path at all.
func TestRemoteRenameMovesOnceWithAnOpenHandle(t *testing.T) {
wfs := newInvalidateTestWFS(t)
oldPath, newPath := util.FullPath("/dir/file"), util.FullPath("/dir/renamed")
inode := wfs.inodeToPath.Lookup(oldPath, time.Now().Unix(), false, false, 0, true)
wfs.fhMap.AcquireFileHandle(wfs, inode, &filer_pb.Entry{
Name: "file",
Attributes: &filer_pb.FuseAttributes{FileSize: 88},
}, 0, 0)
wfs.onEntryInvalidation(meta_cache.EntryInvalidation{Path: oldPath, RenamedTo: newPath, TsNs: 1000})
if got, status := wfs.inodeToPath.GetPath(inode); status != fuse.OK || got != newPath {
t.Errorf("inode resolves to %q (%v), want %s", got, status, newPath)
}
if got, found := wfs.inodeToPath.GetInode(newPath); !found || got != inode {
t.Errorf("%s resolves to %d (found %v), want %d", newPath, got, found, inode)
}
}
// The subscription can redeliver a rename once it falls out of the dedup ring.
// Replaying one must not unlink what the first delivery put at the target, nor
// mark the moved file's handle deleted.
func TestRemoteRenameReplayLeavesTheTargetAlone(t *testing.T) {
wfs := newInvalidateTestWFS(t)
oldPath, newPath := util.FullPath("/dir/file"), util.FullPath("/dir/renamed")
inode := wfs.inodeToPath.Lookup(oldPath, time.Now().Unix(), false, false, 0, true)
fh, _, _, _ := wfs.fhMap.AcquireFileHandle(wfs, inode, &filer_pb.Entry{
Name: "file",
Attributes: &filer_pb.FuseAttributes{FileSize: 88},
}, 0, 0)
rename := meta_cache.EntryInvalidation{Path: oldPath, RenamedTo: newPath, TsNs: 1000}
wfs.onEntryInvalidation(rename)
wfs.onEntryInvalidation(rename)
if got, status := wfs.inodeToPath.GetPath(inode); status != fuse.OK || got != newPath {
t.Errorf("inode resolves to %q (%v), want %s", got, status, newPath)
}
if got, found := wfs.inodeToPath.GetInode(newPath); !found || got != inode {
t.Errorf("%s resolves to %d (found %v), want %d", newPath, got, found, inode)
}
if fh.isDeleted {
t.Error("the moved file's handle was marked deleted by the replay")
}
}
// A rename event older than the handle's version fence is still a rename. The
// fence is about which version of the content the handle holds, and skipping
// the move would leave both the inode and the handle on a name the filer has
// vacated.
func TestRenameBelowTheVersionFenceStillMoves(t *testing.T) {
wfs := newInvalidateTestWFS(t)
oldPath, newPath := util.FullPath("/dir/file"), util.FullPath("/dir/renamed")
inode := wfs.inodeToPath.Lookup(oldPath, time.Now().Unix(), false, false, 0, true)
fh, _, _, _ := wfs.fhMap.AcquireFileHandle(wfs, inode, &filer_pb.Entry{
Name: "file",
Attributes: &filer_pb.FuseAttributes{FileSize: 88},
}, 0, 0)
// The handle already reflects a later position in the same clock domain, so
// the fence would drop this event.
fh.advanceEntryVersion(5000, wfs.signature)
wfs.onEntryInvalidation(meta_cache.EntryInvalidation{
Path: oldPath, RenamedTo: newPath, TsNs: 1000,
Signatures: []int32{wfs.signature},
})
if got, status := wfs.inodeToPath.GetPath(inode); status != fuse.OK || got != newPath {
t.Errorf("inode resolves to %q (%v), want %s", got, status, newPath)
}
if dir, name := fh.savedDir, fh.savedName; util.FullPath(dir).Child(name) != newPath {
t.Errorf("handle remembers %s/%s, want %s", dir, name, newPath)
}
}