mount: keep an unpublished truncate across a peer's base change

The truncate mark was tied to the base pointer, so a peer event that moved
a dirty handle's base (a merged content change or a metadata-only change)
dropped it, and a later merge on a regrown file kept peer chunks inside
the truncated range. Those base moves now carry the mark; a flush ack or a
discarded handle state still clears it.

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:31:47 -08:00
1 parent b0cc4abb87
commit 71f4743950
3 files changed
+48 -2

No files matched your search

+9
View File
@@ -250,6 +250,15 @@ func (fh *FileHandle) noteTruncate(size uint64) {
fh.truncatedTo.Store(&truncateMark{base: base, size: int64(size)})
}
// adoptPeerBase moves a dirty handle's base to a peer's entry. A truncate the
// handle has not published yet still applies over the new base.
func (fh *FileHandle) adoptPeerBase(base *filer_pb.Entry) {
old := fh.baseEntry.Swap(base)
if m := fh.truncatedTo.Load(); m != nil && m.base == old {
fh.truncatedTo.Store(&truncateMark{base: base, size: m.size})
}
}
// truncatedSince is the lowest size truncated to over base, or -1.
func (fh *FileHandle) truncatedSince(base *filer_pb.Entry) int64 {
if m := fh.truncatedTo.Load(); m != nil && m.base == base {
+4 -2
View File
@@ -1052,7 +1052,7 @@ func (wfs *WFS) invalidateOpenFileHandle(invalidation meta_cache.EntryInvalidati
if contentChanged && fh.dirtyMetadata && sameContentFile(candidate, base) {
// An unmergeable change keeps the writes; the flush reports it.
if fh.mergePeerContent(candidate, base) {
fh.baseEntry.Store(proto.Clone(candidate).(*filer_pb.Entry))
fh.adoptPeerBase(proto.Clone(candidate).(*filer_pb.Entry))
fh.advanceEntryVersion(candidateTsNs, 0)
}
return
@@ -1063,8 +1063,10 @@ func (wfs *WFS) invalidateOpenFileHandle(invalidation meta_cache.EntryInvalidati
}
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.baseEntry.Store(proto.Clone(candidate).(*filer_pb.Entry))
fh.advanceEntryVersion(candidateTsNs, 0)
return
}
@@ -2159,3 +2159,38 @@ func TestUnmergeablePeerChangeKeepsDirtyHandle(t *testing.T) {
t.Fatalf("base %v moved to the peer's entry", base)
}
}
// A peer event that moves a dirty handle's base keeps its unpublished
// truncate, so a later merge still clips peer chunks at it.
func TestPeerEventKeepsPendingTruncate(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, 2), 0, 0)
fh.dirtyMetadata = true
fh.noteTruncate(1)
chmod := flushConditionEntry(1, 2)
chmod.Attributes.FileMode = 0600
resp := &filer_pb.SubscribeMetadataResponse{
Directory: "/dir",
TsNs: 1500,
EventNotification: &filer_pb.EventNotification{
OldEntry: flushConditionEntry(1, 2),
NewEntry: chmod,
NewParentPath: "/dir",
},
}
if err := wfs.metaCache.ApplyMetadataResponse(context.Background(), resp, meta_cache.SubscriberMetadataResponseApplyOptions); err != nil {
t.Fatalf("apply: %v", err)
}
wfs.metaCache.WaitForEntryInvalidations()
base := fh.baseEntry.Load()
if base.GetAttributes().GetFileMode() != 0600 {
t.Fatalf("base %v did not move to the peer's entry", base)
}
if got := fh.truncatedSince(base); got != 1 {
t.Fatalf("pending truncate %d after the base moved, want 1", got)
}
}