From 482b6a03cac08f4e841ba16dbdc9cc3cef0bf8d8 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 13 Nov 2026 02:45:27 -0800 Subject: [PATCH] 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 --- weed/mount/weedfs_file_sync.go | 43 ++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/weed/mount/weedfs_file_sync.go b/weed/mount/weedfs_file_sync.go index b407ad449e..e333229110 100644 --- a/weed/mount/weedfs_file_sync.go +++ b/weed/mount/weedfs_file_sync.go @@ -205,24 +205,30 @@ func (wfs *WFS) doFlush(ctx context.Context, fh *FileHandle, uid, gid uint32, al // from open-for-write through close, so no additional distributed lock is // needed here. The local fhLockTable lock below serializes within this mount. func (wfs *WFS) flushMetadataToFiler(ctx context.Context, fh *FileHandle, dir, name string, uid, gid uint32) error { - fileFullPath := fh.FullPath() - // The caller's name may have been unlinked or renamed away since it - // resolved it; the file is reached through the name it has now. - if resolved := util.FullPath(dir).Child(name); fileFullPath != "" && fileFullPath != resolved { - if inode, found := wfs.inodeToPath.GetInode(resolved); !found || inode != fh.inode { - dir, name = fileFullPath.DirAndName() - } - } + fileFullPath, dir, name := wfs.flushTarget(fh, dir, name) // Attribute acknowledgement and local retention must complete before a // whole-entry flush can publish. Take this before the handle's active lock. unlock := wfs.lockMetadataPublication(fileFullPath) - defer unlock() + fhActiveLock := fh.wfs.fhLockTable.AcquireLock("doFlush", fh.fh, util.ExclusiveLock) + // An unlink or rename can land before the locks are held. + for attempt := 0; attempt < 3; attempt++ { + nowPath, nowDir, nowName := wfs.flushTarget(fh, dir, name) + if nowPath == fileFullPath && nowDir == dir && nowName == name { + break + } + fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock) + unlock() + fileFullPath, dir, name = nowPath, nowDir, nowName + unlock = wfs.lockMetadataPublication(fileFullPath) + fhActiveLock = fh.wfs.fhLockTable.AcquireLock("doFlush", fh.fh, util.ExclusiveLock) + } + defer func() { + fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock) + unlock() + }() glog.V(4).Infof("flushMetadataToFiler %s/%s inode %d fh %d", dir, name, fh.inode, fh.fh) - fhActiveLock := fh.wfs.fhLockTable.AcquireLock("doFlush", fh.fh, util.ExclusiveLock) - defer fh.wfs.fhLockTable.ReleaseLock(fh.fh, fhActiveLock) - // Re-check under the lock: Unlink sets the flag under it, so a flush that // was already past the earlier check cannot write the entry back after // the delete removed it. @@ -323,6 +329,19 @@ func (wfs *WFS) flushMetadataToFiler(ctx context.Context, fh *FileHandle, dir, n return err } +// flushTarget returns the lock path and the name a flush of fh publishes +// through: the caller's name while it still names the file, else the name the +// file has now. +func (wfs *WFS) flushTarget(fh *FileHandle, dir, name string) (util.FullPath, string, string) { + fileFullPath := fh.FullPath() + if resolved := util.FullPath(dir).Child(name); fileFullPath != "" && fileFullPath != resolved { + if inode, found := wfs.inodeToPath.GetInode(resolved); !found || inode != fh.inode { + dir, name = fileFullPath.DirAndName() + } + } + return fileFullPath, dir, name +} + // createContentEntry publishes a handle's whole entry only over the filer // state the handle observed. Never refresh the guard and replay a stale whole // entry after a conflict: that would discard a peer's acknowledged chunks.