mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
* fix(mount): serialize hard-link mutations on HardLinkId syncHardLinkSiblings stamps every sibling of a hard-link to authoritativeEntry.HardLinkCounter, and the caller computes that value as entry.HardLinkCounter - 1 (Unlink) or entry.HardLinkCounter + 1 (Link) from a cached entry read before the filer mutation. With concurrent Unlinks on different links of the same file, both callers observe the same pre-decrement counter, the filer's atomic blob decrement lands correctly, but both then stamp their siblings to counter-1 — leaving the mount metacache one higher than the authoritative blob. Serialize Link and Unlink on string(HardLinkId) via a new hardLinkLockTable on WFS, and re-load the entry under the lock so the second caller sees the updated sibling counter its predecessor just wrote before computing its own delta. First-link races (empty HardLinkId on the source) are a separate pre-existing issue and are not addressed here. Full pjdfstest suite still passes (235 files, 8803 tests). * fix(mount): abort on stale pre-lock entry after HardLinkId lock Review follow-up: if maybeLoadEntry fails after acquiring the hardLinkLockTable lock, the prior revision silently fell back to the pre-lock snapshot, reintroducing the stale-base update the lock is meant to prevent. - Unlink: treat fuse.ENOENT as success (the file was already removed by the thread that held the lock before us) and propagate any other error. - Link: abort with the returned status so we never derive the next HardLinkCounter from a stale source entry. * fix(mount): re-resolve Link source alias under HardLinkId lock Review follow-up: Link resolved oldEntryPath from in.Oldnodeid before waiting on the HardLinkId lock. A concurrent Unlink that held the same lock could remove the specific alias we picked pre-lock while leaving other sibling hard links for the same inode intact. The post-lock maybeLoadEntry then returned ENOENT even though the source inode was still reachable. Call GetPath(in.Oldnodeid) again under the lock to pick whichever alias is still active, refresh oldParentPath, and only return ENOENT if no sibling survived.
240 lines
8.3 KiB
Go
240 lines
8.3 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)
|
|
}
|
|
}
|
|
if wfs.fuseServer != nil {
|
|
if status := wfs.fuseServer.InodeNotify(inode, 0, -1); status != fuse.OK {
|
|
glog.V(4).Infof("syncHardLinkSiblings invalidate inode %d: %v", inode, status)
|
|
}
|
|
}
|
|
}
|