Files
seaweedfs/weed/filer/reader_pattern.go
T
Chris LuandDevin 8227ae3f49 filer: keep a ranged read in random mode through its contiguous tail
A far ReadAt on a fresh ReaderPattern left the sequential counter at -1,
so the next buffer of the same ranged request landed on the frontier and
flipped the verdict straight back to sequential — readChunkSliceAt then
paid a whole-chunk fetch for the remainder of the range. Drop the
counter to -ModeChangeLimit when random mode is entered so the verdict
needs sustained sequential evidence to undo, matching the hysteresis an
established sequential stream already gets.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-02 09:32:40 -07:00

70 lines
2.3 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 <= 0 {
// Entering random mode is a strong verdict: drop to the bottom of
// the window so the contiguous tail of one ranged request cannot
// flip it back on the next buffer read and pay a whole-chunk fetch.
atomic.StoreInt64(&rp.isSequentialCounter, -ModeChangeLimit)
} else {
atomic.AddInt64(&rp.isSequentialCounter, -1)
}
}
func (rp *ReaderPattern) IsRandomMode() bool {
return atomic.LoadInt64(&rp.isSequentialCounter) < 0
}