mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-09 15:57:47 +02:00
* fix(mount): skip pressure-eviction of gappy page chunks (#9330) A page chunk whose written-interval list has an internal hole was being sealed under buffer-pressure eviction, then SaveContent would emit one volume chunk per maximal adjacent run with no chunk covering the hole; reads then silently zero-fill the gap (filer/stream.go:177-186). On a sequential cp through FUSE, that bakes in-flight 4 KiB writes into split volume chunks and leaves chunk-sized blocks of zeros on the destination. Filter the pressure-driven sealers (SaveDataAt's over-limit path, EvictOneWritableChunk, ProactiveFlush) to only seal chunks whose written intervals form one unbroken run. The flush-on-close path (FlushAll) is unchanged: at close every gap is by definition a sparse-file write that the app legitimately never made. * fix(mount): also gate IsContiguouslyWritten on leading zero-offset Tighten IsContiguouslyWritten to also reject empty lists and lists whose first interval does not start at offset 0. The internal-gap and leading-gap cases are symmetric for pressure-driven sealing: both put in-flight FUSE writeback for the missing range at risk of being baked into split volume chunks. The flush-on-close path is still unfiltered (sparse writes are sealed legitimately at FlushAll). Also align EvictOneWritableChunk's bestBytes initialization with SaveDataAt (start at 0) so an empty chunk is never picked, matching the new semantic. Addresses gemini-code-assist review on PR #9334. * fix(mount): preserve cap-pressure liveness in EvictOneWritableChunk The previous version of this fix had EvictOneWritableChunk return false whenever every dirty chunk was gappy. That broke the accountant's Reserve loop: cond.Wait only wakes on Release, Release only fires on upload completion, and refusing to seal anything means no upload starts — the writer hangs at the -writeBufferSizeMB cap forever. Two-pass selection: prefer the fullest gap-free chunk (issue #9330: this is what protects sequential cp from racing FUSE writeback), fall back to the oldest non-empty writer when nothing is gap-free. Oldest-first maximizes the chance that FUSE writeback for the gap range has already settled. The actual sealing path is unchanged — SaveContent still emits one volume chunk per maximal adjacent run; pages that arrive after the seal land in a fresh MemChunk for the same logicChunkIndex and are sealed in turn, so coverage is reconstructed at read time by readResolvedChunks. Sequential cp at default settings always hits the strict pass (writes arrive contiguous-from-0 within their logicChunkIndex), so the bug-fix behavior is preserved; the fallback only runs under genuinely sparse workloads or under FUSE writeback so backed up that no chunk has settled, where forced progress is preferable to a hung mount. * test(mount): pin ProactiveFlush gap-skip behavior (#9330) Sibling regression test for the ProactiveFlush guard added in this series: same 3-chunk setup as TestEvictOneWritableChunk_SkipsGappyChunks (internal gap, leading gap, contiguous). Verifies ProactiveFlush picks the contiguous chunk when staleness criteria are otherwise satisfied, returns false when only gappy chunks remain (no liveness fallback like EvictOneWritableChunk has — failing here is just a missed optimization), and that filling the holes lets the chunks auto-seal via maybeMoveToSealed. * style(mount): trim verbose comments on #9330 fix
261 lines
8.4 KiB
Go
261 lines
8.4 KiB
Go
package page_writer
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func TestUploadPipeline(t *testing.T) {
|
|
|
|
uploadPipeline := NewUploadPipeline(nil, 2*1024*1024, nil, 16, "", nil)
|
|
|
|
writeRange(uploadPipeline, 0, 131072)
|
|
writeRange(uploadPipeline, 131072, 262144)
|
|
writeRange(uploadPipeline, 262144, 1025536)
|
|
|
|
confirmRange(t, uploadPipeline, 0, 1025536)
|
|
|
|
writeRange(uploadPipeline, 1025536, 1296896)
|
|
|
|
confirmRange(t, uploadPipeline, 1025536, 1296896)
|
|
|
|
writeRange(uploadPipeline, 1296896, 2162688)
|
|
|
|
confirmRange(t, uploadPipeline, 1296896, 2162688)
|
|
|
|
confirmRange(t, uploadPipeline, 1296896, 2162688)
|
|
}
|
|
|
|
// startOff and stopOff must be divided by 4
|
|
func writeRange(uploadPipeline *UploadPipeline, startOff, stopOff int64) {
|
|
p := make([]byte, 4)
|
|
for i := startOff / 4; i < stopOff/4; i += 4 {
|
|
util.Uint32toBytes(p, uint32(i))
|
|
uploadPipeline.SaveDataAt(p, i, false, 0)
|
|
}
|
|
}
|
|
|
|
func confirmRange(t *testing.T, uploadPipeline *UploadPipeline, startOff, stopOff int64) {
|
|
p := make([]byte, 4)
|
|
for i := startOff; i < stopOff/4; i += 4 {
|
|
uploadPipeline.MaybeReadDataAt(p, i, 0)
|
|
x := util.BytesToUint32(p)
|
|
if x != uint32(i) {
|
|
t.Errorf("expecting %d found %d at offset [%d,%d)", i, x, i, i+4)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pressure-driven eviction must not seal a chunk with a leading or
|
|
// internal gap (issue #9330).
|
|
func TestEvictOneWritableChunk_SkipsGappyChunks(t *testing.T) {
|
|
const cs int64 = 2 * 1024 * 1024
|
|
// nil saveToStorage so the async upload SaveContent is a no-op.
|
|
up := NewUploadPipeline(util.NewLimitedConcurrentExecutor(2), cs, nil, 16, "", nil)
|
|
|
|
block := make([]byte, cs/4)
|
|
|
|
// chunk 0: internal gap; chunk 1: leading gap; chunk 2: contiguous from 0.
|
|
if _, err := up.SaveDataAt(block, 0, true, 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, 3*cs/4, true, 2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs+cs/4, true, 3); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs+cs/2, true, 4); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, 2*cs, true, 5); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, 2*cs+cs/4, true, 6); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !up.EvictOneWritableChunk() {
|
|
t.Fatalf("EvictOneWritableChunk returned false; expected contiguous chunk 2 to be evictable")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(2)]; stillWritable {
|
|
t.Errorf("chunk 2 should have moved to sealed")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; !stillWritable {
|
|
t.Errorf("chunk 0 (internal gap) must remain writable")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; !stillWritable {
|
|
t.Errorf("chunk 1 (leading gap) must remain writable")
|
|
}
|
|
|
|
// Filling holes makes IsComplete true; maybeMoveToSealed auto-seals.
|
|
if _, err := up.SaveDataAt(block, cs/4, true, 7); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs/2, true, 8); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs, true, 9); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs+3*cs/4, true, 10); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; stillWritable {
|
|
t.Errorf("chunk 0 should have auto-sealed after gap was filled")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; stillWritable {
|
|
t.Errorf("chunk 1 should have auto-sealed after leading range was filled")
|
|
}
|
|
}
|
|
|
|
// When every chunk is gappy, the fallback must still seal one so
|
|
// accountant.Reserve can wake; oldest-LastWriteTsNs wins.
|
|
func TestEvictOneWritableChunk_FallbackPicksOldestGappy(t *testing.T) {
|
|
const cs int64 = 2 * 1024 * 1024
|
|
up := NewUploadPipeline(util.NewLimitedConcurrentExecutor(2), cs, nil, 16, "", nil)
|
|
|
|
block := make([]byte, cs/4)
|
|
|
|
// Strictly increasing tsNs so chunk 0 is oldest; equal WrittenSize.
|
|
if _, err := up.SaveDataAt(block, 0, true, 100); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, 3*cs/4, true, 101); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs+cs/4, true, 200); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs+cs/2, true, 201); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, 2*cs, true, 300); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, 2*cs+3*cs/4, true, 301); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !up.EvictOneWritableChunk() {
|
|
t.Fatalf("fallback must seal a gappy chunk to free a Reserve slot")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; stillWritable {
|
|
t.Errorf("oldest gappy chunk (0) should have been picked by the fallback")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; !stillWritable {
|
|
t.Errorf("chunk 1 should remain writable")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(2)]; !stillWritable {
|
|
t.Errorf("chunk 2 should remain writable")
|
|
}
|
|
}
|
|
|
|
// Strict pass preempts the fallback even when a gappy chunk is older.
|
|
func TestEvictOneWritableChunk_PrefersStrictOverFallback(t *testing.T) {
|
|
const cs int64 = 2 * 1024 * 1024
|
|
up := NewUploadPipeline(util.NewLimitedConcurrentExecutor(2), cs, nil, 16, "", nil)
|
|
|
|
block := make([]byte, cs/4)
|
|
|
|
// chunk 0 (older, gappy), chunk 1 (newer, contiguous from 0).
|
|
if _, err := up.SaveDataAt(block, 0, true, 100); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, 3*cs/4, true, 101); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs, true, 200); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs+cs/4, true, 201); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !up.EvictOneWritableChunk() {
|
|
t.Fatalf("EvictOneWritableChunk returned false")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; stillWritable {
|
|
t.Errorf("contiguous chunk 1 must be picked over older gappy chunk 0")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; !stillWritable {
|
|
t.Errorf("gappy chunk 0 must remain writable")
|
|
}
|
|
}
|
|
|
|
// ProactiveFlush also skips gappy chunks (issue #9330). No liveness
|
|
// fallback here — unlike EvictOneWritableChunk, returning false is just
|
|
// a missed optimization.
|
|
func TestProactiveFlush_SkipsGappyChunks(t *testing.T) {
|
|
const cs int64 = 2 * 1024 * 1024
|
|
up := NewUploadPipeline(util.NewLimitedConcurrentExecutor(2), cs, nil, 16, "", nil)
|
|
|
|
block := make([]byte, cs/4)
|
|
|
|
// chunk 0: internal gap; chunk 1: leading gap; chunk 2: contiguous from 0.
|
|
if _, err := up.SaveDataAt(block, 0, true, 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, 3*cs/4, true, 2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs+cs/4, true, 3); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs+cs/2, true, 4); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, 2*cs, true, 5); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, 2*cs+cs/4, true, 6); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// nowNs >> chunk timestamps so age clears the idle/maxHold gates;
|
|
// fillRatio < WrittenSize so nearlyFull is true. Leaves
|
|
// IsContiguouslyWritten as the only differentiator.
|
|
const (
|
|
nowNs int64 = 1_000_000_000
|
|
idleThresholdNs int64 = 100
|
|
maxHoldNs int64 = 200
|
|
fillRatio int64 = cs / 8
|
|
)
|
|
if !up.ProactiveFlush(nowNs, idleThresholdNs, maxHoldNs, fillRatio, 0, false) {
|
|
t.Fatalf("ProactiveFlush returned false; expected contiguous chunk 2 to be sealed")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(2)]; stillWritable {
|
|
t.Errorf("chunk 2 should have moved to sealed")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; !stillWritable {
|
|
t.Errorf("chunk 0 (internal gap) must remain writable")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; !stillWritable {
|
|
t.Errorf("chunk 1 (leading gap) must remain writable")
|
|
}
|
|
|
|
if up.ProactiveFlush(nowNs, idleThresholdNs, maxHoldNs, fillRatio, 0, false) {
|
|
t.Errorf("ProactiveFlush returned true with only gappy chunks left")
|
|
}
|
|
|
|
if _, err := up.SaveDataAt(block, cs/4, true, 7); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs/2, true, 8); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs, true, 9); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := up.SaveDataAt(block, cs+3*cs/4, true, 10); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(0)]; stillWritable {
|
|
t.Errorf("chunk 0 should have auto-sealed after gap was filled")
|
|
}
|
|
if _, stillWritable := up.writableChunks[LogicChunkIndex(1)]; stillWritable {
|
|
t.Errorf("chunk 1 should have auto-sealed after leading range was filled")
|
|
}
|
|
}
|