mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-13 17:48:29 +02:00
* mount: propagate request context through metadata lookups The FUSE interrupt channel never reached the filer RPC: lookupEntry ran its LookupEntry under context.Background(), so a stalled filer parked ops like ListXAttr forever even after the kernel gave up on them. Thread a context from each op's cancel channel through maybeReadEntry, maybeLoadEntry, lookupEntry, ensureDirectoryVisited and waitForPendingAsyncFlush; the existing withFilerClient attempt bound turns it into both an interrupt and a deadline. A caller cancellation now surfaces as EINTR instead of waiting out the RPC. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: reuse ctxFromCancel for the read path's interrupt bridge Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: regression tests for canceled and stalled filer lookups Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: thread ctx through AcquireHandle Open, Create and Mknod all carry the request's cancel channel; let it bound the pending-flush wait and metadata load inside AcquireHandle too. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: keep flush waits and write sequences uninterruptible The cancel-derived context must only cover metadata reads: an interrupted Unlink or Rename that skips the pending-flush wait can race the async write back into existence, and Link/Rename mutations that run under a canceled context abort mid-sequence, strand the rollback, and leave wrong hard-link counts. Restore the blocking flush wait and run each op's write phase on context.Background(). Rmdir and Unlink also propagate EINTR from the parent lookup instead of proceeding past a permission check the interrupt prevented. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: interrupt shared directory builds and directory reads Waiters behind another request's singleflight directory build blocked until it finished even when interrupted; switch the wait to DoChan so the request context can abandon it. ReadDir paths now carry the request context through EnsureListingFresh, cache listings and direct reads, and map its cancellation to EINTR rather than EIO. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: assert exact statuses in filer-stall regression tests Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: let read-side flush waits release on interrupt waitForPendingAsyncFlush reports whether the flush actually finished: lookupEntry and AcquireHandle return EINTR when the request is abandoned mid-wait, while Unlink and Rename keep waiting on context.Background() so the delete/rename still sees the flushed state. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: propagate interrupt through remaining lookup checks Rename's source and destination permission lookups now return EINTR on interrupt instead of skipping checkStickyBit or masking an existing target, and the readdir preload visit maps cancellation to EINTR like the other branches. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: fail closed on errored rename prerequisite lookups * mount: stop rmdir on interrupted target lookup, fail closed on dir lookup errors --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
225 lines
7.5 KiB
Go
225 lines
7.5 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
)
|
|
|
|
/** Create a directory
|
|
*
|
|
* Note that the mode argument may not have the type specification
|
|
* bits set, i.e. S_ISDIR(mode) can be false. To obtain the
|
|
* correct directory type bits use mode|S_IFDIR
|
|
* */
|
|
func (wfs *WFS) Mkdir(cancel <-chan struct{}, in *fuse.MkdirIn, name string, out *fuse.EntryOut) (code fuse.Status) {
|
|
|
|
if wfs.IsOverQuotaWithUncommitted() {
|
|
return fuse.Status(syscall.ENOSPC)
|
|
}
|
|
|
|
var s fuse.Status
|
|
if name, s = checkName(name); s != fuse.OK {
|
|
return s
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
|
|
dirFullPath, code := wfs.inodeToPath.GetPath(in.NodeId)
|
|
if code != fuse.OK {
|
|
return
|
|
}
|
|
|
|
entryFullPath := dirFullPath.Child(name)
|
|
|
|
// Pre-allocate the mount's local inode and stamp it into the create
|
|
// request so both the mount and the filer agree on object identity from
|
|
// the start. Without this, the filer assigns its own inode in CreateEntry
|
|
// and the cached entry then reports a different value than the one we
|
|
// return to the kernel here.
|
|
inode := wfs.inodeToPath.AllocateInode(entryFullPath, now)
|
|
newEntry := &filer_pb.Entry{
|
|
Name: name,
|
|
IsDirectory: true,
|
|
Attributes: &filer_pb.FuseAttributes{
|
|
Mtime: now,
|
|
Crtime: now,
|
|
Ctime: now,
|
|
FileMode: uint32(os.ModeDir) | in.Mode,
|
|
Uid: in.Uid,
|
|
Gid: in.Gid,
|
|
Inode: inode,
|
|
},
|
|
}
|
|
|
|
wfs.mapPbIdFromLocalToFiler(newEntry)
|
|
// Defer restoring to local uid/gid AFTER the entry is sent to the filer
|
|
// but BEFORE outputPbEntry writes attributes to the kernel. We restore
|
|
// explicitly below instead of using defer so the kernel gets local values.
|
|
|
|
request := &filer_pb.CreateEntryRequest{
|
|
// Defensive: dirFullPath is clean by construction for mount-originated
|
|
// mutations, but could carry invalid-UTF-8 bytes if metaCache was
|
|
// populated from a non-gRPC source (direct store write, legacy import).
|
|
// Sanitizing here keeps the marshal strictly per-request on the off
|
|
// chance invalid bytes do reach us.
|
|
Directory: dirFullPath.Sanitized(),
|
|
Entry: newEntry,
|
|
Signatures: []int32{wfs.signature},
|
|
SkipCheckParentDirectory: true,
|
|
// mkdir(2) is exclusive by contract: creating an existing path must
|
|
// fail with EEXIST. Without OExcl the filer treats a concurrent
|
|
// duplicate as an update and reports success to both callers; the
|
|
// kernel's pre-mkdir lookup only catches the duplicate when the
|
|
// winner's create is already visible, which a cross-node race defeats.
|
|
// The filer routes an OExcl create to the entry's ring owner, so its
|
|
// per-path lock arbitrates every creator in the cluster.
|
|
OExcl: true,
|
|
}
|
|
|
|
glog.V(1).Infof("mkdir: %v", request)
|
|
resp, err := wfs.streamCreateEntry(context.Background(), request)
|
|
if err != nil {
|
|
glog.V(0).Infof("mkdir %s: %v", entryFullPath, err)
|
|
} else {
|
|
event := resp.GetMetadataEvent()
|
|
if event == nil {
|
|
event = metadataCreateEvent(string(dirFullPath), newEntry)
|
|
}
|
|
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
|
|
glog.Warningf("mkdir %s: best-effort metadata apply failed: %v", entryFullPath, applyErr)
|
|
wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
|
|
}
|
|
wfs.inodeToPath.TouchDirectory(dirFullPath)
|
|
wfs.touchDirMtimeCtimeBest(dirFullPath)
|
|
wfs.inodeToPath.AdjustSubdirCount(dirFullPath, 1)
|
|
}
|
|
|
|
glog.V(3).Infof("mkdir %s: %v", entryFullPath, err)
|
|
|
|
if err != nil {
|
|
wfs.mapPbIdFromFilerToLocal(newEntry)
|
|
if errors.Is(err, filer_pb.ErrEntryAlreadyExists) {
|
|
// Lost a create race: the path exists on the filer but not yet in
|
|
// this mount's cache. Drop the parent's children cache so the next
|
|
// lookup fetches the winner's entry instead of waiting for the
|
|
// metadata subscription to deliver it.
|
|
wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
|
|
return fuse.Status(syscall.EEXIST)
|
|
}
|
|
return fuse.EIO
|
|
}
|
|
|
|
// Map uid/gid back to local-space before writing attributes to the
|
|
// kernel. The kernel (especially macFUSE) caches these and uses them
|
|
// for subsequent permission checks on children.
|
|
wfs.mapPbIdFromFilerToLocal(newEntry)
|
|
|
|
inode = wfs.inodeToPath.Lookup(entryFullPath, newEntry.Attributes.Crtime, true, false, inode, true)
|
|
|
|
// The newly created directory is guaranteed to be empty, so mark it as
|
|
// cached immediately to avoid a needless filer round-trip on the first
|
|
// Lookup or ReadDir inside this directory.
|
|
wfs.inodeToPath.MarkChildrenCached(entryFullPath)
|
|
|
|
wfs.outputPbEntry(out, inode, newEntry)
|
|
|
|
return fuse.OK
|
|
|
|
}
|
|
|
|
/** Remove a directory */
|
|
func (wfs *WFS) Rmdir(cancel <-chan struct{}, header *fuse.InHeader, name string) (code fuse.Status) {
|
|
|
|
if name == "." {
|
|
return fuse.Status(syscall.EINVAL)
|
|
}
|
|
if name == ".." {
|
|
return fuse.Status(syscall.ENOTEMPTY)
|
|
}
|
|
|
|
// Sanitize before it reaches DeleteEntryRequest.Name; see sanitizeFuseName.
|
|
name = sanitizeFuseName(name)
|
|
|
|
dirFullPath, code := wfs.inodeToPath.GetPath(header.NodeId)
|
|
if code != fuse.OK {
|
|
return
|
|
}
|
|
entryFullPath := dirFullPath.Child(name)
|
|
|
|
ctx, cancelFunc := ctxFromCancel(cancel)
|
|
defer cancelFunc()
|
|
targetEntry, _, targetCode := wfs.maybeLoadEntry(ctx, entryFullPath)
|
|
if targetCode == fuse.EINTR {
|
|
return targetCode
|
|
}
|
|
if targetCode != fuse.OK {
|
|
targetEntry = nil
|
|
}
|
|
|
|
// POSIX: enforce sticky bit on the parent directory.
|
|
if dirEntry, _, dirCode := wfs.maybeLoadEntry(ctx, dirFullPath); dirCode != fuse.OK {
|
|
return dirCode
|
|
} else if dirEntry != nil && dirEntry.Attributes != nil {
|
|
targetUid := uint32(0)
|
|
if targetEntry != nil && targetEntry.Attributes != nil {
|
|
targetUid = targetEntry.Attributes.Uid
|
|
}
|
|
if code := checkStickyBit(dirEntry.Attributes.FileMode, dirEntry.Attributes.Uid, targetUid, header.Uid); code != fuse.OK {
|
|
return code
|
|
}
|
|
}
|
|
|
|
glog.V(3).Infof("remove directory: %v", entryFullPath)
|
|
deleteReq := &filer_pb.DeleteEntryRequest{
|
|
Directory: string(dirFullPath),
|
|
Name: name,
|
|
IsDeleteData: true,
|
|
IgnoreRecursiveError: true, // ignore recursion error since the OS should manage it
|
|
Signatures: []int32{wfs.signature},
|
|
}
|
|
resp, err := wfs.streamDeleteEntry(context.Background(), deleteReq)
|
|
if err != nil {
|
|
glog.V(1).Infof("remove %s: %v", entryFullPath, err)
|
|
if filer.IsNonEmptyFolderError(err) {
|
|
return fuse.Status(syscall.ENOTEMPTY)
|
|
}
|
|
return fuse.ENOENT
|
|
}
|
|
|
|
event := metadataDeleteEvent(string(dirFullPath), name, true)
|
|
if resp != nil && resp.MetadataEvent != nil {
|
|
event = resp.MetadataEvent
|
|
}
|
|
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), event); applyErr != nil {
|
|
glog.Warningf("rmdir %s: best-effort metadata apply failed: %v", entryFullPath, applyErr)
|
|
wfs.inodeToPath.InvalidateChildrenCache(dirFullPath)
|
|
}
|
|
// The filer serialized the delete against concurrent updates and returned
|
|
// the entry as it stood; the snapshot loaded above may predate one.
|
|
if oldEntry := resp.GetMetadataEvent().GetEventNotification().GetOldEntry(); oldEntry.GetAttributes() != nil {
|
|
targetEntry = proto.Clone(oldEntry).(*filer_pb.Entry)
|
|
wfs.mapPbIdFromFilerToLocal(targetEntry)
|
|
}
|
|
wfs.inodeToPath.RemovePath(entryFullPath, func(inode uint64) {
|
|
if targetEntry != nil {
|
|
wfs.rememberRemovedDir(inode, targetEntry)
|
|
}
|
|
})
|
|
wfs.inodeToPath.TouchDirectory(dirFullPath)
|
|
wfs.touchDirMtimeCtimeBest(dirFullPath)
|
|
wfs.inodeToPath.AdjustSubdirCount(dirFullPath, -1)
|
|
|
|
return fuse.OK
|
|
|
|
}
|