mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* fix(mount): make Mkdir exclusive so a concurrent duplicate fails with EEXIST Mkdir sent CreateEntryRequest without OExcl, so the filer treated a concurrent duplicate as an update and reported success to both callers; the kernel's pre-mkdir lookup only masks this when the winner's create is already visible. Set OExcl, map the entry-already-exists sentinel to EEXIST instead of EIO, and drop the parent's children cache on the losing side so the next lookup fetches the winner's entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mount): route exclusive creates to the path's owner filer The filer's per-path lock is filer-local and the store insert has upsert semantics, so two mounts streaming to different filers can both create the same path even with OExcl (measured 18/30 both-success on a 3-filer cluster). Hash the path over the sorted filer list so every mount sends the same path's exclusive create to the same owner filer: keep the mutation stream when it already targets the owner, fall back to it when the owner is unreachable. Also let doUnary hand failed creates to CreateEntry so the structured error code survives as EEXIST instead of collapsing into the stream's generic EIO. Same race after the change: 30/30 exactly one winner, every loser fails with EEXIST. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mount): review fixes — pin exclusive creates to the owner filer An OExcl create now goes only to the path's owner filer: retrying on a different filer would race the owner's possibly still-in-flight create through a separate per-path lock, the very hole this routing closes. A broken mutation stream retries the same owner over unary, and an unreachable owner fails the create instead of degrading. Pick the owner by rendezvous hashing so the choice is independent of the configured filer order, and mounts configured with different but overlapping lists still agree wherever the winning filer appears in both. Reject a stream create wrapper whose nested response is nil instead of handing it to CreateEntry, which would dereference it. Add ownerFilerAddress unit tests: order independence, subset agreement, spread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * mount: drop the client-side owner ring, the filer routes exclusive creates now Exclusive creates are arbitrated cluster-wide on the server: the filer resolves an OExcl create's ring owner and forwards one hop, so one filer's per-path lock binds every creator — mount, S3, the HTTP surface and the Java client alike, not only the ones that opted into a client-side ring. That makes exclusiveCreateEntry redundant. It hashed the mount's configured -filer list, which names a different owner than the master-maintained ring, and failed the mkdir outright when its chosen owner was unreachable rather than letting the ring reassign. Mkdir goes back to streamCreateEntry; OExcl and the EEXIST mapping stay, and now mean what they say. Claude-Session: https://claude.ai/code/session_01Fx1Hx8RqsJqHpbfbgTf4WJ * mount: cover the create error plumbing that turns a lost race into EEXIST Letting a failed create's structured code survive doUnary is what makes a lost mkdir race report EEXIST instead of EIO, and it had no test. Pull the two steps out so they can be exercised without a live stream: hasCreateResponse decides whether a response still carries a code to unwrap, createEntryFromResponse does the unwrapping. Reading the guard the other way round also says what it means — consume the response only when there is no nested code left to recover — rather than negating a type assertion inline. Claude-Session: https://claude.ai/code/session_01Fx1Hx8RqsJqHpbfbgTf4WJ * mount: do not trust a create reply's shape before reading it createEntryFromResponse read cr.ErrorCode without checking the nested response was there. Nothing our filer sends is shaped that way, but the mount reads this off the wire and a nil there panics the whole mount, so report it instead. A top-level failure whose nested response carries no code was also returned as success, silently losing the error. Fall back to the top-level errno when the nested response explains nothing. Claude-Session: https://claude.ai/code/session_01Fx1Hx8RqsJqHpbfbgTf4WJ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
218 lines
7.3 KiB
Go
218 lines
7.3 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)
|
|
|
|
targetEntry, _, targetCode := wfs.maybeLoadEntry(entryFullPath)
|
|
if targetCode != fuse.OK {
|
|
targetEntry = nil
|
|
}
|
|
|
|
// POSIX: enforce sticky bit on the parent directory.
|
|
if dirEntry, _, dirCode := wfs.maybeLoadEntry(dirFullPath); dirCode == fuse.OK && 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
|
|
|
|
}
|