Files
seaweedfs/weed/mount/weedfs_file_write_test.go
T
4f70ad015e mount: fail the write when the swap file chunk cannot be created (#12655)
* mount: fail the write when the swap file chunk cannot be created

#7957 added a nil check after NewSwapFileChunk so a failed swap file
creation returns an error instead of panicking. The result was assigned
to pageChunk, a PageChunk interface, before the check, and an interface
holding a nil *SwapFileChunk is not nil, so the check never fired:
SaveDataAt stored the nil chunk and panicked in WriteDataAt.

Check the *SwapFileChunk before converting it. The new test points the
swap directory below a regular file; on master it panics in
SwapFileChunk.WriteDataAt.

Signed-off-by: anant <170962632+antcybersec@users.noreply.github.com>

* mount: grow the file only after its write is buffered

Write set the new size, times, pending append offset and uncommitted
bytes before AddPage. When AddPage failed (now that a swap file chunk
that cannot be created returns an error), the write was reported as
failed but the larger size stayed on the handle's entry, so the next
flush published it with a hole where the data should be.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: report a partly buffered write as a short write

PageWriter.AddPage fills one chunk at a time, so when a later chunk
cannot be created the earlier chunks already hold their bytes. Return
how much was buffered, and let Write grow the size and times over that
prefix and report it, instead of failing the whole write while the
prefix still reaches the next flush.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: report the bytes a failed copy_file_range already buffered

A streaming copy whose page fails partway returned an error even though
earlier pages, or the start of the failing one, were already buffered and
would reach the next flush with the destination size left short. Count
the buffered bytes, grow the destination over them, and return them as a
short copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: free the copy test's destination pages

The buffered memory chunk counts toward the process-wide memory chunk
ceiling, so repeated runs of the test pushed later ones onto the
unusable swap directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: count only the bytes a swap chunk stored

A swap chunk whose file write fails stores a prefix or nothing, but
SaveDataAt returned no error and AddPage counted the whole span as
buffered. SaveDataAt now reports the short write, and AddPage passes the
stored count up so a write is never acknowledged past it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* mount: fail a partly buffered writeback write

Kernel writeback ignores the count in a WRITE reply, so a short write
there drops the unbuffered rest without an error. A write flagged as
page-cache writeback now keeps its buffered prefix but returns the
error, which the kernel reports on fsync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Signed-off-by: anant <170962632+antcybersec@users.noreply.github.com>
Co-authored-by: anant <170962632+antcybersec@users.noreply.github.com>
Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 19:34:43 -08:00

96 lines
3.8 KiB
Go

//go:build !freebsd
package mount
import (
"bytes"
"context"
"os"
"path/filepath"
"syscall"
"testing"
"github.com/seaweedfs/go-fuse/v2/fuse"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
"google.golang.org/protobuf/proto"
)
// A write the swap file cannot buffer fails, and must not grow the file: the
// next flush would publish that size with a hole where its data should be. A
// write that buffers only its first chunk is a short write of those bytes, or
// an error for kernel writeback, which would drop the rest silently.
func TestFailedWriteDoesNotGrowFile(t *testing.T) {
t.Run("short write", func(t *testing.T) { testFailedWriteDoesNotGrowFile(t, 0) })
t.Run("kernel writeback", func(t *testing.T) { testFailedWriteDoesNotGrowFile(t, fuse.WRITE_CACHE) })
}
func testFailedWriteDoesNotGrowFile(t *testing.T, writeFlags uint32) {
base := attributeAppendEntry(1)
base.CommitTsNs = 100
peer := &g6ConditionalPeer{entry: proto.Clone(base).(*filer_pb.Entry)}
peer.blobs = map[string][]byte{base.Chunks[0].GetFileIdString(): bytes.Repeat([]byte{'O'}, 256)}
w := newInvalidateTestWFS(t)
notADir := filepath.Join(t.TempDir(), "file")
if err := os.WriteFile(notADir, nil, 0600); err != nil {
t.Fatal(err)
}
w.option.uniqueCacheDirForWrite = filepath.Join(notADir, "swap")
w.option.DefaultPermissions = true
w.posixLocks = NewPosixLockTable()
w.option.ConcurrentWriters = 1
w.concurrentWriters = util.NewLimitedConcurrentExecutor(1)
startFakeFiler(t, w, peer)
w.inodeToPath.Lookup("/dir", 1, true, false, 0, false)
w.inodeToPath.Lookup("/dir/file", 1, false, false, 42, true)
w.inodeToPath.MarkChildrenCached("/dir")
if err := w.metaCache.InsertEntry(context.Background(), filer.FromPbEntry("/dir", proto.Clone(base).(*filer_pb.Entry)), 1); err != nil {
t.Fatal(err)
}
out := &fuse.OpenOut{}
if code := w.Open(nil, &fuse.OpenIn{InHeader: fuse.InHeader{NodeId: 42}, Flags: syscall.O_WRONLY}, out); code != fuse.OK {
t.Fatal(code)
}
fh := w.GetHandle(FileHandleId(out.Fh))
g6AppendUploader(t, fh, 100, peer)
write := func(offset uint64, size int, value byte) (uint32, fuse.Status) {
return w.Write(nil, &fuse.WriteIn{InHeader: fuse.InHeader{NodeId: 42}, Fh: out.Fh, Offset: offset, Size: uint32(size), WriteFlags: writeFlags}, bytes.Repeat([]byte{value}, size))
}
if _, code := write(256, 256, 'A'); code != fuse.OK {
t.Fatalf("buffered write: %v", code)
}
// The only writable chunk slot is taken, so this write needs the swap file.
if _, code := write(4096, 256, 'B'); code == fuse.OK {
t.Fatal("write through an unusable swap directory succeeded")
}
if size := fh.GetEntry().GetEntry().Attributes.FileSize; size != 512 {
t.Fatalf("file size after the failed write = %d, want 512", size)
}
// The first 24 bytes fit the existing chunk; the next chunk needs the swap file.
n, code := write(1000, 48, 'C')
if writeFlags == 0 && (code != fuse.OK || n != 24) {
t.Fatalf("boundary-crossing write = %d, %v; want a short write of 24", n, code)
}
if writeFlags != 0 && code == fuse.OK {
t.Fatalf("boundary-crossing writeback = %d, OK; want an error", n)
}
if size := fh.GetEntry().GetEntry().Attributes.FileSize; size != 1024 {
t.Fatalf("file size after the short write = %d, want 1024", size)
}
if code := w.Fsync(nil, &fuse.FsyncIn{InHeader: fuse.InHeader{NodeId: 42}, Fh: out.Fh}); code != fuse.OK {
t.Fatal(code)
}
peer.mu.Lock()
defer peer.mu.Unlock()
want := append(bytes.Repeat([]byte{'O'}, 256), bytes.Repeat([]byte{'A'}, 256)...)
want = append(want, make([]byte, 1000-512)...)
want = append(want, bytes.Repeat([]byte{'C'}, 24)...)
if got := g6StoredBytes(t, peer); peer.entry.Attributes.FileSize != 1024 || !bytes.Equal(got, want) {
t.Fatalf("stored size %d, want 1024 with the original and buffered bytes", peer.entry.Attributes.FileSize)
}
}