mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-06 00:04:02 +00:00
7e13340dcb
* filer: tolerance-window read pattern detection for concurrent readahead ReaderPattern detected sequential access via strict contiguity (lastReadStopOffset == offset). Under concurrent/reordered ReadAt (parallel readahead, which ReadAt already supports via RLock + atomics), that exact match frequently misses even for a genuinely sequential stream, drifting toward random mode and disabling prefetch. Track a read frontier (max offset+size, advanced with a lock-free CAS loop) and treat a read as sequential when its start is within SeqTolerance (8 MiB) of the frontier. The window absorbs reordered/concurrent readahead while still rejecting far random jumps; the existing ModeChangeLimit hysteresis is unchanged. API (NewReaderPattern/MonitorReadAt/IsRandomMode) is unchanged, so reader_at is unaffected. Adds reader_pattern_test.go. * filer: read the read frontier inside the CAS loop Capture the frontier from the CAS loop's own load rather than a separate up-front snapshot, so the sequentiality diff is judged against the freshest pre-image even if a concurrent readahead advances the frontier while we loop. Also drops the now-redundant load. Mirrors the same fix on the write-side ReaderPattern twin (review feedback on the companion PR). * filer: add frontier/boundary/recovery guard tests for ReaderPattern Mirror the regression guards added to the write-side WriterPattern: assert the max-frontier never regresses on a backward read (the CAS invariant), pin both sides of the inclusive SeqTolerance boundary, and verify sustained near reads recover sequential mode out of the negative floor. Each guards an invariant the original four tests left uncovered.
105 lines
3.8 KiB
Go
105 lines
3.8 KiB
Go
package filer
|
|
|
|
import (
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
const mb = 1 << 20
|
|
|
|
func TestReaderPatternSequentialAndHysteresis(t *testing.T) {
|
|
rp := NewReaderPattern()
|
|
rp.MonitorReadAt(0, mb) // near(0 vs frontier 0) -> +1
|
|
rp.MonitorReadAt(mb, mb) // +2
|
|
rp.MonitorReadAt(2*mb, mb) // +3 (capped)
|
|
if rp.IsRandomMode() {
|
|
t.Fatal("a stream from offset 0 must not be random")
|
|
}
|
|
// a single far outlier does not flip to random (hysteresis): +3 -> +2
|
|
rp.MonitorReadAt(900*mb, mb)
|
|
if rp.IsRandomMode() {
|
|
t.Fatal("a single outlier read must not flip to random mode")
|
|
}
|
|
}
|
|
|
|
func TestReaderPatternFarFirstReadIsRandom(t *testing.T) {
|
|
rp := NewReaderPattern()
|
|
rp.MonitorReadAt(500*mb, mb) // far from frontier 0 -> -1
|
|
if !rp.IsRandomMode() {
|
|
t.Fatal("a far first read should be random")
|
|
}
|
|
}
|
|
|
|
func TestReaderPatternToleranceAbsorbsReorder(t *testing.T) {
|
|
rp := NewReaderPattern()
|
|
rp.MonitorReadAt(0, mb) // +1, frontier 1MB
|
|
rp.MonitorReadAt(6*mb, mb) // |6-1|=5MB <= 8MB tolerance -> near, +2, frontier 7MB
|
|
rp.MonitorReadAt(3*mb, mb) // |3-7|=4MB <= 8MB -> near, +3
|
|
if rp.IsRandomMode() {
|
|
t.Fatal("reordered reads within tolerance must stay sequential")
|
|
}
|
|
}
|
|
|
|
func TestReaderPatternRandomRecoveryNeedsHysteresis(t *testing.T) {
|
|
rp := NewReaderPattern()
|
|
rp.MonitorReadAt(100*mb, mb) // far -> -1
|
|
rp.MonitorReadAt(300*mb, mb) // far -> -2
|
|
rp.MonitorReadAt(500*mb, mb) // far -> -3 (capped), frontier 501MB
|
|
// a single near read must not immediately flip back to sequential: -3 -> -2
|
|
rp.MonitorReadAt(501*mb, mb)
|
|
if !rp.IsRandomMode() {
|
|
t.Fatal("one near read must not flip back from deep random mode")
|
|
}
|
|
}
|
|
|
|
// The max-frontier (vs the old atomic.Swap of the last read's stop offset) is the
|
|
// load-bearing difference of this detector: the frontier must never move backward,
|
|
// or a backward read would lower the baseline and make later far reads look near.
|
|
func TestReaderPatternFrontierNeverRegresses(t *testing.T) {
|
|
rp := NewReaderPattern()
|
|
rp.MonitorReadAt(500*mb, mb) // far first read -> -1, frontier 501MB
|
|
rp.MonitorReadAt(0, mb) // a backward read must not pull the frontier back
|
|
if got := atomic.LoadInt64(&rp.readFrontier); got != 501*mb {
|
|
t.Fatalf("frontier regressed to %d; the max-frontier must never move backward", got)
|
|
}
|
|
// Behavioral consequence: reads far below the preserved frontier stay random.
|
|
// Had the frontier regressed to ~1MB, this would wrongly read as sequential.
|
|
if !rp.IsRandomMode() {
|
|
t.Fatal("reads far below the preserved frontier must remain random")
|
|
}
|
|
}
|
|
|
|
// SeqTolerance is the central new tuning knob; pin both sides of the inclusive
|
|
// boundary so an off-by-one or a '<' vs '<=' change can't slip through silently.
|
|
func TestReaderPatternToleranceBoundary(t *testing.T) {
|
|
atBoundary := NewReaderPattern()
|
|
atBoundary.MonitorReadAt(SeqTolerance, 0) // diff == SeqTolerance -> near (inclusive), +1
|
|
if atBoundary.IsRandomMode() {
|
|
t.Fatal("a read exactly at frontier+SeqTolerance must count as sequential")
|
|
}
|
|
pastBoundary := NewReaderPattern()
|
|
pastBoundary.MonitorReadAt(SeqTolerance+1, 0) // diff == SeqTolerance+1 -> far, -1
|
|
if !pastBoundary.IsRandomMode() {
|
|
t.Fatal("a read one byte past frontier+SeqTolerance must count as random")
|
|
}
|
|
}
|
|
|
|
// The escape from random mode: sustained near reads must climb the counter back
|
|
// out of the negative floor and re-enter sequential (whole-chunk cache) mode.
|
|
func TestReaderPatternRecoversFromRandom(t *testing.T) {
|
|
rp := NewReaderPattern()
|
|
rp.MonitorReadAt(100*mb, mb) // far -> -1
|
|
rp.MonitorReadAt(300*mb, mb) // far -> -2
|
|
rp.MonitorReadAt(500*mb, mb) // far -> -3 (capped), frontier 501MB
|
|
if !rp.IsRandomMode() {
|
|
t.Fatal("three far reads must be random")
|
|
}
|
|
// contiguous near reads: -3 -> -2 -> -1 -> 0 -> +1, back out of random mode
|
|
for i := int64(0); i < 4; i++ {
|
|
rp.MonitorReadAt(501*mb+i*mb, mb)
|
|
}
|
|
if rp.IsRandomMode() {
|
|
t.Fatal("sustained near reads must recover sequential mode")
|
|
}
|
|
}
|