diff --git a/weed/mount/weedfs.go b/weed/mount/weedfs.go index 14d0424fe5..9a85bf8476 100644 --- a/weed/mount/weedfs.go +++ b/weed/mount/weedfs.go @@ -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 } diff --git a/weed/mount/weedfs_invalidate_open_handle_test.go b/weed/mount/weedfs_invalidate_open_handle_test.go index 03a2daec55..a7cfefdf36 100644 --- a/weed/mount/weedfs_invalidate_open_handle_test.go +++ b/weed/mount/weedfs_invalidate_open_handle_test.go @@ -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) + } + }) + } +}