Files
seaweedfs/weed/mount/weedfs_link.go
T
Chris LuandClaude Opus 5.5 8fcb21c977 mount: keep the pending link read when its name is already gone
A flush could read the removed alias name, find nothing, and drop the
pending read of the surviving link, leaving the stale base in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 01:46:13 -08:00

388 lines
15 KiB
Go

package mount
import (
"bytes"
"context"
"errors"
"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"
"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)
}
var s fuse.Status
if name, 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()
// The new link has to be reported with the node id the kernel already holds
// for the source, which is what makes the two names share one file.
// oldEntry.Attributes.Inode is not a substitute. It is a mount-runtime
// number that only entries created through a mount carry: entries written
// by the S3 API, WebDAV or a direct filer call persist inode 0, and the
// kernel rejects a LINK reply with NodeId 0 as EIO (invalid_nodeid). It is
// also the wrong key for inodeToPath, which is keyed by the numbers handed
// to the kernel rather than by the persisted attribute; Lookup's collision
// probe can move the two apart even for an entry that does carry one.
sourceInode := in.Oldnodeid
ctx, cancelFunc := ctxFromCancel(cancel)
defer cancelFunc()
// A queued close may not have created or updated the source on the filer.
if !wfs.waitForPendingAsyncFlush(ctx, sourceInode) {
return fuse.EINTR
}
oldEntry, _, status := wfs.maybeLoadEntry(ctx, 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(ctx, 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)
updateResp, err := wfs.streamUpdateEntry(context.Background(), updateOldEntryRequest)
if err == nil {
updateEvent := updateResp.GetMetadataEvent()
if updateEvent == nil {
updateEvent = metadataUpdateEvent(oldParentPath, updateOldEntryRequest.Entry)
if updateEvent != nil {
updateEvent.TsNs = ackVersionTsNs(updateResp)
}
}
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), updateEvent); applyErr != nil {
glog.Warningf("link %s: best-effort metadata apply failed: %v", oldEntryPath, applyErr)
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(oldParentPath))
}
}
var linkVersionTsNs int64
var linkSignature int32
var linkedEntry *filer_pb.Entry
if err == nil {
var createResp *filer_pb.CreateEntryResponse
createResp, err = wfs.streamCreateEntry(context.Background(), 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(context.Background(), rollbackReq); rollbackErr != nil {
glog.Warningf("link rollback %s: %v", oldEntryPath, rollbackErr)
}
} else {
linkVersionTsNs = ackVersionTsNs(createResp)
linkSignature = createResp.GetLogSignature()
createEvent := createResp.GetMetadataEvent()
if createEvent == nil {
createEvent = metadataCreateEvent(string(newParentPath), request.Entry)
if createEvent != nil {
createEvent.TsNs = linkVersionTsNs
}
}
if stored := createEvent.GetEventNotification().GetNewEntry(); stored != nil {
linkedEntry = proto.Clone(stored).(*filer_pb.Entry)
linkedEntry.Name = oldEntry.Name
}
if applyErr := wfs.applyLocalMetadataEvent(context.Background(), 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(sourceInode, newEntryPath)
if fh, found := wfs.fhMap.FindFileHandle(sourceInode); found {
fh.adoptLinkChange(linkedEntry, linkVersionTsNs, linkSignature)
}
// 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(ctx, sourceInode, oldEntry, linkVersionTsNs, oldEntryPath, newEntryPath)
wfs.outputPbEntry(out, sourceInode, request.Entry)
return fuse.OK
}
// adoptLinkChange moves the handle past a hard link change this mount made,
// stored being the entry the filer now holds at the name the handle flushes
// through, in filer ids. A base that differs from it in more than the link
// fields and times is left alone, so a peer's write is still refused. Pending
// content stays pending.
func (fh *FileHandle) adoptLinkChange(stored *filer_pb.Entry, versionTsNs int64, signature int32) {
fhActiveLock := fh.wfs.fhLockTable.AcquireLock("adoptLinkChange", fh.fh, util.ExclusiveLock)
defer fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
fh.adoptLinkChangeLocked(stored, versionTsNs, signature)
}
func (fh *FileHandle) adoptLinkChangeLocked(stored *filer_pb.Entry, versionTsNs int64, signature int32) {
if stored == nil {
return
}
handleSignature := fh.entryVersionSignature.Load()
provablyOtherClock := signature != 0 && handleSignature != 0 && signature != handleSignature
if versionTsNs != 0 && !provablyOtherClock && versionTsNs <= fh.entryVersionTsNs.Load() {
return
}
if base := fh.baseEntry.Load(); base != nil && !fh.wfs.sameApartFromLink(stored, base) {
return
}
linked := proto.Clone(stored).(*filer_pb.Entry)
fh.wfs.mapPbIdFromFilerToLocal(linked)
if fh.dirtyMetadata {
fh.UpdateEntry(func(entry *filer_pb.Entry) {
entry.Name = linked.Name
entry.HardLinkId, entry.HardLinkCounter = linked.HardLinkId, linked.HardLinkCounter
if a, b := entry.Attributes, linked.Attributes; a != nil && b != nil && (b.Ctime > a.Ctime || b.Ctime == a.Ctime && b.CtimeNs > a.CtimeNs) {
a.Ctime, a.CtimeNs = b.Ctime, b.CtimeNs
}
})
} else {
fh.SetEntry(proto.Clone(linked).(*filer_pb.Entry))
}
fh.setAuthoritativeBase(linked)
fh.advanceEntryVersion(versionTsNs, signature)
}
// adoptStoredLink reads the entry stored at path and adopts it as a change of
// hardLinkId. A failed read, or a path already gone, leaves the id on the
// handle, so its next flush reads again instead of publishing over a base the
// change made stale. The caller holds the handle's lock.
func (wfs *WFS) adoptStoredLink(ctx context.Context, fh *FileHandle, path util.FullPath, hardLinkId []byte) error {
stored, tsNs, signature, err := filer_pb.GetEntry(ctx, wfs, path)
if err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
fh.linkRefresh = hardLinkId
return err
}
if stored == nil {
fh.linkRefresh = hardLinkId
return nil
}
fh.linkRefresh = nil
if bytes.Equal(stored.GetHardLinkId(), hardLinkId) {
fh.adoptLinkChangeLocked(stored, tsNs, signature)
}
return nil
}
// sameApartFromLink reports whether stored, in filer ids, still holds base
// apart from what a link or unlink rewrites: the link fields, the times, the
// name, the commit stamp and the derived size.
func (wfs *WFS) sameApartFromLink(stored, base *filer_pb.Entry) bool {
current, observed := proto.Clone(stored).(*filer_pb.Entry), proto.Clone(base).(*filer_pb.Entry)
wfs.mapPbIdFromFilerToLocal(current)
for _, e := range []*filer_pb.Entry{current, observed} {
e.Name, e.CommitTsNs = "", 0
e.HardLinkId, e.HardLinkCounter = nil, 0
if e.Attributes != nil {
e.Attributes.Mtime, e.Attributes.MtimeNs = 0, 0
e.Attributes.Ctime, e.Attributes.CtimeNs = 0, 0
e.Attributes.FileSize = filer.FileSize(e)
}
filer_pb.BeforeEntrySerialization(e.Chunks)
}
return proto.Equal(current, observed)
}
// syncHardLinkSiblings refreshes every sibling link of the given inode from
// the filer, so their cached HardLinkCounter (and any other fields) reflect
// the mutation. `authoritativeEntry` identifies the hard-link group;
// `versionTsNs` is the log position the mutation committed at — the fetch
// runs after the commit, so the fetched state already covers it, and the
// recorded position is the higher of the two. `skipPaths` are the link paths
// already updated by the caller (typically the source and/or the newly
// created/removed link).
func (wfs *WFS) syncHardLinkSiblings(ctx context.Context, inode uint64, authoritativeEntry *filer_pb.Entry, versionTsNs int64, 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{}{}
}
// One overall deadline for the sequential sibling lookups: they run
// under the caller's exclusive hard-link lock, so a slow filer must not
// stall every other link operation on this group.
ctx, cancel := context.WithTimeout(ctx, metadataFlushTimeout)
defer cancel()
for _, p := range paths {
if _, skipped := skip[p]; skipped {
continue
}
sibling, siblingTsNs, err := wfs.metaCache.FindEntry(ctx, p)
if err != nil || sibling == nil {
continue
}
// Refresh the sibling from the filer rather than stamping the cached
// copy: the cache may predate an in-flight event of its own, and
// claiming this mutation's version for that stale content would
// fence the pending event out at the version gate, leaving old
// chunks and attributes until a refresh.
pbEntry, entryTsNs, _, fetchErr := filer_pb.GetEntry(ctx, wfs, p)
if fetchErr == nil {
// A path the filer no longer knows is on its way out — leave the
// cache alone. Only touch entries 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).
if pbEntry == nil || !bytes.Equal(pbEntry.HardLinkId, authoritativeEntry.HardLinkId) {
continue
}
if entryTsNs < versionTsNs {
entryTsNs = versionTsNs
}
dir, _ := p.DirAndName()
if err := wfs.metaCache.UpdateEntryChecked(ctx, filer.FromPbEntry(dir, pbEntry), entryTsNs); err != nil {
glog.V(4).Infof("syncHardLinkSiblings update %s: %v", p, err)
}
continue
}
// The fetch failed: patch the counter in place so the sibling is
// not left stale, but claim only the version its content already
// holds — a newer in-flight event still lands and corrects it,
// while a replayed older delete stays fenced instead of removing
// a live link.
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, siblingTsNs); 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.
}