mount: bring a peer's metadata change into a dirty handle

A metadata-only event moved a dirty handle's base but left its live
entry, so the next flush read the peer's chmod as a local edit and
published the old mode back. Replay the handle's own changes onto the
peer's entry instead, as for a content change.

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:58:31 -08:00
1 parent afa813ff88
commit 659493830b
2 files changed
+60 -16

No files matched your search

+11 -16
View File
@@ -1044,30 +1044,25 @@ func (wfs *WFS) invalidateOpenFileHandle(invalidation meta_cache.EntryInvalidati
fh.advanceEntryVersion(candidateTsNs, 0)
return
}
// Dirty pages overlay content, so only a content change invalidates them:
// a metadata-only event (chmod, touch, or a committed copy's own event
// 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 fh.dirtyMetadata && base != nil && (contentChanged || !sameContentFile(candidate, base)) {
// An unmergeable change or a replaced file keeps the writes; the
// flush reports the conflict.
if contentChanged && sameContentFile(candidate, base) && fh.mergePeerContent(candidate, base) {
// A dirty handle replays its own changes onto the peer's entry, keeping
// its pages. An unmergeable change or a replaced file keeps the writes;
// the flush reports the conflict.
if fh.dirtyMetadata && base != nil {
if sameContentFile(candidate, base) && fh.mergePeerContent(candidate, base) {
fh.adoptPeerBase(proto.Clone(candidate).(*filer_pb.Entry))
fh.advanceEntryVersion(candidateTsNs, 0)
}
return
}
if contentChanged {
// Dirty pages overlay content, so only a content change invalidates them:
// a metadata-only event (chmod, touch, or a committed copy's own event
// against an approximate base) leaves them valid.
if base == nil || !sameEntryContent(candidate, base) {
fh.dirtyPages.Destroy()
fh.dirtyPages = newPageWriter(fh, wfs.option.ChunkSizeLimit)
}
if contentChanged || !fh.dirtyMetadata {
fh.SetEntry(candidate)
fh.baseEntry.Store(proto.Clone(candidate).(*filer_pb.Entry))
} else {
fh.adoptPeerBase(proto.Clone(candidate).(*filer_pb.Entry))
}
fh.SetEntry(candidate)
fh.baseEntry.Store(proto.Clone(candidate).(*filer_pb.Entry))
fh.advanceEntryVersion(candidateTsNs, 0)
return
}
@@ -2276,3 +2276,52 @@ func TestPeerReplacementKeepsDirtyHandle(t *testing.T) {
})
}
}
// A peer's metadata-only change reaches a dirty handle's live entry, so its
// next flush does not publish the stale value back; the handle's own edits
// stay on top.
func TestPeerChmodReachesDirtyHandle(t *testing.T) {
for _, name := range []string{"peer-only", "both-changed"} {
t.Run(name, func(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}})
want := uint32(0600)
if name == "both-changed" {
fh.UpdateEntry(func(e *filer_pb.Entry) { e.Attributes.FileMode = 0640 })
want = 0640
}
fh.dirtyMetadata = true
pagesBefore := fh.dirtyPages
peer := flushConditionEntry(1)
peer.Attributes.FileMode = 0600
resp := &filer_pb.SubscribeMetadataResponse{
Directory: "/dir",
TsNs: 1500,
EventNotification: &filer_pb.EventNotification{
OldEntry: flushConditionEntry(1),
NewEntry: peer,
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 peer chmod")
}
entry := fh.GetEntry().GetEntry()
if got := entry.Attributes.FileMode; got != want || len(entry.Chunks) != 2 {
t.Fatalf("handle mode %o with %d chunks, want %o with its own chunk", got, len(entry.Chunks), want)
}
if base := fh.baseEntry.Load(); base.GetAttributes().GetFileMode() != 0600 {
t.Fatalf("base %v, want the peer's entry", base)
}
})
}
}