mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-13 07:19:18 +01:00
* fix(mount): preserve acknowledged chunks during attribute updates * mount: keep the WFS field block aligned Adding the publication-lock fields inside the aligned block made gofmt re-indent 25 unrelated lines. Put them in their own group after dirIdleEvict, matching the enterprise layout, so syncs do not conflict there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: finish attribute saves through a FUSE interrupt saveEntryAttributes derived its RPC context from the syscall's cancel channel, so an interrupt while the conditional UpdateEntry was at the owner returned EIO for an update the owner then applied, and skipped the local install and retention. Keep the metadata deadline but drop the cancellation, like the other metadata mutations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: map attribute CAS ids in place instead of cloning the entry saveEntryAttributes cloned the whole entry three times per attempt only to rewrite uid/gid for the filer, and each clone is as large as the chunk list. Switch the ids in place and restore the exact local values after the call, keeping one clone. The test now checks the reply carries the local uid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: compare attribute updates against the filer's own uid/gid With -map.uid/-map.gid, a filer id that is also a mapped local id reads back unchanged and maps forward to another filer id. The IF_ENTRY_EQUAL expected entry then never matched, so chmod, utimes and xattr changes on such files failed with EAGAIN after three attempts, and an update would have rewritten the owner. Keep the filer ids read with the entry for the ids the syscall leaves alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mount): align attribute retention with read lock order * mount: take the handle lock before entryLock in the attribute snapshot attributeEntrySnapshot held entryLock while it queued for the shared handle lock. A Read holding that lock waits on entryLock, the snapshot waits behind a queued exclusive (an invalidation or a Write), and the exclusive waits on the Read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mount): complete handle lock order and preserve explicit ID intent * mount: re-read a refused attribute update from the file's owner filer The filer checks the IF_ENTRY_EQUAL condition on the owner of the path's write route key. The retry read any filer, so behind one that had not applied the owner's latest update, every attempt sent the same stale base and the syscall failed with EAGAIN. Read from the owner once the lock ring is known, like lookupFilerEntry, but keep the filer's raw ids. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
213 lines
8.4 KiB
Go
213 lines
8.4 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)
|
|
// The previous holder's flush may not have reached the local cache,
|
|
// and the lock orders this writer after it: take the filer's entry.
|
|
if entry, version, err := wfs.lookupFilerEntry(ctx, path); err == nil {
|
|
fileHandle.installAckedEntry(entry, version.tsNs, version.signature)
|
|
}
|
|
}
|
|
return fileHandle, openFh, fuse.OK
|
|
}
|
|
|
|
// lookupFilerEntry reads path from the filer, bypassing the local cache, in
|
|
// local ids and with the log position the answer reflects.
|
|
func (wfs *WFS) lookupFilerEntry(ctx context.Context, path util.FullPath) (*filer_pb.Entry, entryVersion, error) {
|
|
entry, version, err := wfs.lookupOwnerEntry(ctx, path)
|
|
if err != nil {
|
|
return nil, entryVersion{}, err
|
|
}
|
|
if entry.Attributes == nil {
|
|
entry.Attributes = &filer_pb.FuseAttributes{}
|
|
}
|
|
wfs.mapPbIdFromFilerToLocal(entry)
|
|
return entry, version, nil
|
|
}
|
|
|
|
// lookupOwnerEntry reads path in filer ids from the filer that checks path's
|
|
// write conditions, since another filer may lag it.
|
|
func (wfs *WFS) lookupOwnerEntry(ctx context.Context, path util.FullPath) (*filer_pb.Entry, entryVersion, error) {
|
|
dir, name := path.DirAndName()
|
|
var entry *filer_pb.Entry
|
|
var version entryVersion
|
|
lookup := func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
|
|
resp, err := filer_pb.LookupEntry(ctx, client, &filer_pb.LookupDirectoryEntryRequest{Directory: dir, Name: name})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entry, version = resp.Entry, entryVersion{tsNs: resp.LogTsNs, signature: resp.LogSignature}
|
|
return nil
|
|
}
|
|
var err error
|
|
if owner := wfs.entryWriteOwner(path); owner != "" {
|
|
err = wfs.callFiler(ctx, false, wfs.filerRPCWait(), owner.ToGrpcAddress(), lookup)
|
|
} else {
|
|
err = wfs.withFilerClient(ctx, false, wfs.filerRPCWait(), lookup)
|
|
}
|
|
if err != nil {
|
|
return nil, entryVersion{}, err
|
|
}
|
|
return entry, version, nil
|
|
}
|
|
|
|
// 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)
|
|
}
|