mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-29 08:20:42 +01:00
* mount: drain OFD locks when a file description is released The kernel scopes OFD fcntl locks to the open file description but reports no lock_owner for them at RELEASE — it only sends one for flock holders — so an OFD write lock taken on a file handle outlived the holder and blocked every later open of that file (e.g. etcd failing to reopen its WAL after a restart). The mount now records the owner of each granted non-flock lock under its file handle and releases those owners on RELEASE, which also covers any POSIX locks left over when FLUSH could not run. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: give every open its own fh value over the shared handle AcquireFileHandle reuses one FileHandle per inode, so every open reported the same fh to the kernel. A RELEASE for one description could then not be told apart from a sibling still holding the inode open — draining lock owners recorded under the shared fh dropped locks belonging to descriptions that stay open. Each open now registers its own fh value in fh2inode, all resolving to the shared FileHandle, and RELEASE removes exactly the id that is closing. This also stops a duplicated RELEASE on one id from double-decrementing the open count. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: forget flushed lock owners so a later release cannot re-drop locks POSIX lock owners are process-scoped: Flush releases every lock the process holds on the inode, but the owner stayed recorded under each open's fh for the Release-time drain. If the process took a fresh lock through another open before an old description finished closing, the stale record made that RELEASE drop the new lock too. Flush now drops the released owner from every recorded open of the inode, so the Release drain only ever fires for owners whose locks were never flushed. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: keep close-time lock owner records when the release fails A routed POSIX owner release that errors keeps the flush hint for a retry, but Flush also dropped the owner from every open's release records unconditionally — so the final Release skipped the retry and the owner filer's lock was stranded until lease reaping. Only forget the owner once its locks were actually released. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
153 lines
6.0 KiB
Go
153 lines
6.0 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"
|
|
)
|
|
|
|
// 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) {
|
|
// 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
|
|
}
|
|
|
|
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 bool
|
|
fileHandle, openFh, existed = wfs.fhMap.AcquireFileHandle(wfs, inode, entry, entryVersionTsNs.tsNs, entryVersionTsNs.signature)
|
|
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)
|
|
}
|
|
|
|
// 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.fhMap.ReleaseByHandle(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)
|
|
}
|