Files
seaweedfs/weed/filer/reader_pattern.go
T
Chris Lu 7e13340dcb filer: tolerance-window read pattern detection for concurrent readahead (#9983)
* 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.
2026-06-16 09:15:44 -07:00

67 lines
2.0 KiB
Go

package filer
import (
"sync/atomic"
)
type ReaderPattern struct {
isSequentialCounter int64
readFrontier int64 // highest (offset+size) observed across reads
}
const ModeChangeLimit = 3
// SeqTolerance: a read whose start is within this many bytes of the current read
// frontier still counts as sequential. Using a tolerance window rather than
// strict contiguity absorbs reordered/concurrent readahead (multiple ReadAt can
// be in flight at once) while still rejecting far random jumps.
const SeqTolerance = 8 << 20 // 8 MiB
// For streaming read: only cache the first chunk
// For random read: only fetch the requested range, instead of the whole chunk
func NewReaderPattern() *ReaderPattern {
return &ReaderPattern{
isSequentialCounter: 0,
readFrontier: 0,
}
}
func (rp *ReaderPattern) MonitorReadAt(offset int64, size int) {
// Advance the frontier to max(frontier, offset+size) and capture, in the same
// CAS loop, the pre-image this read is judged against. Reading the frontier
// inside the loop (rather than once up front) keeps `diff` below comparing
// against the freshest value even if a concurrent readahead advances the
// frontier while we loop. Lock-free, consistent with the rest of this type.
end := offset + int64(size)
var frontier int64
for {
frontier = atomic.LoadInt64(&rp.readFrontier)
if end <= frontier || atomic.CompareAndSwapInt64(&rp.readFrontier, frontier, end) {
break
}
}
// near = this read starts within SeqTolerance of where reads had reached.
// Hysteresis (the ±ModeChangeLimit counter) keeps a single outlier read from
// flipping the mode.
diff := offset - frontier
if diff < 0 {
diff = -diff
}
counter := atomic.LoadInt64(&rp.isSequentialCounter)
if diff <= SeqTolerance {
if counter < ModeChangeLimit {
atomic.AddInt64(&rp.isSequentialCounter, 1)
}
} else {
if counter > -ModeChangeLimit {
atomic.AddInt64(&rp.isSequentialCounter, -1)
}
}
}
func (rp *ReaderPattern) IsRandomMode() bool {
return atomic.LoadInt64(&rp.isSequentialCounter) < 0
}