mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-08 13:07:45 +01:00
feat: Phase 3 — performance tuning, iSCSI session refactor, store integration
Phase 3 delivers five checkpoints: CP1 Engine Tuning: BlockVolConfig tunables, 256-shard DirtyMap, adaptive group commit (low-watermark immediate flush), WAL pressure handling with backpressure and ErrWALFull timeout. CP2 iSCSI Session Refactor: RX/TX goroutine split with respCh (cap 64), txLoop for serialized response writes, StatSN assignment modes. Login phase stays single-goroutine; full-duplex after login. CP3 Store Integration: BlockVolAdapter (iscsi.BlockDevice interface), BlockVolumeStore management, BlockService in volume_server_block.go, CLI flags (--block.listen/dir/iqn.prefix), sw-block-attach.sh helper. CP5 Concurrency Hardening: WAL reuse guard (LSN validation in ReadLBA), opsOutstanding counter with beginOp/endOp + Close drain, appendWithRetry shared by WriteLBA and TrimLBA, flusher LSN guard in FlushOnce. Bug fixes (P3-BUG-2–11): unbounded pending queue cap, Data-Out timeout, flusher error logging, GroupCommitter panic recovery, Close vs concurrent ops guard, target shutdown race, WAL-full retry vs Close, WRITE SAME(16) for XFS, MODE SENSE(10) + VPD 0xB0/0xB2 for Linux kernel compatibility. 797 tests passing (517 engine + 280 iSCSI), go vet clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
9b7be60b0c
commit
80801b0fac
37 files changed
+6569
-461
No files matched your search
@@ -21,6 +21,15 @@ func TestGroupCommitter(t *testing.T) {
|
||||
{name: "group_sequential", run: testGroupSequential},
|
||||
{name: "group_shutdown", run: testGroupShutdown},
|
||||
{name: "group_submit_after_stop", run: testGroupSubmitAfterStop},
|
||||
// Phase 3 Task 1.4: Adaptive group commit.
|
||||
{name: "adaptive_single_immediate", run: testAdaptiveSingleImmediate},
|
||||
{name: "adaptive_batch_above_watermark", run: testAdaptiveBatchAboveWatermark},
|
||||
{name: "adaptive_max_batch_triggers_early", run: testAdaptiveMaxBatchTriggersEarly},
|
||||
{name: "adaptive_watermark_zero_compat", run: testAdaptiveWatermarkZeroCompat},
|
||||
{name: "adaptive_threshold_boundary", run: testAdaptiveThresholdBoundary},
|
||||
{name: "adaptive_ramp_up", run: testAdaptiveRampUp},
|
||||
{name: "adaptive_max_delay_honored", run: testAdaptiveMaxDelayHonored},
|
||||
{name: "adaptive_fsync_error_all_waiters", run: testAdaptiveFsyncErrorAllWaiters},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -262,6 +271,294 @@ func testGroupShutdown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Phase 3 Task 1.4: Adaptive group commit tests ---
|
||||
|
||||
func testAdaptiveSingleImmediate(t *testing.T) {
|
||||
// With lowWatermark=4, a single Submit should flush immediately (no delay).
|
||||
var syncCalls atomic.Uint64
|
||||
gc := NewGroupCommitter(GroupCommitterConfig{
|
||||
SyncFunc: func() error {
|
||||
syncCalls.Add(1)
|
||||
return nil
|
||||
},
|
||||
MaxDelay: 5 * time.Second, // very long delay
|
||||
LowWatermark: 4,
|
||||
})
|
||||
go gc.Run()
|
||||
defer gc.Stop()
|
||||
|
||||
start := time.Now()
|
||||
if err := gc.Submit(); err != nil {
|
||||
t.Fatalf("Submit: %v", err)
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if syncCalls.Load() != 1 {
|
||||
t.Errorf("syncCalls = %d, want 1", syncCalls.Load())
|
||||
}
|
||||
// Should complete much faster than 5s maxDelay.
|
||||
if elapsed > 500*time.Millisecond {
|
||||
t.Errorf("Submit took %v, expected immediate (low watermark skip)", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func testAdaptiveBatchAboveWatermark(t *testing.T) {
|
||||
// With lowWatermark=4 and a slow sync, 10 concurrent submits should batch.
|
||||
var syncCalls atomic.Uint64
|
||||
gc := NewGroupCommitter(GroupCommitterConfig{
|
||||
SyncFunc: func() error {
|
||||
syncCalls.Add(1)
|
||||
time.Sleep(5 * time.Millisecond) // slow sync so requests queue up
|
||||
return nil
|
||||
},
|
||||
MaxDelay: 50 * time.Millisecond,
|
||||
MaxBatch: 64,
|
||||
LowWatermark: 4,
|
||||
})
|
||||
go gc.Run()
|
||||
defer gc.Stop()
|
||||
|
||||
const n = 10
|
||||
var wg sync.WaitGroup
|
||||
// Pre-enqueue all requests, then start Run.
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
gc.Submit()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// With slow sync, many should batch. At most n fsyncs (no batching), at least 1.
|
||||
c := syncCalls.Load()
|
||||
if c == 0 {
|
||||
t.Error("syncCalls = 0, want > 0")
|
||||
}
|
||||
t.Logf("adaptive batch: %d submits, %d fsyncs", n, c)
|
||||
}
|
||||
|
||||
func testAdaptiveMaxBatchTriggersEarly(t *testing.T) {
|
||||
// maxBatch should still trigger immediate flush even with adaptive watermark.
|
||||
gc := NewGroupCommitter(GroupCommitterConfig{
|
||||
SyncFunc: func() error { return nil },
|
||||
MaxDelay: 5 * time.Second,
|
||||
MaxBatch: 8,
|
||||
LowWatermark: 4,
|
||||
})
|
||||
go gc.Run()
|
||||
defer gc.Stop()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(8)
|
||||
for i := 0; i < 8; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
gc.Submit()
|
||||
}()
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("maxBatch=8 did not trigger flush with adaptive watermark")
|
||||
}
|
||||
}
|
||||
|
||||
func testAdaptiveWatermarkZeroCompat(t *testing.T) {
|
||||
// LowWatermark=0 should preserve Phase 2 behavior (always wait for delay).
|
||||
var syncCalls atomic.Uint64
|
||||
gc := NewGroupCommitter(GroupCommitterConfig{
|
||||
SyncFunc: func() error {
|
||||
syncCalls.Add(1)
|
||||
return nil
|
||||
},
|
||||
MaxDelay: 10 * time.Millisecond,
|
||||
LowWatermark: 0,
|
||||
})
|
||||
go gc.Run()
|
||||
defer gc.Stop()
|
||||
|
||||
// Two sequential submits should each trigger their own fsync.
|
||||
if err := gc.Submit(); err != nil {
|
||||
t.Fatalf("Submit 1: %v", err)
|
||||
}
|
||||
if err := gc.Submit(); err != nil {
|
||||
t.Fatalf("Submit 2: %v", err)
|
||||
}
|
||||
|
||||
if c := syncCalls.Load(); c != 2 {
|
||||
t.Errorf("syncCalls = %d, want 2 (no watermark optimization)", c)
|
||||
}
|
||||
}
|
||||
|
||||
func testAdaptiveThresholdBoundary(t *testing.T) {
|
||||
// Exactly lowWatermark concurrent submits: should batch and flush.
|
||||
var syncCalls atomic.Uint64
|
||||
gc := NewGroupCommitter(GroupCommitterConfig{
|
||||
SyncFunc: func() error {
|
||||
syncCalls.Add(1)
|
||||
return nil
|
||||
},
|
||||
MaxDelay: 50 * time.Millisecond,
|
||||
MaxBatch: 64,
|
||||
LowWatermark: 4,
|
||||
})
|
||||
go gc.Run()
|
||||
defer gc.Stop()
|
||||
|
||||
const n = 4 // exactly lowWatermark
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
gc.Submit()
|
||||
}()
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// All 4 should complete. SyncCount should be >= 1.
|
||||
if c := syncCalls.Load(); c == 0 {
|
||||
t.Error("syncCalls = 0, want >= 1")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("threshold boundary: timed out waiting for 4 submits")
|
||||
}
|
||||
}
|
||||
|
||||
func testAdaptiveRampUp(t *testing.T) {
|
||||
// Start with single callers (below watermark -> immediate), then ramp up.
|
||||
var syncCalls atomic.Uint64
|
||||
gc := NewGroupCommitter(GroupCommitterConfig{
|
||||
SyncFunc: func() error {
|
||||
syncCalls.Add(1)
|
||||
return nil
|
||||
},
|
||||
MaxDelay: 50 * time.Millisecond,
|
||||
MaxBatch: 64,
|
||||
LowWatermark: 4,
|
||||
})
|
||||
go gc.Run()
|
||||
defer gc.Stop()
|
||||
|
||||
// Phase 1: single serial submits (below watermark, immediate).
|
||||
for i := 0; i < 5; i++ {
|
||||
if err := gc.Submit(); err != nil {
|
||||
t.Fatalf("serial Submit %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
serialSyncs := syncCalls.Load()
|
||||
if serialSyncs != 5 {
|
||||
t.Errorf("serial phase: syncCalls = %d, want 5 (1 per call)", serialSyncs)
|
||||
}
|
||||
|
||||
// Phase 2: burst 64 concurrent submits (above watermark, batched).
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(64)
|
||||
for i := 0; i < 64; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
gc.Submit()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
totalSyncs := syncCalls.Load()
|
||||
batchSyncs := totalSyncs - serialSyncs
|
||||
// 64 submits should complete without deadlock. Some batching expected
|
||||
// but fast syncFunc means each call may flush individually.
|
||||
if batchSyncs > 64 {
|
||||
t.Errorf("batch phase: %d fsyncs for 64 submits, want <= 64", batchSyncs)
|
||||
}
|
||||
t.Logf("ramp_up: serial=%d, batch=%d fsyncs for 64 submits", serialSyncs, batchSyncs)
|
||||
}
|
||||
|
||||
func testAdaptiveMaxDelayHonored(t *testing.T) {
|
||||
// 2 concurrent submits (below watermark) should still complete promptly.
|
||||
gc := NewGroupCommitter(GroupCommitterConfig{
|
||||
SyncFunc: func() error { return nil },
|
||||
MaxDelay: 5 * time.Second,
|
||||
LowWatermark: 4,
|
||||
})
|
||||
go gc.Run()
|
||||
defer gc.Stop()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
for i := 0; i < 2; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
gc.Submit()
|
||||
}()
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Good: completed without waiting 5s maxDelay (adaptive skip).
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("2 submits should complete quickly (below watermark), took >2s")
|
||||
}
|
||||
}
|
||||
|
||||
func testAdaptiveFsyncErrorAllWaiters(t *testing.T) {
|
||||
// Inject fsync error during batched flush: all waiters must receive the error.
|
||||
errIO := errors.New("simulated disk error")
|
||||
var degradedCalled atomic.Bool
|
||||
|
||||
gc := NewGroupCommitter(GroupCommitterConfig{
|
||||
SyncFunc: func() error {
|
||||
return errIO
|
||||
},
|
||||
MaxDelay: 50 * time.Millisecond,
|
||||
MaxBatch: 64,
|
||||
LowWatermark: 4,
|
||||
OnDegraded: func() { degradedCalled.Store(true) },
|
||||
})
|
||||
go gc.Run()
|
||||
defer gc.Stop()
|
||||
|
||||
const n = 8
|
||||
var wg sync.WaitGroup
|
||||
errs := make([]error, n)
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
errs[idx] = gc.Submit()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for i, err := range errs {
|
||||
if !errors.Is(err, errIO) {
|
||||
t.Errorf("Submit[%d]: got %v, want %v", i, err, errIO)
|
||||
}
|
||||
}
|
||||
if !degradedCalled.Load() {
|
||||
t.Error("OnDegraded was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func testGroupSubmitAfterStop(t *testing.T) {
|
||||
gc := NewGroupCommitter(GroupCommitterConfig{
|
||||
SyncFunc: func() error { return nil },
|
||||
|
||||
Reference in new issue
Block a user