mount: refuse to merge a whole-file rewrite over a peer's chunk change

maybeMergeChunks re-uploads the handle's whole view as unstamped chunks.
Merged after a peer's change, they tie with a peer's own rewrite or bring
back a range the peer truncated, so that flush fails as before.

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:12:38 -08:00
1 parent 3fc45d9f83
commit 8499d787cd
2 files changed
+20 -4

No files matched your search

@@ -308,6 +308,15 @@ func TestMergeContentEntry(t *testing.T) {
t.Fatal("merged into a replaced file")
}
rewritten := proto.Clone(base).(*filer_pb.Entry)
rewritten.Chunks = []*filer_pb.FileChunk{{Fid: &filer_pb.FileId{VolumeId: 1, FileKey: 8, Cookie: 1}, Size: 2}}
if _, ok := mergeContentEntry(stored, base, rewritten, rewritten); ok {
t.Fatal("merged a stale whole-file rewrite over a peer append")
}
if _, ok := mergeContentEntry(proto.Clone(base).(*filer_pb.Entry), base, rewritten, rewritten); !ok {
t.Fatal("refused a rewrite with no peer chunk change")
}
empty := flushConditionEntry()
inline := proto.Clone(empty).(*filer_pb.Entry)
inline.Content = []byte("peer")
+11 -4
View File
@@ -373,9 +373,10 @@ const maxContentFlushMerges = 3
// content it changed, taking those values from sent (local in stored's id
// space). Everything else keeps the stored value. A local truncation keeps
// its size and clips stored chunks to it; otherwise the larger size wins.
// A different file at the path, inline content changed on both sides, or
// inline content next to chunks (reads stop at the inline bytes) cannot be
// merged.
// A different file at the path, inline content changed on both sides,
// inline content next to chunks (reads stop at the inline bytes), or a local
// whole-file rewrite (maybeMergeChunks, unstamped chunks) over a peer's chunk
// change cannot be merged: the rewrite carries the handle's stale view.
func mergeContentEntry(stored, base, local, sent *filer_pb.Entry) (*filer_pb.Entry, bool) {
if stored == nil || base == nil || stored.IsDirectory || !bytes.Equal(stored.HardLinkId, base.HardLinkId) {
return nil, false
@@ -400,14 +401,20 @@ func mergeContentEntry(stored, base, local, sent *filer_pb.Entry) (*filer_pb.Ent
for _, c := range local.Chunks {
inLocal[contentChunkKey(c)] = true
}
peerChanged := len(stored.Chunks) != len(base.Chunks)
merged.Chunks = merged.Chunks[:0]
for _, c := range stored.Chunks {
if key := contentChunkKey(c); !inBase[key] || inLocal[key] {
key := contentChunkKey(c)
peerChanged = peerChanged || !inBase[key]
if !inBase[key] || inLocal[key] {
merged.Chunks = append(merged.Chunks, proto.Clone(c).(*filer_pb.FileChunk))
}
}
for _, c := range local.Chunks {
if !inBase[contentChunkKey(c)] {
if peerChanged && c.ModifiedTsNs == 0 && !c.IsChunkManifest {
return nil, false
}
merged.Chunks = append(merged.Chunks, proto.Clone(c).(*filer_pb.FileChunk))
}
}