mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-31 17:30:42 +01:00
* 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
168 lines
6.6 KiB
Go
168 lines
6.6 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"syscall"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/cluster/lock_manager"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// AcquireHandle returns the shared handle for inode plus this open's own fh
|
|
// value — the id handed to the kernel and seen again at RELEASE.
|
|
func (wfs *WFS) AcquireHandle(ctx context.Context, inode uint64, flags, uid, gid uint32) (fileHandle *FileHandle, openFh FileHandleId, status fuse.Status) {
|
|
var path util.FullPath
|
|
for {
|
|
// If there is an in-flight async flush for this inode, wait for it to
|
|
// complete before reopening. Otherwise the new handle would be built
|
|
// from pre-close filer metadata and its next flush could overwrite the
|
|
// data that was just written asynchronously.
|
|
if !wfs.waitForPendingAsyncFlush(ctx, inode) {
|
|
return nil, 0, fuse.EINTR
|
|
}
|
|
|
|
var pathStatus fuse.Status
|
|
path, pathStatus = wfs.inodeToPath.GetPath(inode)
|
|
if pathStatus != fuse.OK {
|
|
return nil, 0, pathStatus
|
|
}
|
|
|
|
// A fresh lookup returns the entry with the filer log position it
|
|
// reflects; the handle takes that entry and version as one decision in
|
|
// AcquireFileHandle, so a slower concurrent opener cannot
|
|
// overwrite a newer install. An existing handle keeps its own entry and
|
|
// version — they did not come from this lookup.
|
|
var entry *filer_pb.Entry
|
|
var entryVersionTsNs entryVersion
|
|
freshLookup := false
|
|
if existingFh, found := wfs.fhMap.FindFileHandle(inode); found {
|
|
entry = existingFh.UpdateEntry(func(entry *filer_pb.Entry) {
|
|
if entry != nil && existingFh.entry.Attributes == nil {
|
|
entry.Attributes = &filer_pb.FuseAttributes{}
|
|
}
|
|
})
|
|
entryVersionTsNs = entryVersion{tsNs: existingFh.entryVersionTsNs.Load(), signature: existingFh.entryVersionSignature.Load()}
|
|
} else {
|
|
entry, entryVersionTsNs, status = wfs.maybeLoadEntry(ctx, path)
|
|
if status != fuse.OK {
|
|
return nil, 0, status
|
|
}
|
|
freshLookup = true
|
|
}
|
|
if wormEnforced, _ := wfs.wormEnforcedForEntry(path, entry); wormEnforced && flags&fuse.O_ANYWRITE != 0 {
|
|
return nil, 0, fuse.EPERM
|
|
}
|
|
// Check unix permission bits for the requested access mode. With
|
|
// default_permissions the kernel already enforced them before this
|
|
// open, so the check (and its supplementary-group lookup) is redundant.
|
|
if !wfs.option.DefaultPermissions && entry != nil && entry.Attributes != nil {
|
|
fileUid, fileGid := entry.Attributes.Uid, entry.Attributes.Gid
|
|
if wfs.option.UidGidMapper != nil {
|
|
fileUid, fileGid = wfs.option.UidGidMapper.FilerToLocal(fileUid, fileGid)
|
|
}
|
|
if mask := openFlagsToAccessMask(flags); mask != 0 && !hasAccess(uid, gid, fileUid, fileGid, entry.Attributes.FileMode, mask) {
|
|
return nil, 0, fuse.EACCES
|
|
}
|
|
}
|
|
// need to AcquireFileHandle again to ensure correct handle counter
|
|
var existed, draining bool
|
|
fileHandle, openFh, existed, draining = wfs.fhMap.AcquireFileHandle(wfs, inode, entry, entryVersionTsNs.tsNs, entryVersionTsNs.signature)
|
|
if draining {
|
|
// A RELEASE slipped between waitForPendingAsyncFlush and this
|
|
// attach: the handle's last open closed and its deferred flush
|
|
// is draining. The pending-flush flag is registered under the
|
|
// same mutex that dropped the counter, so the next iteration
|
|
// blocks on the drain (or finds the handle already removed)
|
|
// and rebuilds state from post-flush metadata.
|
|
continue
|
|
}
|
|
fileHandle.RememberPath(path)
|
|
if existed && freshLookup {
|
|
// Another opener created the handle while our lookup was in flight;
|
|
// install only state that provably improves on what it holds.
|
|
fileHandle.installAckedEntry(entry, entryVersionTsNs.tsNs, entryVersionTsNs.signature)
|
|
}
|
|
break
|
|
}
|
|
|
|
// Acquire distributed lock for write opens. The lock is held with
|
|
// auto-renewal until the file handle is released (close).
|
|
// Use the filer path as the lock key since inode numbers are
|
|
// assigned per-mount and differ across mount instances.
|
|
if wfs.lockClient != nil && flags&fuse.O_ANYWRITE != 0 && fileHandle.dlmLock == nil {
|
|
owner := fmt.Sprintf("mount-%d", wfs.signature)
|
|
fileHandle.dlmLock = wfs.lockClient.NewBlockingLongLivedLock(
|
|
string(path), owner, lock_manager.LiveLockTTL,
|
|
)
|
|
if fileHandle.dlmLock == nil {
|
|
// No lock server is reachable: proceeding would silently drop
|
|
// cross-mount write serialization, so fail the open instead.
|
|
wfs.ReleaseHandle(openFh)
|
|
return nil, 0, fuse.Status(syscall.EAGAIN)
|
|
}
|
|
glog.V(1).Infof("DLM lock acquired for %s", path)
|
|
}
|
|
return fileHandle, openFh, fuse.OK
|
|
}
|
|
|
|
// ReleaseHandle is called from FUSE Release. For handles with a pending
|
|
// async flush, the map removal and the pendingAsyncFlush registration are
|
|
// done under a single lock hold so that a concurrent AcquireHandle cannot
|
|
// slip through the gap between the two (P1-1 TOCTOU fix).
|
|
//
|
|
// The handle intentionally stays in fhMap during the drain so that rename
|
|
// and unlink can still find it via FindFileHandle (P1-2 fix). It is
|
|
// removed from fhMap only after the drain completes (RemoveFileHandle).
|
|
func (wfs *WFS) ReleaseHandle(handleId FileHandleId) {
|
|
// Hold pendingAsyncFlushMu across the counter decrement and the
|
|
// pending-flush registration. Lock ordering: pendingAsyncFlushMu → fhMap.
|
|
wfs.pendingAsyncFlushMu.Lock()
|
|
fhToRelease := wfs.fhMap.ReleaseByHandle(handleId)
|
|
if fhToRelease != nil && fhToRelease.asyncFlushPending {
|
|
done := make(chan struct{})
|
|
wfs.pendingAsyncFlush[fhToRelease.inode] = done
|
|
// Add(1) while holding the mutex so WaitForAsyncFlush cannot
|
|
// observe a zero counter and close the channel before we send.
|
|
wfs.asyncFlushWg.Add(1)
|
|
wfs.pendingAsyncFlushMu.Unlock()
|
|
|
|
// Send after unlock to avoid deadlock — workers acquire the
|
|
// same mutex during cleanup.
|
|
wfs.asyncFlushCh <- &asyncFlushItem{fh: fhToRelease, done: done}
|
|
return
|
|
}
|
|
wfs.pendingAsyncFlushMu.Unlock()
|
|
|
|
if fhToRelease != nil {
|
|
fhToRelease.ReleaseHandle()
|
|
}
|
|
}
|
|
|
|
// waitForPendingAsyncFlush blocks until any in-flight async flush for
|
|
// the given inode completes. Called from AcquireHandle before building
|
|
// new handle state, so the filer metadata reflects the flushed data.
|
|
func (wfs *WFS) waitForPendingAsyncFlush(ctx context.Context, inode uint64) bool {
|
|
wfs.pendingAsyncFlushMu.Lock()
|
|
done, found := wfs.pendingAsyncFlush[inode]
|
|
wfs.pendingAsyncFlushMu.Unlock()
|
|
|
|
if !found {
|
|
return true
|
|
}
|
|
glog.V(3).Infof("waitForPendingAsyncFlush: waiting for inode %d", inode)
|
|
select {
|
|
case <-done:
|
|
return true
|
|
case <-ctx.Done():
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (wfs *WFS) GetHandle(handleId FileHandleId) *FileHandle {
|
|
return wfs.fhMap.GetFileHandle(handleId)
|
|
}
|