mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-05 22:12:04 +02:00
* 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> * s3: pin small ranged GETs to range reads A ranged GET whose first read lands within SeqTolerance of offset 0 is judged sequential immediately, and even a far-starting range could flip back mid-request; either way readChunkSliceAt downloads each covered chunk in full, multiplying disk reads for small ranged reads (measured ~7x). Pin random mode for ranged requests no larger than SeqTolerance so all of the request's buffer reads stay range fetches. Larger ranges keep the dynamic pattern, where whole-chunk fetches amortize. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: fetch only the part of a chunk the view covers Replaces the PinRandomMode size heuristic with a per-chunk coverage rule. ViewFromVisibleIntervals already clips chunk views to the request window, so a view that is not IsFullChunk() is one the request only partially needs; fetch it as a range regardless of the detected read pattern. This closes the holes a request-size pin left open: ranges larger than SeqTolerance no longer revert to whole-chunk downloads once their buffers look sequential, and ranges that fully cover a chunk keep the shared whole-chunk path instead of fetching 256KiB slices piecemeal. Prefetch (MaybeCache) skips clipped views so it cannot amplify a range read either. PinRandomMode is dropped: no caller needs it once coverage drives the fetch choice. Range fetches route through fetchChunkDataFn so tests observe them the same way as whole-chunk downloads. * filer: keep ciphered chunks on the whole-chunk path A range fetch cannot save bytes for a ciphered chunk: readEncryptedUrl always downloads and decrypts the whole blob before slicing. Sending partial views of ciphered chunks through fetchChunkRange would repeat the full download per buffer, so they keep the shared whole-chunk path where one download serves every buffer. Prefetch stays enabled for them for the same reason. * filer: keep compressed chunks on the whole-chunk path Like ciphered chunks, a range request on a compressed chunk makes the volume server read and decompress the whole needle, so range-per-buffer would repeat the full backend read for each 256KiB window. Route them through the shared whole-chunk path via ChunkView.CanRangeFetch. * filer: fall back to range fetch when a chunk exceeds the reader budget A ciphered or compressed chunk larger than readerCacheSizeMB can never be read through the whole-chunk path — the budget rejects the buffer — so its partial views must still range-fetch or the GET fails outright. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
125 lines
4.6 KiB
Go
125 lines
4.6 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")
|
|
}
|
|
}
|
|
|
|
// A ranged request's first read lands far from the frontier, but its
|
|
// remaining buffer reads are contiguous. The random verdict must stick for
|
|
// them — otherwise the tail of every range >256KiB pays a whole-chunk fetch.
|
|
func TestReaderPatternRangedReadStaysRandom(t *testing.T) {
|
|
rp := NewReaderPattern()
|
|
rp.MonitorReadAt(500*mb, 256*1024) // far first read -> -ModeChangeLimit
|
|
for i := 1; i <= 2; i++ {
|
|
rp.MonitorReadAt(500*mb+int64(i)*256*1024, 256*1024)
|
|
if !rp.IsRandomMode() {
|
|
t.Fatalf("contiguous read %d of a ranged request flipped back to sequential", i+1)
|
|
}
|
|
}
|
|
for i := 3; i < 10; i++ {
|
|
rp.MonitorReadAt(500*mb+int64(i)*256*1024, 256*1024)
|
|
}
|
|
if rp.IsRandomMode() {
|
|
t.Fatal("sustained sequential reads should restore sequential mode")
|
|
}
|
|
}
|