mount: keep a peer's truncate when the handle left the size alone

The merge took the larger of the handle's and the stored size, so a peer
truncate came back as a zero-filled tail when the handle only overwrote
bytes inside it. The stored size now stands unless the handle changed
its size or wrote past it, and rebasing keeps only the growth writes made
after the flush was taken.

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:19:01 -08:00
1 parent f7be2a75f5
commit 1f2ed6923f
4 files changed
+54 -12

No files matched your search

@@ -7,6 +7,7 @@ import (
"slices"
"sync"
"testing"
"time"
"github.com/seaweedfs/go-fuse/v2/fuse"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
@@ -315,6 +316,19 @@ func TestMergeContentEntry(t *testing.T) {
t.Fatalf("truncate then write kept %v, size %d", got, merged.Attributes.FileSize)
}
// A peer truncate stands when the handle only overwrote inside it.
peerTruncated := proto.Clone(base).(*filer_pb.Entry)
peerTruncated.Chunks, peerTruncated.Attributes.FileSize = peerTruncated.Chunks[:1], 1
overwrote := proto.Clone(base).(*filer_pb.Entry)
overwrote.Chunks = append(overwrote.Chunks, &filer_pb.FileChunk{Fid: &filer_pb.FileId{VolumeId: 1, FileKey: 6, Cookie: 1}, Size: 1, ModifiedTsNs: 6})
merged, ok = mergeContentEntry(peerTruncated, base, overwrote, overwrote, -1)
if !ok {
t.Fatal("overwrite over a peer truncate not merged")
}
if got := chunkFileKeys(merged); !slices.Equal(got, []uint64{1, 6}) || merged.Attributes.FileSize != 1 {
t.Fatalf("overwrite over a peer truncate kept %v, size %d", got, merged.Attributes.FileSize)
}
stored.Attributes.Inode = 99
if _, ok := mergeContentEntry(stored, base, local, local, -1); ok {
t.Fatal("merged into a replaced file")
@@ -382,3 +396,25 @@ func TestContentFlushRepublishesUnchangedEntry(t *testing.T) {
})
}
}
// Rebasing keeps a size the handle reached after the flush was taken, not the
// size it sent: a peer's truncate merged in must stand.
func TestRebaseEntryKeepsMergedSize(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)
sent := proto.Clone(fh.GetEntry().GetEntry()).(*filer_pb.Entry)
merged := flushConditionEntry(1)
merged.Attributes.FileSize = 1
fh.rebaseEntry(merged, sent)
if size := fh.GetEntry().GetEntry().Attributes.FileSize; size != 1 {
t.Fatalf("size %d after rebasing onto a truncate, want 1", size)
}
fh.GetEntry().GetEntry().Attributes.FileSize = 5
fh.rebaseEntry(merged, sent)
if size := fh.GetEntry().GetEntry().Attributes.FileSize; size != 5 {
t.Fatalf("size %d after a write grew the handle, want 5", size)
}
}
+7 -6
View File
@@ -179,10 +179,11 @@ func (fh *FileHandle) installAckedEntry(entry *filer_pb.Entry, versionTsNs int64
}
// rebaseEntry installs an entry merged with a peer's changes, keeping the
// chunks uploaders attached after sent was taken, and refreshes the read view.
func (fh *FileHandle) rebaseEntry(merged *filer_pb.Entry, sent []*filer_pb.FileChunk) {
inSent := make(map[string]bool, len(sent))
for _, c := range sent {
// chunks and growth writers added after sent was taken, and refreshes the
// read view.
func (fh *FileHandle) rebaseEntry(merged, sent *filer_pb.Entry) {
inSent := make(map[string]bool, len(sent.Chunks))
for _, c := range sent.Chunks {
inSent[contentChunkKey(c)] = true
}
merged = proto.Clone(merged).(*filer_pb.Entry)
@@ -194,7 +195,7 @@ func (fh *FileHandle) rebaseEntry(merged *filer_pb.Entry, sent []*filer_pb.FileC
}
}
attrs := merged.Attributes
if size := e.GetAttributes().GetFileSize(); size > attrs.FileSize {
if size := e.GetAttributes().GetFileSize(); size > sent.GetAttributes().GetFileSize() && size > attrs.FileSize {
attrs.FileSize = size
}
e.Attributes, e.Extended, e.Content, e.Chunks = attrs, merged.Extended, merged.Content, chunks
@@ -211,7 +212,7 @@ func (fh *FileHandle) mergePeerContent(peer, base *filer_pb.Entry) bool {
})
merged, ok := mergeContentEntry(peer, base, local, local, fh.truncatedSince(base))
if ok {
fh.rebaseEntry(merged, local.Chunks)
fh.rebaseEntry(merged, local)
}
return ok
}
+10 -5
View File
@@ -287,7 +287,7 @@ func (wfs *WFS) flushMetadataToFiler(ctx context.Context, fh *FileHandle, dir, n
return fmt.Errorf("fh flush create %s: %w", fileFullPath, err)
}
if merged != nil {
fh.rebaseEntry(merged, requestEntry.Chunks)
fh.rebaseEntry(merged, requestEntry)
baseSnapshot = merged
}
@@ -373,7 +373,8 @@ 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
// the handle's size and clips stored chunks at the lowest size truncated to
// (truncatedTo, or -1); otherwise the larger size wins.
// (truncatedTo, or -1); otherwise the stored size stands unless the handle
// changed its size or wrote past it.
// 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
@@ -420,12 +421,17 @@ func mergeContentEntry(stored, base, local, sent *filer_pb.Entry, truncatedTo in
}
merged.Chunks = append(merged.Chunks, c)
}
size := int64(filer.FileSize(stored))
if localSize != filer.FileSize(base) {
size = max(size, int64(localSize))
}
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))
size = max(size, c.Offset+int64(c.Size))
}
}
if len(merged.Content) > 0 && len(merged.Chunks) > 0 {
@@ -472,10 +478,9 @@ func mergeContentEntry(stored, base, local, sent *filer_pb.Entry, truncatedTo in
}
if truncatedTo >= 0 {
merged.Attributes.FileSize = localSize
} else {
merged.Attributes.FileSize = uint64(max(int64(localSize), int64(filer.FileSize(stored))))
size = int64(localSize)
}
merged.Attributes.FileSize = uint64(size)
return merged, true
}
+1 -1
View File
@@ -184,7 +184,7 @@ func (wfs *WFS) flushFileMetadata(fh *FileHandle) error {
return err
}
if merged != nil {
fh.rebaseEntry(merged, requestEntry.Chunks)
fh.rebaseEntry(merged, requestEntry)
baseSnapshot = merged
}