mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-27 15:30:45 +01:00
* cluster: never broadcast an empty lock ring An empty member list is never a usable ring state, but a delayed RemoveServer on a former leader can fire after the new leader already broadcast the recovered ring. That late broadcast carries a newer wall-clock version, so clients accept the empty ring and permanently reject the good one. Skip the broadcast entirely when the member list is empty, keeping the last non-empty snapshot for reconnecting clients. * cluster: periodically rebroadcast the lock ring Ring updates are purely event-driven, so one lost or poisoned update is permanent until the next membership change — with a single filer that may never come. Re-arm a per-group timer after every broadcast so the current leader keeps re-sending the ring; clients reject nothing newer than their last accepted version, so a re-sent snapshot always heals a stale view. * filer,s3api: reset the lock ring on master change Ring versions are per-master monotonic — each master stamps wall-clock nanoseconds — so a late high-version update accepted from a former leader makes the new leader's snapshot look stale forever. Detect a leader change across the reconnect gap (currentMaster is cleared between attempts, so remember the last served master) and reset the ring to bootstrap state so the new leader's view always applies. * cluster: fail lock acquisition when no lock server exists retryUntilLocked loops forever, so a filer reporting an empty lock ring wedges every append write indefinitely. Bound only the "no lock server found" case — ordinary contention is still waited out since the holder releases eventually. The constructors now return nil on failure: the filer append path and S3 object writes fail fast, while mounts degrade to their existing lockless mode. * cluster: reset only the ring version on master change Ring versions are per-master monotonic, so a version gate reset is all a leader change needs. Clearing the whole ring made every filer its own write owner until the next update and dropped the prior-owner window for keys the new leader remaps; the last ring now keeps routing until the new leader's snapshot transitions off it. * cluster: skip redundant ring installs and defer rebroadcasts An unchanged member list now only bumps the accepted version instead of installing a snapshot: periodic rebroadcasts no longer fire the topology-change callback or restart the prior-owner window. And a rebroadcast that lands inside a membership stabilization window yields to the pending timer rather than publishing an intermediate ring. * cluster,mount: bound lock unavailability, fail ops that cannot lock Only 'lock already owned' contention retries without bound now; every other failure — no lock server, or a dead ring member refusing connections — shares the same unavailability budget, so a ring naming departed filers can no longer hang a lock forever. Mount open-write, create, and rename fail with EAGAIN when the required lock cannot be acquired instead of proceeding without cross-mount serialization. * cluster: check pending stabilization inside the broadcast critical section rebroadcast released the mutex between the pending-timer check and nextBroadcastUpdate, so a membership change arriving in the gap could arm a stabilization timer while the rebroadcast emitted an intermediate ring. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: acquire path locks before mutating create/rename state Create took the DLM lock only after the filer create, so a lock failure returned EAGAIN with an eagerly persisted file left behind. Rename marked source handles renamed before acquiring locks, so a failed acquisition left them suppressing old-path flushes for a rename that never happened. Both now take the locks first; the create's lock is released again if the entry race loses to another creator and AcquireHandle takes over. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: keep the old-path lock when rename lock migration fails The migration stopped the handle's lock before acquiring the replacement, so a nil result left the handle writing with no lock at all. Acquiring the new-path lock first means failure keeps the existing lock instead of reporting success with serialization dropped. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: skip new-path rename lock when a handle already holds it A target file open for write on this mount already carries a lock on newPath; the lock manager does not grant a second lock to the same owner, so the rename would wait on itself until the handle closed. Also avoid locking twice when old and new paths coincide. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: hand the rename's target lock to the migrating handle The rename holds a lock on newPath for its duration, so the response migration's fresh acquisition waited on that same lock until the handle released — under fhLockTable, blocking the handle's own close. Adopt the rename's lock directly; nested move responses still acquire their own. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: move the replaced target's lock to the renamed handle When the target path was already locked by an open handle on this mount, the migrated source handle kept only its stale old-path lock — the target's close would then release the last lock on the new path while the renamed handle was still open. Adopt the replaced handle's lock instead. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: stop the handle lock inside the fh lock on release ReleaseHandle stopped fh.dlmLock before taking the fhLockTable slot, so a rename migration holding that slot could still observe and adopt a lock that was already stopping. Stopping under the fh lock makes the transfer serialize against the release. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: claim the replaced target's lock for the renamed handle When the target path is already locked by an open handle on this mount, adopting it at migration time keeps the renamed path protected after that handle closes, without waiting on a lock this mount already holds. If the handle was released mid-migration the claimed lock is stopped, and a fresh acquire covers the case where it was already gone. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: claim the target handle's lock before the rename runs Skipping the new-path lock when a handle already holds it let that handle's close release the lock mid-rename, leaving the path unguarded until the response migrated it. Take over the lock at check time and hold it for the rename's duration: the response adopts it for the migrating handle, or it returns to the target handle / is released on failure. The target handle lookup also falls back to the entry's stored inode for a forgotten path mapping. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: read handle locks only under the fh lock during rename The loose dlmLock reads raced ReleaseHandle, which now mutates the lock inside the handle lock; check and claim it under the same hold. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
525 lines
20 KiB
Go
525 lines
20 KiB
Go
package mount
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/seaweedfs/go-fuse/v2/fuse"
|
|
"github.com/seaweedfs/seaweedfs/weed/cluster"
|
|
"github.com/seaweedfs/seaweedfs/weed/cluster/lock_manager"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// doRename tries the streaming mux first, falling back to unary on transport errors.
|
|
func (wfs *WFS) doRename(ctx context.Context, request *filer_pb.StreamRenameEntryRequest, oldPath, newPath util.FullPath, newPathLock **cluster.LiveLock) error {
|
|
if wfs.streamMutate != nil && wfs.streamMutate.IsAvailable() {
|
|
err := wfs.streamMutate.Rename(ctx, request, func(resp *filer_pb.StreamRenameEntryResponse) error {
|
|
return wfs.handleRenameResponse(ctx, resp, newPath, newPathLock)
|
|
})
|
|
if err == nil || !errors.Is(err, ErrStreamTransport) {
|
|
return err // success or application error
|
|
}
|
|
glog.V(1).Infof("Rename %s => %s: stream failed, falling back to unary: %v", oldPath, newPath, err)
|
|
}
|
|
return wfs.WithFilerClient(true, func(client filer_pb.SeaweedFilerClient) error {
|
|
stream, streamErr := client.StreamRenameEntry(ctx, request)
|
|
if streamErr != nil {
|
|
return fmt.Errorf("dir AtomicRenameEntry %s => %s : %v", oldPath, newPath, streamErr)
|
|
}
|
|
for {
|
|
resp, recvErr := stream.Recv()
|
|
if recvErr != nil {
|
|
if recvErr == io.EOF {
|
|
break
|
|
}
|
|
return fmt.Errorf("dir Rename %s => %s receive: %v", oldPath, newPath, recvErr)
|
|
}
|
|
if err := wfs.handleRenameResponse(ctx, resp, newPath, newPathLock); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
/** Rename a file
|
|
*
|
|
* If the target exists it should be atomically replaced. If
|
|
* the target's inode's lookup count is non-zero, the file
|
|
* system is expected to postpone any removal of the inode
|
|
* until the lookup count reaches zero (see description of the
|
|
* forget function).
|
|
*
|
|
* If this request is answered with an error code of ENOSYS, this is
|
|
* treated as a permanent failure with error code EINVAL, i.e. all
|
|
* future bmap requests will fail with EINVAL without being
|
|
* send to the filesystem process.
|
|
*
|
|
* *flags* may be `RENAME_EXCHANGE` or `RENAME_NOREPLACE`. If
|
|
* RENAME_NOREPLACE is specified, the filesystem must not
|
|
* overwrite *newname* if it exists and return an error
|
|
* instead. If `RENAME_EXCHANGE` is specified, the filesystem
|
|
* must atomically exchange the two files, i.e. both must
|
|
* exist and neither may be deleted.
|
|
*
|
|
* Valid replies:
|
|
* fuse_reply_err
|
|
*
|
|
* @param req request handle
|
|
* @param parent inode number of the old parent directory
|
|
* @param name old name
|
|
* @param newparent inode number of the new parent directory
|
|
* @param newname new name
|
|
*/
|
|
/*
|
|
renameat2()
|
|
renameat2() has an additional flags argument. A renameat2() call
|
|
with a zero flags argument is equivalent to renameat().
|
|
|
|
The flags argument is a bit mask consisting of zero or more of
|
|
the following flags:
|
|
|
|
RENAME_EXCHANGE
|
|
Atomically exchange oldpath and newpath. Both pathnames
|
|
must exist but may be of different types (e.g., one could
|
|
be a non-empty directory and the other a symbolic link).
|
|
|
|
RENAME_NOREPLACE
|
|
Don't overwrite newpath of the rename. Return an error if
|
|
newpath already exists.
|
|
|
|
RENAME_NOREPLACE can't be employed together with
|
|
RENAME_EXCHANGE.
|
|
|
|
RENAME_NOREPLACE requires support from the underlying
|
|
filesystem. Support for various filesystems was added as
|
|
follows:
|
|
|
|
* ext4 (Linux 3.15);
|
|
|
|
* btrfs, tmpfs, and cifs (Linux 3.17);
|
|
|
|
* xfs (Linux 4.0);
|
|
|
|
* Support for many other filesystems was added in Linux
|
|
4.9, including ext2, minix, reiserfs, jfs, vfat, and
|
|
bpf.
|
|
|
|
RENAME_WHITEOUT (since Linux 3.18)
|
|
This operation makes sense only for overlay/union
|
|
filesystem implementations.
|
|
|
|
Specifying RENAME_WHITEOUT creates a "whiteout" object at
|
|
the source of the rename at the same time as performing
|
|
the rename. The whole operation is atomic, so that if the
|
|
rename succeeds then the whiteout will also have been
|
|
created.
|
|
|
|
A "whiteout" is an object that has special meaning in
|
|
union/overlay filesystem constructs. In these constructs,
|
|
multiple layers exist and only the top one is ever
|
|
modified. A whiteout on an upper layer will effectively
|
|
hide a matching file in the lower layer, making it appear
|
|
as if the file didn't exist.
|
|
|
|
When a file that exists on the lower layer is renamed, the
|
|
file is first copied up (if not already on the upper
|
|
layer) and then renamed on the upper, read-write layer.
|
|
At the same time, the source file needs to be "whiteouted"
|
|
(so that the version of the source file in the lower layer
|
|
is rendered invisible). The whole operation needs to be
|
|
done atomically.
|
|
|
|
When not part of a union/overlay, the whiteout appears as
|
|
a character device with a {0,0} device number. (Note that
|
|
other union/overlay implementations may employ different
|
|
methods for storing whiteout entries; specifically, BSD
|
|
union mount employs a separate inode type, DT_WHT, which,
|
|
while supported by some filesystems available in Linux,
|
|
such as CODA and XFS, is ignored by the kernel's whiteout
|
|
support code, as of Linux 4.19, at least.)
|
|
|
|
RENAME_WHITEOUT requires the same privileges as creating a
|
|
device node (i.e., the CAP_MKNOD capability).
|
|
|
|
RENAME_WHITEOUT can't be employed together with
|
|
RENAME_EXCHANGE.
|
|
|
|
RENAME_WHITEOUT requires support from the underlying
|
|
filesystem. Among the filesystems that support it are
|
|
tmpfs (since Linux 3.18), ext4 (since Linux 3.18), XFS
|
|
(since Linux 4.1), f2fs (since Linux 4.2), btrfs (since
|
|
Linux 4.7), and ubifs (since Linux 4.9).
|
|
*/
|
|
const (
|
|
RenameEmptyFlag = 0
|
|
RenameNoReplace = 1
|
|
RenameExchange = 2
|
|
RenameWhiteout = 3
|
|
)
|
|
|
|
func (wfs *WFS) Rename(cancel <-chan struct{}, in *fuse.RenameIn, oldName string, newName string) (code fuse.Status) {
|
|
if wfs.IsOverQuotaWithUncommitted() {
|
|
return fuse.Status(syscall.ENOSPC)
|
|
}
|
|
|
|
// Both names end up in StreamRenameEntryRequest (proto string fields), so
|
|
// sanitize both. checkName handles newName's length check; oldName may
|
|
// legitimately be a pre-existing entry whose bytes were not validated.
|
|
oldName = sanitizeFuseName(oldName)
|
|
var s fuse.Status
|
|
if newName, s = checkName(newName); s != fuse.OK {
|
|
return s
|
|
}
|
|
|
|
switch in.Flags {
|
|
case RenameEmptyFlag:
|
|
case RenameNoReplace:
|
|
case RenameExchange:
|
|
case RenameWhiteout:
|
|
return fuse.ENOTSUP
|
|
default:
|
|
return fuse.EINVAL
|
|
}
|
|
|
|
oldDir, code := wfs.inodeToPath.GetPath(in.NodeId)
|
|
if code != fuse.OK {
|
|
return
|
|
}
|
|
oldPath := oldDir.Child(oldName)
|
|
newDir, code := wfs.inodeToPath.GetPath(in.Newdir)
|
|
if code != fuse.OK {
|
|
return
|
|
}
|
|
newPath := newDir.Child(newName)
|
|
|
|
oldEntry, _, status := wfs.maybeLoadEntry(oldPath)
|
|
if status != fuse.OK {
|
|
return status
|
|
}
|
|
|
|
// POSIX: enforce sticky bit on the source directory.
|
|
if oldDirEntry, _, dirCode := wfs.maybeLoadEntry(oldDir); dirCode == fuse.OK && oldDirEntry != nil && oldDirEntry.Attributes != nil {
|
|
targetUid := uint32(0)
|
|
if oldEntry != nil && oldEntry.Attributes != nil {
|
|
targetUid = oldEntry.Attributes.Uid
|
|
}
|
|
if code := checkStickyBit(oldDirEntry.Attributes.FileMode, oldDirEntry.Attributes.Uid, targetUid, in.Uid); code != fuse.OK {
|
|
return code
|
|
}
|
|
}
|
|
|
|
var newEntry *filer_pb.Entry
|
|
if in.Flags != RenameNoReplace {
|
|
if loaded, _, newStatus := wfs.maybeLoadEntry(newPath); newStatus == fuse.OK {
|
|
newEntry = loaded
|
|
}
|
|
}
|
|
|
|
// POSIX: enforce sticky bit on the destination directory when replacing an existing entry.
|
|
if newEntry != nil {
|
|
if newDirEntry, _, dirCode := wfs.maybeLoadEntry(newDir); dirCode == fuse.OK && newDirEntry != nil && newDirEntry.Attributes != nil {
|
|
targetUid := uint32(0)
|
|
if newEntry.Attributes != nil {
|
|
targetUid = newEntry.Attributes.Uid
|
|
}
|
|
if code := checkStickyBit(newDirEntry.Attributes.FileMode, newDirEntry.Attributes.Uid, targetUid, in.Uid); code != fuse.OK {
|
|
return code
|
|
}
|
|
}
|
|
}
|
|
|
|
if wormEnforced, _ := wfs.wormEnforcedForEntry(oldPath, oldEntry); wormEnforced {
|
|
return fuse.EPERM
|
|
}
|
|
|
|
glog.V(4).Infof("dir Rename %s => %s", oldPath, newPath)
|
|
|
|
// Acquire DLM locks on both old and new paths to prevent another mount
|
|
// from opening either path for writing during the rename. Lock in
|
|
// sorted order to prevent deadlocks when two mounts rename in opposite
|
|
// directions (A→B vs B→A).
|
|
//
|
|
// Skip the old-path lock if this mount already holds it via an open
|
|
// file handle (otherwise we'd deadlock trying to re-acquire our own lock).
|
|
// Acquiring before the handle marks below keeps a lock failure from
|
|
// leaving source handles flagged for a rename that never happened.
|
|
var heldLocks []*cluster.LiveLock
|
|
var newPathLock *cluster.LiveLock
|
|
if wfs.lockClient != nil {
|
|
// A handle open on the target holds its lock; claiming it up front
|
|
// keeps the path locked through the rename (its close would otherwise
|
|
// release it mid-flight) without waiting on our own ownership.
|
|
var newPathLockFh *FileHandle
|
|
defer func() {
|
|
for _, l := range heldLocks {
|
|
l.Stop()
|
|
}
|
|
if newPathLock != nil {
|
|
// Never adopted by a migrated handle — return it to the
|
|
// handle it came from if that is still open.
|
|
if newPathLockFh != nil {
|
|
if cur, ok := wfs.fhMap.FindFileHandle(newPathLockFh.inode); ok && cur == newPathLockFh {
|
|
lk := wfs.fhLockTable.AcquireLock("renameDLM", cur.fh, util.ExclusiveLock)
|
|
if cur.dlmLock == nil {
|
|
cur.dlmLock = newPathLock
|
|
newPathLock = nil
|
|
}
|
|
wfs.fhLockTable.ReleaseLock(cur.fh, lk)
|
|
}
|
|
}
|
|
if newPathLock != nil {
|
|
newPathLock.Stop()
|
|
}
|
|
}
|
|
}()
|
|
owner := fmt.Sprintf("mount-%d", wfs.signature)
|
|
|
|
// Check if the source file handle already holds a DLM lock on oldPath
|
|
oldPathAlreadyLocked := false
|
|
sourceInode, sourceMapped := wfs.inodeToPath.GetInode(oldPath)
|
|
if !sourceMapped && oldEntry != nil && oldEntry.Attributes != nil {
|
|
sourceInode = oldEntry.Attributes.Inode
|
|
}
|
|
if sourceInode != 0 {
|
|
if fh, ok := wfs.fhMap.FindFileHandle(sourceInode); ok {
|
|
lk := wfs.fhLockTable.AcquireLock("renameDLM", fh.fh, util.ExclusiveLock)
|
|
oldPathAlreadyLocked = fh.dlmLock != nil
|
|
wfs.fhLockTable.ReleaseLock(fh.fh, lk)
|
|
}
|
|
}
|
|
targetInode, targetMapped := wfs.inodeToPath.GetInode(newPath)
|
|
if !targetMapped && newEntry != nil && newEntry.Attributes != nil {
|
|
targetInode = newEntry.Attributes.Inode
|
|
}
|
|
if targetInode != 0 && targetInode != sourceInode {
|
|
if targetFh, ok := wfs.fhMap.FindFileHandle(targetInode); ok {
|
|
targetFhLock := wfs.fhLockTable.AcquireLock("renameDLM", targetFh.fh, util.ExclusiveLock)
|
|
if targetFh.dlmLock != nil {
|
|
newPathLock = targetFh.dlmLock
|
|
targetFh.dlmLock = nil
|
|
newPathLockFh = targetFh
|
|
}
|
|
wfs.fhLockTable.ReleaseLock(targetFh.fh, targetFhLock)
|
|
}
|
|
}
|
|
|
|
// Determine which paths need new DLM locks
|
|
pathsToLock := []string{}
|
|
if newPathLock == nil {
|
|
pathsToLock = append(pathsToLock, string(newPath))
|
|
}
|
|
if !oldPathAlreadyLocked && string(oldPath) != string(newPath) {
|
|
pathsToLock = append(pathsToLock, string(oldPath))
|
|
}
|
|
// Sort for consistent lock ordering
|
|
if len(pathsToLock) == 2 && pathsToLock[0] > pathsToLock[1] {
|
|
pathsToLock[0], pathsToLock[1] = pathsToLock[1], pathsToLock[0]
|
|
}
|
|
|
|
for _, p := range pathsToLock {
|
|
dlmLock := wfs.lockClient.NewBlockingLongLivedLock(p, owner, lock_manager.LiveLockTTL)
|
|
if dlmLock == nil {
|
|
return fuse.Status(syscall.EAGAIN)
|
|
}
|
|
if p == string(newPath) {
|
|
newPathLock = dlmLock
|
|
} else {
|
|
heldLocks = append(heldLocks, dlmLock)
|
|
}
|
|
}
|
|
glog.V(1).Infof("DLM locks acquired for rename %s => %s (oldPathAlreadyLocked=%v)", oldPath, newPath, oldPathAlreadyLocked)
|
|
}
|
|
|
|
// Ensure the source file's metadata exists on the filer before renaming.
|
|
// Two cases can leave the entry only in the local cache:
|
|
// 1. deferFilerCreate=true — file handle still open, dirtyMetadata set.
|
|
// 2. writebackCache — close() triggered async flush, handle released.
|
|
// The filer rename will fail with ENOENT unless we flush/wait first.
|
|
sourceInode, sourceMapped := wfs.inodeToPath.GetInode(oldPath)
|
|
if sourceMapped {
|
|
// Case 1: handle still open with deferred metadata — flush synchronously
|
|
// BEFORE any async flush interference.
|
|
if fh, ok := wfs.fhMap.FindFileHandle(sourceInode); ok && fh.dirtyMetadata {
|
|
glog.V(4).Infof("dir Rename %s: flushing deferred metadata before rename", oldPath)
|
|
// Prerequisite for the rename, so it must complete: non-cancellable context.
|
|
if flushStatus := wfs.doFlush(context.Background(), fh, oldEntry.Attributes.Uid, oldEntry.Attributes.Gid, false); flushStatus != fuse.OK {
|
|
glog.Warningf("dir Rename %s: flush before rename failed: %v", oldPath, flushStatus)
|
|
return flushStatus
|
|
}
|
|
}
|
|
// Case 2: handle already released, async flush may be in flight.
|
|
// Mark ALL handles for this inode as renamed so the async flush
|
|
// skips old-path metadata creation (which would re-insert the
|
|
// renamed entry into the meta cache after rename events clean it up).
|
|
wfs.fhMap.MarkInodeRenamed(sourceInode)
|
|
wfs.waitForPendingAsyncFlush(sourceInode)
|
|
} else if oldEntry != nil && oldEntry.Attributes != nil && oldEntry.Attributes.Inode != 0 {
|
|
// GetInode failed (Forget already removed the mapping), but the
|
|
// entry's stored inode can still identify pending async flushes.
|
|
sourceInode = oldEntry.Attributes.Inode
|
|
wfs.fhMap.MarkInodeRenamed(sourceInode)
|
|
wfs.waitForPendingAsyncFlush(sourceInode)
|
|
}
|
|
|
|
// Replacing the destination deletes what it held, so a handle still open
|
|
// on that entry has to stop writing the name back, exactly as an unlink
|
|
// makes it. Windows is where this bites: the close carrying the flush runs
|
|
// after the application's CloseHandle returned, so the flush can land on
|
|
// top of what the rename just put there.
|
|
if in.Flags == RenameEmptyFlag {
|
|
targetInode, targetMapped := wfs.inodeToPath.GetInode(newPath)
|
|
if !targetMapped && newEntry != nil && newEntry.Attributes != nil {
|
|
// Forget dropped the mapping, but the entry's stored inode still
|
|
// names the handle, the way the source side falls back.
|
|
targetInode = newEntry.Attributes.Inode
|
|
}
|
|
if targetInode != 0 && targetInode != sourceInode {
|
|
// Drained before the mark, not after: a flush already queued
|
|
// belongs to the destination as it stands, and one that runs
|
|
// marked is skipped and takes its handle with it, past the reach
|
|
// of the restore below.
|
|
wfs.waitForPendingAsyncFlush(targetInode)
|
|
epoch := wfs.markHandleDeleted(targetInode)
|
|
// A rename that does not happen leaves the destination where it
|
|
// was, so its handle has to go back to writing that name.
|
|
defer func() {
|
|
if code != fuse.OK {
|
|
wfs.clearHandleDeleted(targetInode, epoch)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// update remote filer
|
|
request := &filer_pb.StreamRenameEntryRequest{
|
|
OldDirectory: string(oldDir),
|
|
OldName: oldName,
|
|
NewDirectory: string(newDir),
|
|
NewName: newName,
|
|
Signatures: []int32{wfs.signature},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
err := wfs.doRename(ctx, request, oldPath, newPath, &newPathLock)
|
|
if err != nil {
|
|
glog.V(0).Infof("Rename %s => %s: %v", oldPath, newPath, err)
|
|
// Map error strings to FUSE status codes. String matching is used
|
|
// instead of raw errno to stay portable across platforms (errno
|
|
// numeric values differ between Linux and macOS).
|
|
msg := err.Error()
|
|
if strings.Contains(msg, "not found") {
|
|
return fuse.Status(syscall.ENOENT)
|
|
} else if strings.Contains(msg, "not empty") {
|
|
return fuse.Status(syscall.ENOTEMPTY)
|
|
} else if strings.Contains(msg, "not directory") {
|
|
return fuse.ENOTDIR
|
|
}
|
|
return fuse.EIO
|
|
}
|
|
wfs.inodeToPath.TouchDirectory(oldDir)
|
|
wfs.inodeToPath.TouchDirectory(newDir)
|
|
wfs.touchDirMtimeCtimeBest(oldDir)
|
|
if oldDir != newDir {
|
|
wfs.touchDirMtimeCtimeBest(newDir)
|
|
// Adjust subdirectory counts when moving a directory across parents.
|
|
if oldEntry != nil && oldEntry.IsDirectory {
|
|
wfs.inodeToPath.AdjustSubdirCount(oldDir, -1)
|
|
wfs.inodeToPath.AdjustSubdirCount(newDir, 1)
|
|
}
|
|
}
|
|
|
|
return fuse.OK
|
|
|
|
}
|
|
|
|
func (wfs *WFS) handleRenameResponse(ctx context.Context, resp *filer_pb.StreamRenameEntryResponse, renameNewPath util.FullPath, renameNewPathLock **cluster.LiveLock) error {
|
|
// comes from filer StreamRenameEntry, can only be create or delete entry
|
|
|
|
glog.V(4).Infof("dir Rename %+v", resp.EventNotification)
|
|
|
|
if resp.EventNotification.NewEntry != nil {
|
|
if err := wfs.applyLocalMetadataEvent(ctx, metadataEventFromRenameResponse(resp)); err != nil {
|
|
glog.Warningf("rename apply metadata event: %v", err)
|
|
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(resp.Directory))
|
|
if resp.EventNotification.NewParentPath != "" {
|
|
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(resp.EventNotification.NewParentPath))
|
|
}
|
|
}
|
|
|
|
oldParent, newParent := util.FullPath(resp.Directory), util.FullPath(resp.EventNotification.NewParentPath)
|
|
oldName, newName := resp.EventNotification.OldEntry.Name, resp.EventNotification.NewEntry.Name
|
|
|
|
oldPath := oldParent.Child(oldName)
|
|
newPath := newParent.Child(newName)
|
|
|
|
sourceInode, targetInode := wfs.inodeToPath.MovePath(oldPath, newPath)
|
|
if sourceInode != 0 {
|
|
fh, foundFh := wfs.fhMap.FindFileHandle(sourceInode)
|
|
if foundFh {
|
|
if entry := fh.GetEntry(); entry != nil {
|
|
entry.Name = newName
|
|
}
|
|
// Keep the saved handle path current so any flush fallback
|
|
// after Forget uses the post-rename location, not the old one.
|
|
fh.RememberPath(newPath)
|
|
|
|
// Migrate the DLM lock from old path to new path so the
|
|
// lock key matches the current file location. Hold the
|
|
// fhLockTable to prevent ReleaseHandle from concurrently
|
|
// stopping the lock during migration. Acquire before
|
|
// releasing: a failed migration keeps the old lock rather
|
|
// than leaving the handle unlocked.
|
|
if wfs.lockClient != nil && oldPath != newPath {
|
|
fhActiveLock := wfs.fhLockTable.AcquireLock("renameDLM", fh.fh, util.ExclusiveLock)
|
|
if fh.dlmLock != nil {
|
|
var newLock *cluster.LiveLock
|
|
if newPath == renameNewPath && renameNewPathLock != nil && *renameNewPathLock != nil {
|
|
// The rename already holds a lock on the target;
|
|
// adopt it — re-acquiring our own lock would block
|
|
// until this handle is released.
|
|
newLock = *renameNewPathLock
|
|
*renameNewPathLock = nil
|
|
} else {
|
|
owner := fmt.Sprintf("mount-%d", wfs.signature)
|
|
newLock = wfs.lockClient.NewBlockingLongLivedLock(
|
|
string(newPath), owner, lock_manager.LiveLockTTL,
|
|
)
|
|
}
|
|
if newLock != nil {
|
|
fh.dlmLock.Stop()
|
|
fh.dlmLock = newLock
|
|
glog.V(1).Infof("DLM lock migrated from %s to %s", oldPath, newPath)
|
|
} else {
|
|
glog.Warningf("DLM lock migration to %s failed; keeping lock on %s", newPath, oldPath)
|
|
}
|
|
}
|
|
wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock)
|
|
}
|
|
}
|
|
// invalidate attr and data
|
|
// wfs.fuseServer.InodeNotify(sourceInode, 0, -1)
|
|
}
|
|
if targetInode != 0 {
|
|
// invalidate attr and data
|
|
// wfs.fuseServer.InodeNotify(targetInode, 0, -1)
|
|
}
|
|
|
|
} else if resp.EventNotification.OldEntry != nil {
|
|
// without new entry, only old entry name exists. This is the second step to delete old entry
|
|
if err := wfs.applyLocalMetadataEvent(ctx, metadataEventFromRenameResponse(resp)); err != nil {
|
|
glog.Warningf("rename apply delete event: %v", err)
|
|
wfs.inodeToPath.InvalidateChildrenCache(util.FullPath(resp.Directory))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|