mount: keep an open file publishing when a peer unlinks one of its names

A delete event from another client marked the inode's open handle deleted
even when other names still held the hard-linked file, so every later
fsync reported success without saving metadata. The delete event's entry
carries the link count the filer decided on, so an alias delete now drops
only that name, keeps the handle on a name this mount knows, and has the
next flush read the survivor's stored entry before publishing.

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:51:33 -08:00
1 parent 70d6639155
commit ee1de6d533
2 files changed
+32 -1

No files matched your search

+8 -1
View File
@@ -937,6 +937,9 @@ type EntryInvalidation struct {
// once the path is empty, so this is the only thing left saying whether a
// directory or a file went away.
WasDirectory bool
// LiveHardLinkId is set on a delete that left other names holding the
// hard-linked file, by the count the filer kept its chunks on.
LiveHardLinkId []byte
}
type metadataResponseSideEffects struct {
@@ -1440,7 +1443,11 @@ func collectEntryInvalidations(resp *filer_pb.SubscribeMetadataResponse) []Entry
if filer_pb.IsDelete(resp) && message.OldEntry != nil {
oldKey := util.NewFullPath(resp.Directory, message.OldEntry.Name)
invalidations = append(invalidations, EntryInvalidation{Path: oldKey, TsNs: resp.TsNs, Deleted: true, Signatures: signatures, WasDirectory: message.OldEntry.IsDirectory})
invalidation := EntryInvalidation{Path: oldKey, TsNs: resp.TsNs, Deleted: true, Signatures: signatures, WasDirectory: message.OldEntry.IsDirectory}
if len(message.OldEntry.HardLinkId) > 0 && message.OldEntry.HardLinkCounter > 1 {
invalidation.LiveHardLinkId = message.OldEntry.HardLinkId
}
invalidations = append(invalidations, invalidation)
}
return invalidations
+24
View File
@@ -1023,6 +1023,12 @@ func (wfs *WFS) invalidateOpenFileHandle(invalidation meta_cache.EntryInvalidati
// so no flush recreates the unlinked name. Either way the entry and
// dirty pages stay, so the open fd still reads its buffered writes.
if invalidation.Deleted {
// The next flush reads the survivor's stored entry before
// publishing, so the version stays where it was.
if len(invalidation.LiveHardLinkId) > 0 && wfs.followSurvivingLink(fh, inode, filePath) {
fh.linkRefresh = invalidation.LiveHardLinkId
return
}
fh.isDeleted = true
fh.deleteEpoch++
}
@@ -1061,6 +1067,24 @@ func (wfs *WFS) invalidateOpenFileHandle(invalidation meta_cache.EntryInvalidati
return
}
// followSurvivingLink drops a hard link name another client deleted and
// keeps the handle on a name of the inode this mount still knows, reporting
// false when it knows none.
func (wfs *WFS) followSurvivingLink(fh *FileHandle, inode uint64, removed util.FullPath) bool {
survives := false
for _, p := range wfs.inodeToPath.GetAllPaths(inode) {
survives = survives || p != removed
}
if !survives {
return false
}
wfs.inodeToPath.RemovePath(removed, nil)
if survivor, status := wfs.inodeToPath.GetPath(inode); status == fuse.OK {
fh.RememberPath(survivor)
}
return true
}
func (wfs *WFS) LookupFn() wdclient.LookupFileIdFunctionType {
if wfs.option.VolumeServerAccess == "filerProxy" {
return func(ctx context.Context, fileId string) (targetUrls []string, err error) {