Files
seaweedfs/weed/mount/weedfs_link.go
T
Chris Lu 7916e61c08 fix(mount): avoid self-notify deadlock in Link and CopyFileRange handlers (#9110)
The Link and CopyFileRange FUSE request handlers were calling
fuseServer.InodeNotify (and EntryNotify for copy) synchronously while
the kernel was still waiting for the request's reply on the same
/dev/fuse fd. Notifications share that fd, so the syscall.Write can
block indefinitely when the kernel hasn't drained its queue yet,
hanging the entire mount. A goroutine dump from a stuck mount showed
the Link handler blocked in syscall.Write inside InodeNotify while the
server's read loop kept waiting for new requests.

Drop the synchronous notifies. The local meta cache is still updated
inline, so subsequent filesystem ops see the fresh state; the kernel's
attr/dentry caches re-fetch once their TTL expires.
2026-04-16 14:09:32 -07:00

240 lines
8.4 KiB
Go

package mount
import (
"bytes"
"context"
"syscall"
"time"
"github.com/seaweedfs/go-fuse/v2/fuse"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
/*
What is an inode?
If the file is an hardlinked file:
use the hardlink id as inode
Otherwise:
use the file path as inode
When creating a link:
use the original file inode
*/
/** Create a hard link to a file */
func (wfs *WFS) Link(cancel <-chan struct{}, in *fuse.LinkIn, name string, out *fuse.EntryOut) (code fuse.Status) {
if wfs.IsOverQuotaWithUncommitted() {
return fuse.Status(syscall.ENOSPC)
}
if s := checkName(name); s != fuse.OK {
return s
}
newParentPath, code := wfs.inodeToPath.GetPath(in.NodeId)
if code != fuse.OK {
return
}
oldEntryPath, code := wfs.inodeToPath.GetPath(in.Oldnodeid)
if code != fuse.OK {
return
}
oldParentPath, _ := oldEntryPath.DirAndName()
oldEntry, status := wfs.maybeLoadEntry(oldEntryPath)
if status != fuse.OK {
return status
}
// hardlink is not allowed in WORM mode
if wormEnforced, _ := wfs.wormEnforcedForEntry(oldEntryPath, oldEntry); wormEnforced {
return fuse.EPERM
}
// If the source is already a hard link, serialize on its HardLinkId
// so concurrent Link/Unlink operations on different siblings cannot
// both compute a new counter from a stale base. Re-load the entry
// under the lock to pick up any prior holder's sibling update.
if len(oldEntry.HardLinkId) > 0 {
hlKey := string(oldEntry.HardLinkId)
lock := wfs.hardLinkLockTable.AcquireLock("link", hlKey, util.ExclusiveLock)
defer wfs.hardLinkLockTable.ReleaseLock(hlKey, lock)
// Under the lock, re-resolve the source alias from the inode.
// A concurrent Unlink that held this same lock may have removed
// the specific alias we picked pre-lock even though other
// sibling hard links for the same inode are still around;
// GetPath(Oldnodeid) returns whichever alias is still active.
refreshedPath, refreshedStatus := wfs.inodeToPath.GetPath(in.Oldnodeid)
if refreshedStatus != fuse.OK {
return refreshedStatus
}
oldEntryPath = refreshedPath
oldParentPath, _ = oldEntryPath.DirAndName()
// Do not fall back to the pre-lock snapshot: if every alias
// was deleted while we waited, abort instead of deriving the
// next counter from a stale entry.
fresh, freshStatus := wfs.maybeLoadEntry(oldEntryPath)
if freshStatus != fuse.OK {
return freshStatus
}
oldEntry = fresh
}
// update old file to hardlink mode
origHardLinkId := oldEntry.HardLinkId
origHardLinkCounter := oldEntry.HardLinkCounter
if len(oldEntry.HardLinkId) == 0 {
oldEntry.HardLinkId = filer.NewHardLinkId()
oldEntry.HardLinkCounter = 1
glog.V(4).Infof("Link: new HardLinkId %x for %s", oldEntry.HardLinkId, oldEntryPath)
}
oldEntry.HardLinkCounter++
glog.V(4).Infof("Link: %s -> %s/%s HardLinkId %x counter=%d",
oldEntryPath, newParentPath, name, oldEntry.HardLinkId, oldEntry.HardLinkCounter)
updateOldEntryRequest := &filer_pb.UpdateEntryRequest{
Directory: oldParentPath,
Entry: oldEntry,
Signatures: []int32{wfs.signature},
}
// CreateLink 1.2 : update new file to hardlink mode
linkNow := time.Now()
oldEntry.Attributes.Mtime = linkNow.Unix()
oldEntry.Attributes.MtimeNs = int32(linkNow.Nanosecond())
oldEntry.Attributes.Ctime = linkNow.Unix()
oldEntry.Attributes.CtimeNs = int32(linkNow.Nanosecond())
request := &filer_pb.CreateEntryRequest{
Directory: string(newParentPath),
Entry: &filer_pb.Entry{
Name: name,
IsDirectory: false,
Attributes: oldEntry.Attributes,
Chunks: oldEntry.GetChunks(),
Extended: oldEntry.Extended,
HardLinkId: oldEntry.HardLinkId,
HardLinkCounter: oldEntry.HardLinkCounter,
},
Signatures: []int32{wfs.signature},
SkipCheckParentDirectory: true,
}
// apply changes to the filer, and also apply to local metaCache
wfs.mapPbIdFromLocalToFiler(request.Entry)
ctx := context.Background()
updateResp, err := wfs.streamUpdateEntry(ctx, updateOldEntryRequest)
if err == nil {
updateEvent := updateResp.GetMetadataEvent()
if updateEvent == nil {
updateEvent = metadataUpdateEvent(oldParentPath, updateOldEntryRequest.Entry)
}
if applyErr := wfs.applyLocalMetadataEvent(ctx, updateEvent); applyErr != nil {
glog.Warningf("link %s: best-effort metadata apply failed: %v", oldEntryPath, applyErr)
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(oldParentPath))
}
}
if err == nil {
var createResp *filer_pb.CreateEntryResponse
createResp, err = wfs.streamCreateEntry(ctx, request)
if err != nil {
// Rollback: restore original HardLinkId/Counter on the source entry
oldEntry.HardLinkId = origHardLinkId
oldEntry.HardLinkCounter = origHardLinkCounter
rollbackReq := &filer_pb.UpdateEntryRequest{
Directory: oldParentPath,
Entry: oldEntry,
Signatures: []int32{wfs.signature},
}
if _, rollbackErr := wfs.streamUpdateEntry(ctx, rollbackReq); rollbackErr != nil {
glog.Warningf("link rollback %s: %v", oldEntryPath, rollbackErr)
}
} else {
createEvent := createResp.GetMetadataEvent()
if createEvent == nil {
createEvent = metadataCreateEvent(string(newParentPath), request.Entry)
}
if applyErr := wfs.applyLocalMetadataEvent(ctx, createEvent); applyErr != nil {
glog.Warningf("link %s: best-effort metadata apply failed: %v", newParentPath.Child(name), applyErr)
wfs.inodeToPath.InvalidateChildrenCache(newParentPath)
}
wfs.touchDirMtimeCtimeBest(newParentPath)
}
}
newEntryPath := newParentPath.Child(name)
// Map back to local uid/gid before writing attributes to the kernel.
wfs.mapPbIdFromFilerToLocal(request.Entry)
if err != nil {
glog.V(0).Infof("Link %v -> %s: %v", oldEntryPath, newEntryPath, err)
return fuse.EIO
}
wfs.inodeToPath.AddPath(oldEntry.Attributes.Inode, newEntryPath)
// Propagate the new HardLinkCounter to sibling cache entries and
// invalidate the kernel's inode attr cache. Without this, `stat` on any
// existing sibling link (other than the source we just wrote) returns
// the old nlink from the local metacache — pjdfstest link/00.t catches
// this after `link n1 n2` when it stats n0.
wfs.syncHardLinkSiblings(oldEntry.Attributes.Inode, oldEntry, oldEntryPath, newEntryPath)
wfs.outputPbEntry(out, oldEntry.Attributes.Inode, request.Entry)
return fuse.OK
}
// syncHardLinkSiblings rewrites the cached HardLinkCounter (and ctime) on
// every sibling link of the given inode, and invalidates the kernel's inode
// attr cache. `authoritativeEntry` carries the counter/ctime that should be
// propagated. `skipPaths` are the link paths already updated by the caller
// (typically the source and/or the newly created/removed link).
func (wfs *WFS) syncHardLinkSiblings(inode uint64, authoritativeEntry *filer_pb.Entry, skipPaths ...util.FullPath) {
if authoritativeEntry == nil || len(authoritativeEntry.HardLinkId) == 0 {
return
}
paths := wfs.inodeToPath.GetAllPaths(inode)
if len(paths) == 0 {
return
}
skip := make(map[util.FullPath]struct{}, len(skipPaths))
for _, p := range skipPaths {
skip[p] = struct{}{}
}
ctx := context.Background()
for _, p := range paths {
if _, skipped := skip[p]; skipped {
continue
}
sibling, err := wfs.metaCache.FindEntry(ctx, p)
if err != nil || sibling == nil {
continue
}
// Only touch siblings that genuinely share the same hard-link id.
// inodeToPath's shared-inode invariant should already guarantee
// this, but a mismatch can occur transiently (e.g. a rename
// replaced one of the paths), and blindly stamping an unrelated
// entry's counter would corrupt it.
if !bytes.Equal(sibling.HardLinkId, authoritativeEntry.HardLinkId) {
continue
}
sibling.HardLinkCounter = authoritativeEntry.HardLinkCounter
if authoritativeEntry.Attributes != nil {
sibling.Attr.Ctime = time.Unix(authoritativeEntry.Attributes.Ctime, int64(authoritativeEntry.Attributes.CtimeNs))
}
if err := wfs.metaCache.UpdateEntry(ctx, sibling); err != nil {
glog.V(4).Infof("syncHardLinkSiblings update %s: %v", p, err)
}
}
// Note: we deliberately do NOT call fuseServer.InodeNotify here. That
// call would be made from the FUSE Link request handler goroutine, and
// writes onto the same /dev/fuse fd that the kernel is still waiting to
// read the Link reply from — causing a self-notify deadlock. The kernel
// will re-stat siblings once its attr-cache TTL expires.
}