Files
seaweedfs/weed/storage/blockvol/nvme/write_retry.go
T
Ping QiuandClaude Opus 4.6 3557ae283f feat: Phase 10 CP10-3 -- NVMe/TCP Tier 1 optimizations, WAL admission control, benchmark platform
CP10-3 Tier 1 optimizations (T1-T4):
- TCP_NODELAY + 256KB socket buffers on NVMe/TCP connections
- Response batching: all C2H data chunks + CapsuleResp in single flush
- Tiered buffer pool (4KB/64KB/256KB sync.Pool) for write payloads
- Configurable MaxH2CDataLength wiring through controller/IC/chunking

BUG-CP103-1: NVMe write retry with jittered backoff for transient WAL pressure
- writeWithRetry() with bounded backoff [50/200/800ms]
- throttleOnWALPressure() pre-write delay above 90% WAL usage
- WALPressureProvider interface + NVMeAdapter.WALPressure()

BUG-CP103-2: Volume-level WAL admission control
- WALAdmission with counting semaphore (max concurrent writers)
- Soft watermark (0.7): small delay to desynchronize herd
- Hard watermark (0.9): block until flusher drains
- Single-deadline budget shared across watermark wait + semaphore
- Close-aware during both watermark and semaphore waits
- Wired into BlockVol.WriteLBA() and Trim()

Benchmark platform enhancements:
- NVMe benchmark actions and scenarios (A/B, CW sweep, IOQ sweep)
- Database benchmark actions (SQLite, pgbench)
- K8s operator QA reconciler tests
- New testrunner scenarios for HA, fault injection, CSI lifecycle

Test counts: 213 NVMe + 625 engine + operator + testrunner tests, all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:44:01 -07:00

81 lines
2.2 KiB
Go

package nvme
import (
"errors"
"math/rand"
"time"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
)
// WALPressureProvider extends BlockDevice with WAL pressure reporting.
type WALPressureProvider interface {
WALPressure() float64 // 0.0 = empty, 1.0 = full
}
// isRetryableWALPressure returns true if the error represents transient
// WAL pressure that may clear with a short retry.
func isRetryableWALPressure(err error) bool {
return err != nil && errors.Is(err, blockvol.ErrWALFull)
}
// writeRetryBackoffs defines the backoff schedule for writeWithRetry.
var writeRetryBackoffs = [3]time.Duration{
50 * time.Millisecond,
200 * time.Millisecond,
800 * time.Millisecond,
}
// sleepFn is the sleep function used by retry/throttle helpers.
// Replaced in tests for deterministic behavior.
var sleepFn = time.Sleep
// jitterFn returns a jitter duration given a max value.
// Replaced in tests for deterministic behavior.
var jitterFn = func(max time.Duration) time.Duration {
if max <= 0 {
return 0
}
return time.Duration(rand.Int63n(int64(max)))
}
// writeWithRetry wraps dev.WriteAt with target-side retry on WAL pressure.
// Non-WAL errors return immediately. On WAL pressure, retries with jittered
// backoff before giving up. Returns the last error unchanged so mapBlockError
// preserves DNR=0 semantics.
func writeWithRetry(dev BlockDevice, lba uint64, data []byte) error {
err := dev.WriteAt(lba, data)
if err == nil || !isRetryableWALPressure(err) {
return err
}
for _, backoff := range writeRetryBackoffs {
jitter := jitterFn(backoff / 4)
sleepFn(backoff + jitter)
err = dev.WriteAt(lba, data)
if err == nil || !isRetryableWALPressure(err) {
return err
}
}
return err
}
// throttleOnWALPressure inserts a small delay when WAL pressure is high,
// desynchronizing concurrent writers to reduce thundering-herd retry storms.
// No-op if the device does not implement WALPressureProvider.
func throttleOnWALPressure(dev BlockDevice) {
prov, ok := dev.(WALPressureProvider)
if !ok {
return
}
p := prov.WALPressure()
if p < 0.9 {
return
}
// Scale: 0.9→1ms, 0.95→3ms, 1.0→5ms
ms := (p - 0.9) * 50
if ms > 0 {
sleepFn(time.Duration(ms * float64(time.Millisecond)))
}
}