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>
This commit is contained in:
Chris LuandClaude Opus 5.5 committed 2026-11-13 02:45:27 -08:00
1 parent 357a698425
commit 482b6a03ca
1 file changed
+31 -12
+31 -12
View File
@@ -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.