mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 16:28:30 +01:00
* mount: keep flushing an open file whose other hard link survives an unlink Unlink marks the inode's open handle deleted so no flush recreates the removed name. When the entry was one of several hard links the inode lives on under another name, yet the mark stayed and every later fsync and close returned OK without publishing metadata. Lift the unlink's own mark once the name is gone and the inode still has a path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: carry a link's stored entry into the file's open handle Link rewrites the source entry with the new hard link id, count and times, but an open handle kept its old base and entry. Its next conditional content flush was refused (EIO), and an unconditional one would have dropped the hard link id. Move the handle onto the stored entry when only the link fields and times changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: carry an alias unlink's stored entry into the open handle Removing one of several hard links lowers the stored link count and stamps a new ctime, and the handle may still hold the removed name. Look up the entry at the name the handle flushes through and adopt it, so the next content flush matches the stored entry, writes the right count, and attribute saves address the surviving name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: test an open handle across link and alias unlink Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: read an alias unlink's lost entry again at the next flush When the read of the surviving entry right after an alias unlink fails, the handle kept a base with the old link count and ctime, and every later flush was refused even after the filer recovered. Leave the hard link id on the handle and have the next flush read the stored entry and adopt it before publishing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: publish a queued close before unlinking a name another link keeps With writeback caching, close only queues the flush. Unlink marked the handle deleted before waiting for that flush, so the flush skipped its metadata and the surviving link kept the content from before the close. When another link keeps the file, wait for the queued flush first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: let a queued close reach the filer before linking the file With writeback caching a new file is created on the filer by its queued close, and a listed directory serves the source from the cache, so a link right after close updated a name the filer did not have yet and failed with EIO. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: skip a link change no newer than the handle's entry A link change carried into the handle could replace a base already moved past it by a later acknowledgment. Use the same clock-domain check as installAckedEntry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: keep a link change's ctime in the dirty entry The flush does not restamp times, so a dirty entry carrying an earlier write's ctime moved the filer's ctime back from the link or unlink. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: flush through the file's current name when the resolved one is gone doFlush resolves the name before uploading the data. An unlink of that alias, or a rename, in the meantime sent the publish to a name the file no longer has. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * 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> * mount: test link and alias unlink across queued closes, flush races, versions and ctime Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: pick a flush's name again once its locks are held An unlink or rename that lands between the name pick and the locks would otherwise send the flush to the removed name, where the create guard fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: test a flush waiting on its lock through an alias unlink Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: check the flush target after every lock acquisition A flush whose name kept changing published to the name chosen before its last lock wait. Check after each acquisition and return a retryable error once the attempts run out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * mount: test a flush whose name changes during every lock wait Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
388 lines
15 KiB
Go
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.
|
|
}
|