mount: keep a dirty handle's writes when a peer replaces the file

A peer entry with another inode at the handle's path went down the
fallback that destroys the dirty pages and installs the replacement, so
close returned OK without the writes. The handle now keeps its writes
and base like it does for an unmergeable change, and its flush refuses to
update the replacement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris LuandClaude Opus 5.5 committed 2026-11-12 17:36:24 -08:00
1 parent 2d719fb4ea
commit 0c8ecb3ef6
2 files changed
+42 -3

No files matched your search

+4 -3
View File
@@ -1049,9 +1049,10 @@ func (wfs *WFS) invalidateOpenFileHandle(invalidation meta_cache.EntryInvalidati
// against an approximate base) leaves them valid. A dirty handle likewise
// keeps its diverged entry unless foreign content supersedes it.
contentChanged := base == nil || !sameEntryContent(candidate, base)
if contentChanged && fh.dirtyMetadata && sameContentFile(candidate, base) {
// An unmergeable change keeps the writes; the flush reports it.
if fh.mergePeerContent(candidate, base) {
if contentChanged && fh.dirtyMetadata && base != nil {
// An unmergeable change or a replaced file keeps the writes; the
// flush reports the conflict.
if sameContentFile(candidate, base) && fh.mergePeerContent(candidate, base) {
fh.adoptPeerBase(proto.Clone(candidate).(*filer_pb.Entry))
fh.advanceEntryVersion(candidateTsNs, 0)
}
@@ -2231,3 +2231,41 @@ func TestPeerTruncateWaitsForBufferedWrites(t *testing.T) {
t.Fatalf("base %v moved to the peer's truncate", base)
}
}
// A peer replacing the file at the handle's path (another inode) leaves the
// dirty handle's writes and base alone instead of installing the newcomer.
func TestPeerReplacementKeepsDirtyHandle(t *testing.T) {
wfs := newInvalidateTestWFS(t)
inode := wfs.inodeToPath.Lookup(util.FullPath("/dir/file"), time.Now().Unix(), false, false, 0, false)
fh, _, _, _ := wfs.fhMap.AcquireFileHandle(wfs, inode, flushConditionEntry(1), 0, 0)
fh.AddChunks([]*filer_pb.FileChunk{{FileId: "1,0300000001", Offset: 2, Size: 1, ModifiedTsNs: 3}})
fh.dirtyMetadata = true
pagesBefore := fh.dirtyPages
replacement := flushConditionEntry(7)
replacement.Attributes.Inode = 99
resp := &filer_pb.SubscribeMetadataResponse{
Directory: "/dir",
TsNs: 1500,
EventNotification: &filer_pb.EventNotification{
OldEntry: flushConditionEntry(1),
NewEntry: replacement,
NewParentPath: "/dir",
},
}
if err := wfs.metaCache.ApplyMetadataResponse(context.Background(), resp, meta_cache.SubscriberMetadataResponseApplyOptions); err != nil {
t.Fatalf("apply: %v", err)
}
wfs.metaCache.WaitForEntryInvalidations()
if fh.dirtyPages != pagesBefore {
t.Fatal("dirty pages destroyed by a replacement")
}
if got := chunkFileKeys(fh.GetEntry().GetEntry()); len(got) != 2 || got[0] != 1 {
t.Fatalf("handle chunks %v, want its own", got)
}
if base := fh.baseEntry.Load(); base.GetAttributes().GetInode() != 42 {
t.Fatalf("base %v moved to the replacement", base)
}
}