[feature](be) Add range read-ahead for cold queries - #67292
Draft
bobhan1 wants to merge 21 commits into
Draft
Conversation
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Cold-read prefetch and background cache-hole filling both need a reusable algorithm that turns sparse logical file ranges into bounded physical reads. Add a pure range coalescer that normalizes duplicate and overlapping bytes, then enforces maximum gap, range size, and cumulative read amplification at every merge boundary.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh --run --filter=FileRangeCoalescerTest.* -j100
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Cold-read prefetch needs to turn logical page intervals into bounded physical reads while opportunistically producing complete File Cache blocks. Add a pure planner that first applies generic range coalescing, then greedily completes high-coverage blocks by smallest additional read cost without exceeding the final range limit. The plan preserves input-to-buffer mappings and separately accounts for requested bytes, coalesced gaps, and block-fill bytes.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh --run --filter=FileRangePlannerTest.* -j100
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Cold-read prefetch needs to submit a complete batch of already-planned file ranges without blocking the scanner or coupling execution to page and cache-block policy. Add a generic asynchronous range reader with atomic query and BE budget admission, fixed worker concurrency, exact private buffers, worker-owned IO context, cancellation, shutdown, and terminal-state notification. Range slots are released at terminal completion while byte reservations follow the last buffer reference, so queued work and decoded data remain bounded independently.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh --run --filter=AsyncFileRangeReaderTest.* -j100
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Asynchronous cold-read ranges must preserve their planned byte boundaries and must not populate File Cache before consumption is known. Add per-read cache alignment control and an exact no-write CachedRemoteFileReader path that probes without creating cache cells, reuses only downloaded blocks, combines adjacent remote gaps, skips downloader ownership and waits, and falls back to exact remote reads when local cache files fail. AsyncFileRangeReader now selects this path even when peer-cache reads are enabled, while ordinary aligned read-through behavior remains unchanged.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh --run --filter=BlockFileCacheTest.*:AsyncCachedRemoteFileReaderTest.*:AsyncFileRangeReaderTest.* -j100 (177 passed, 3 existing disabled tests skipped)
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Scanner batches expose too little future IO for efficient remote reads. Add a per-physical-column byte window that always includes current-batch pages, advances through the final scan rowids in either direction, replenishes at configurable watermarks, and retires consumed, skipped, or fallback pages without coupling the component to cache lookup or IO execution.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh --run --filter=ColumnReadAheadTest.* -j100
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Per-column byte windows expose page candidates independently, while remote IO must be planned and admitted as one segment-level batch. Add a coordinator that probes Page Cache, coalesces all cache misses into final file ranges, submits the batch atomically to the asynchronous range reader, serves exact page slices from completed buffers, falls back to the original reader on planning, admission, or IO failure, and forwards only ranges with consumed pages to the future writeback path.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh --run --filter=SegmentReadAheadTest.* -j100
- ./run-be-ut.sh --run --filter=SegmentReadAheadTest.*:ColumnReadAheadTest.*:FileRangePlannerTest.*:AsyncFileRangeReaderTest.* -j100
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Concurrent range reads must be shared at BE scope while every scanner of one query must share range and resident-buffer admission. Add startup-owned AsyncFileRangeReader lifecycle, a lazily shared query context, cancellation propagation, and disabled-by-default runtime configuration. Fixed query and BE admission guards protect pathological plans without restoring the removed inflight tuning surface.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh --run --filter=QueryContextReadAheadTest.*:AsyncFileRangeReaderTest.* -j100
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Column byte windows were not connected to physical ordinal indexes or page decoding. Build compressed page candidates from each FileColumnIterator ordinal index, expose prepared physical-column plans, consume planned pages only after complete decoding succeeds, and retry tracked read-ahead or page-cache decode failures through the original reader while preserving existing corruption recovery.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh --run --filter=ColumnReaderTest.FileColumnIteratorPreparesCompressedPageByteWindow:ColumnReaderTest.FileColumnIteratorConsumesSubmittedReadAheadPage:ColumnReaderTest.FileColumnIteratorFallsBackAfterReadAheadChecksumFailure:SegmentReadAheadTest.*:ColumnReadAheadTest.* -j100
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Complex column iterators hid physical ranges from read-ahead, while array and map child ordinals cannot be known until their offset columns are decoded. Route planning through active struct children, plan offset and null-map columns with the current phase, and immediately submit exact array item or map key/value rowids with the lazy byte window after decoding offsets. Preserve scan direction and aggregate sparse map element ranges before child reads.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh --run --filter=ColumnReaderTest.StructReadAheadRoutesPhysicalColumnsByReadPhase:ColumnReaderTest.MapReadAheadPlansDependentChildrenAfterOffsets:ColumnReaderTest.ArrayReadAheadPlansDependentItemsAfterOffsets:ColumnReaderTest.FileColumnIteratorPreparesCompressedPageByteWindow:ColumnReaderTest.FileColumnIteratorConsumesSubmittedReadAheadPage:ColumnReaderTest.FileColumnIteratorFallsBackAfterReadAheadChecksumFailure -j100
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: SegmentIterator decoded each scanner batch before the range reader could see all currently plannable columns, preserving serial remote IO dependencies across small batches. Initialize query-owned range read-ahead before column iterators, classify physical columns by their actual dependency stage, submit eager and lazy plans together before the first decode, and immediately plan nested dependent children after offsets become available. Planning, admission, and asynchronous read failures continue through the original read path, while the new query path takes precedence over the legacy segment prefetcher.
### Release note
Add disabled-by-default query data-page range read-ahead with byte-window and range-planning settings.
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run '--filter=SegmentIteratorReadAheadTest.*:SegmentReadAheadTest.*:ColumnReaderTest.*ReadAhead*'
- Behavior changed: Yes. When enable_query_read_ahead is enabled in Cloud mode, query segment reads use unified eager and lazy range read-ahead; the default remains disabled.
- Does this need documentation: Yes. The Phase 2 design document is maintained separately.
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Query range writeback must hand complete File Cache blocks to the existing asynchronous write path without duplicating its buffer ownership, write-epoch fencing, inflight deduplication, and backpressure rollback. Extract that sequence into one AsyncCacheWriteManager operation and keep CachedRemoteFileReader on the same entry point. The operation copies transient input bytes before returning and reports each best-effort rejection reason to its caller.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run '--filter=AsyncCacheWriteManagerTest.SubmitBlock*:BlockFileCacheTest.async_write*'
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Query read-ahead produces variable file ranges, while the existing asynchronous File Cache writer accepts complete fixed-size blocks. Add a synchronous ownership-transfer boundary that splits an already consumed range at cache-block boundaries, routes complete blocks and partial fragments to separate consumers, and handles the short physical EOF block as complete. The dispatcher reports generated and accepted fragments without retaining the query range buffer.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run '--filter=RangeWritebackDispatcherTest.*'
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Partial query ranges can cover disjoint bytes inside one File Cache block. Add a block-local planner that unions existing coverage, takes its complement to produce holes, and applies the dedicated three-parameter coalescing policy to reduce background remote requests without crossing the block boundary.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run '--filter=HoleFillPlannerTest.*'
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Background hole filling produces an owned complete cache-block buffer. Add a Phase 1 handoff that preserves that buffer without another payload copy, while retaining epoch fencing, inflight deduplication, and queue admission. Also expose a point-in-time spare-capacity check so low-priority hole-fill reads start only when the write queue can accept a block without evicting foreground work.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run '--filter=AsyncCacheWriteManagerTest.SubmitOwnedBlockPreservesBufferIdentity:AsyncCacheWriteManagerTest.ReportsOnlyUnusedPendingCapacity:AsyncCacheWriteManagerTest.SubmitBlockCopiesDeduplicatesAndRollsBackBackpressure:AsyncCacheWriteManagerTest.SubmitBlockRejectsStaleEpochAndAllocationFailure'
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: A background hole-fill task can observe spare write capacity before its remote read and find the queue full at final handoff. Add a spare-capacity-only admission mode so this low-priority result is rejected instead of evicting an already queued foreground cache write. Normal submissions retain the existing oldest-queued replacement behavior.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run '--filter=AsyncCacheWriteManagerTest.ReportsOnlyUnusedPendingCapacity:AsyncCacheWriteManagerTest.DropOldestReplacesOnlyOldestQueuedTaskAndKeepsInflightReaderAlive:AsyncCacheWriteManagerTest.SubmitOwnedBlockPreservesBufferIdentity'
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Query read-ahead can produce partially covered File Cache blocks that need bounded asynchronous completion before entering the existing cache write path. Add a BE-level partial-block manager that merges queued fragments, deduplicates active blocks, evicts the oldest queued block under pressure, waits for actual Phase 1 spare capacity, fills block-local holes through a dedicated remote-read pool, and hands completed buffers to Phase 1 without another copy. Expose the number of currently unused Phase 1 task slots so concurrent hole-fill reads reserve capacity correctly.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run --filter=PartialBlockWritebackManagerTest.*:PartialBlockWritebackOptionsTest.*:AsyncCacheWriteManagerTest.ReportsOnlyUnusedPendingCapacity
- Behavior changed: No; this commit adds an unwired internal component.
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Background partial-block writeback needs production defaults and a BE-owned lifetime that outlives query read-ahead work while remaining inside the File Cache lifetime. Add validated hole-fill configuration, create the manager after File Cache initialization, and stop it before File Cache teardown. Keep option invariants with the option type through PartialBlockWritebackOptions::validate().
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run --filter=PartialBlockWritebackOptionsTest.*
- Behavior changed: No; this commit initializes an internal component that is not yet connected to query read-ahead writeback.
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: A consumed read-ahead range can be written back only with cache invalidation state captured before its asynchronous read begins. Add a per-submission consumer factory to SegmentReadAhead, capture it immediately before submitting IO, and retain the resulting consumer only on accepted ranges. This keeps cache-specific state outside the segment coordinator while ensuring unused ranges never invoke writeback.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run --filter=SegmentReadAheadTest.*:ColumnReaderTest.FileColumnIteratorConsumesSubmittedReadAheadPage:ColumnReaderTest.FileColumnIteratorFallsBackAfterReadAheadChecksumFailure
- Behavior changed: No; the production writeback consumer is not connected yet.
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Query read-ahead produces consumed variable-length buffers that can span both complete and partial File Cache blocks. Add RangeCacheWriteback to split each consumed range, submit complete blocks directly to the existing asynchronous write path, and enqueue partial fragments for background hole filling under the same pre-read invalidation epoch. Expose Phase 1 submission readiness so disabled or stopped managers retain no epoch or partial tasks, and retire queued hole-fill work if its downstream manager stops.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run --filter=RangeCacheWritebackTest.*:PartialBlockWritebackManagerTest.RejectsStoppedPhase1WithoutQueueing:AsyncCacheWriteManagerTest.ReportsSubmissionLifecycle
- ./run-be-ut.sh -j100 --run --filter=PartialBlockWritebackManagerTest.DropsQueuedTaskWhenPhase1Stops
- Behavior changed: No; this commit adds an internal writeback router that is not connected to SegmentIterator yet.
- Does this need documentation: No
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Query read-ahead must publish only buffers that were actually consumed, while preserving the File Cache invalidation epoch captured before the remote read begins. Build immutable writeback metadata from CachedRemoteFileReader, retain its underlying remote reader for background hole filling, capture an epoch before each accepted asynchronous range submission, and route only consumed READY ranges into complete-block or partial-block writeback. Generic file readers continue to use read-ahead without cache writeback.
### Release note
Enable consumed query read-ahead ranges to populate File Cache when asynchronous cache writeback is enabled.
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run --filter=AsyncCachedRemoteFileReaderTest.builds_consumed_range_writeback_context:SegmentReadAheadTest.*:SegmentIteratorReadAheadTest.*
- Behavior changed: Yes; consumed successful query read-ahead ranges now enter cache writeback when both read-ahead and asynchronous cache writeback are enabled.
- Does this need documentation: Yes; covered by the Phase 2 design document.
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: Read-ahead option invariants were split across free validation functions whose ownership was unclear. Move validation onto FileRangeCoalesceOptions, FileRangePlanOptions, AsyncFileRangeReaderOptions, ColumnReadAheadOptions, and PartialBlockWritebackOptions, and make each planner, reader, window, and lifecycle path call those member functions. Composite options recursively validate nested coalescing options before applying their own cross-field constraints.
### Release note
None
### Check List (For Author)
- Test: Unit Test
- ./run-be-ut.sh -j100 --run --filter=FileRangeCoalescerTest.*:FileRangePlannerTest.*:AsyncFileRangeReaderTest.ValidatesReaderOptions:ColumnReadAheadTest.ValidateWatermarks
- ./run-be-ut.sh -j100 --run --filter=PartialBlockWritebackOptionsTest.*
- Behavior changed: No; validation ownership is refactored without changing valid option semantics.
- Does this need documentation: No
Contributor
|
Thank you for your contribution to Apache Doris. Please clearly describe your PR:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What problem does this PR solve?
Issue Number: None
Problem Summary:
Doris data pages are usually much smaller than a 1 MiB File Cache block. A cold query that misses File Cache currently expands each page read to block boundaries, which provides a fixed 1 MiB form of read-ahead but also creates large read amplification for sparse row IDs, point queries, TopN, and delayed materialization. At the same time,
FileColumnIteratorconsumes data pages one at a time, so the relatively small scanner batch exposes many serial S3 dependencies even when the query will soon read adjacent pages from the same segment.This PR adds an opt-in, query-owned range read-ahead path for cloud scans. It plans future pages by compressed bytes across scanner batches, coalesces nearby page intervals into bounded file ranges, reads those ranges concurrently, serves exact page slices to the existing page decoder, and writes back only ranges that the query actually consumes. Complete 1 MiB blocks enter the existing asynchronous File Cache write path directly; partial blocks are completed by a bounded background hole-fill pipeline before entering that same path.
The feature is disabled by default. Admission rejection, cancellation, speculative-read failure, checksum/decode failure, and cache-writeback pressure do not change query correctness: reads fall back to the existing path, while writeback is best effort.
Architecture
flowchart LR SCAN["Scanner batch row IDs"] --> ROLE["SegmentIterator<br/>classify eager and lazy columns"] ROLE --> WINDOW["ColumnReadAhead<br/>compressed-byte windows"] WINDOW --> PLAN["FileRangePlanner<br/>coalesce and optional block completion"] PLAN --> ASYNC["AsyncFileRangeReader<br/>bounded concurrent exact reads"] ASYNC --> BUFFER["Query-owned range buffers"] BUFFER --> PAGE["ReadAheadFileReader<br/>exact page slices"] PAGE --> DECODE["Existing PageIO and page decoder"] DECODE --> USED{"Was any page in the range consumed?"} USED -->|No| DROP["Release buffer"] USED -->|Yes| SPLIT["Split at 1 MiB File Cache boundaries"] SPLIT -->|Complete block| PHASE1["Existing AsyncCacheWriteManager"] SPLIT -->|Partial block| HOLE["PartialBlockWritebackManager"] HOLE --> GET["Dedicated S3 GET pool<br/>read block-local holes"] GET --> PHASE1Read-ahead planning
Columns are divided by data dependency rather than by SQL expression kind:
Each physical column keeps a high and low watermark measured in compressed page bytes. Pages required by the current batch always enter the plan, even if they exceed the high watermark. Additional pages extend across future scanner batches until the high watermark is reached. Once consumed, discarded, or failed pages reduce the remaining window to the low watermark, the planner refills it. This compensates for scanner batch boundaries without tying I/O depth to row count or data type.
sequenceDiagram participant Scanner participant Segment as SegmentIterator participant Window as ColumnReadAhead participant Reader as AsyncFileRangeReader participant Decoder as Page decoder Scanner->>Segment: Request the next row batch Segment->>Window: Refill every currently plannable physical column Note over Window: Eager target 8 MiB and refill at 4 MiB<br/>Lazy target 256 KiB and refill at 128 KiB Window->>Reader: Submit one segment-level range plan par Concurrent exact range reads Reader->>Reader: Read range1 and Reader->>Reader: Read range2 and Reader->>Reader: Read range3 end Reader-->>Segment: Publish ready range buffers Segment->>Decoder: Decode eager pages Decoder-->>Segment: Produce filters and dependent row IDs Segment->>Window: Plan newly resolvable dependent child pages Segment->>Decoder: Decode selected lazy pagesflowchart LR FULL["Window near its high watermark"] --> USE["Pages are consumed, discarded, or failed"] USE --> CHECK{"Remaining compressed bytes<br/>at or below the low watermark?"} CHECK -->|No| KEEP["Keep the current plan"] CHECK -->|Yes| EXTEND["Append future pages toward<br/>the high watermark"] EXTEND --> SUBMIT["Submit newly formed ranges"] SUBMIT --> FULLRange coalescing and foreground block completion
The coalescing shape follows the same left-to-right idea used by Velox: sort and de-duplicate page intervals, then add the next interval only when all three limits remain satisfied.
After base ranges are formed, the planner evaluates File Cache blocks touched by their boundaries. A block with at least 50% requested-page coverage may be completed in the foreground when the final range still fits the 2 MiB limit. If both boundaries qualify but cannot both fit, the side requiring fewer additional bytes is selected first. This avoids sending a nearly complete block through a second background S3 GET: for object storage, reading a modest number of additional bytes in the existing request is normally cheaper than adding another request and scheduling round.
flowchart LR PAGES["Requested page intervals"] --> SORT["Sort and de-duplicate"] SORT --> NEXT{"Add the next interval?"} NEXT -->|"gap within 64 KiB<br/>range within 2 MiB<br/>amplification within 2.0"| MERGE["Extend current base range"] MERGE --> NEXT NEXT -->|Any limit exceeded| CLOSE["Close current base range"] CLOSE --> COVER{"Boundary block coverage<br/>at least 50 percent?"} COVER -->|Yes and final range fits 2 MiB| FILL["Extend to the block boundary"] COVER -->|No| EXACT["Keep the base range"] FILL --> READ["Submit exact asynchronous range read"] EXACT --> READAsyncFileRangeReaderperforms the final ranges through aNO_WRITE + UNALIGNEDrequest context.CachedRemoteFileReaderstill reuses already downloaded cache blocks, but a cold miss reads only the exact missing spans and does not reserve or populate File Cache on the query thread.Concurrency, ownership, and cancellation
AsyncFileRangeReaderis a BE-level concurrent file-range executor. It deliberately does not understand pages, coalescing, or writeback policy. A submission atomically reserves all buffers and both query/BE budgets before any range is queued; a rejected submission leaves budgets unchanged and all pages return to the original synchronous path.The default executor has 64 workers. Fixed safety guards allow at most 128 in-flight ranges and 256 MiB of resident buffers per query, and 1024 ranges and 1 GiB per BE. A range slot is released at terminal I/O state, while its byte reservation follows the range buffer lifetime so decoded pages and writeback cannot escape accounting. Query cancellation cancels queued work and marks running work for cancellation; BE shutdown rejects new submissions, cancels queued work, and waits for running reads before releasing shared resources.
Consumed-range writeback
Writeback metadata and the File Cache invalidation epoch are captured immediately before each asynchronous read submission. A successfully read range becomes eligible only after at least one of its pages is decoded. Purely speculative ranges are released without cache work.
Eligible ranges are split at File Cache block boundaries:
PartialBlockWritebackManagerqueue.The existing fixed-block asynchronous cache write path is the only component that creates and persists File Cache blocks. Query range buffers and background partial-block buffers remain separate from its queue and ownership model.
flowchart TD READY["Range read is ready"] --> USED{"At least one page was decoded?"} USED -->|No| RELEASE["Release query-owned buffer"] USED -->|Yes| CUT["Split consumed range at 1 MiB block boundaries"] CUT --> COMPLETE{"Fragment covers the complete valid block?"} COMPLETE -->|Yes| WRITER["Existing AsyncCacheWriteManager"] COMPLETE -->|No| PARTIAL["PartialBlockWritebackManager queue"] PARTIAL --> HOLES["Complete block-local holes"] HOLES --> WRITER WRITER --> CACHE["File Cache block"]Background hole fill
The partial-block queue is bounded to 256 MiB per BE. Fragments for the same queued block are merged; a block that is already active is deduplicated. Under pressure, the oldest queued block is evicted. When all pending blocks are already active, the new fragment is rejected. These outcomes affect only cache population.
The scheduler dispatches a block only when the existing cache writer has spare capacity. If capacity is unavailable, the task remains queued and the scheduler waits; S3 GET workers are not occupied while waiting for cache-write capacity. The default dedicated hole-fill pool has 32 workers, so slow object-storage GETs do not block the queue scheduler or the foreground read-ahead executor.
For one partial block, the hole-fill algorithm is:
flowchart TD ENQUEUE["Partial block fragment arrives"] --> STATE{"State for the same block"} STATE -->|Queued| MERGE["Merge covered intervals into the queued entry"] STATE -->|Active| DEDUP["Drop duplicate writeback work"] STATE -->|Absent| ADMIT{"Pending-byte budget available?"} ADMIT -->|Yes| QUEUE["Append block entry to the queue"] ADMIT -->|No and queued entries exist| EVICT["Evict the oldest queued entry"] EVICT --> QUEUE ADMIT -->|No and every entry is active| REJECT["Reject this cache-only work"] MERGE --> WAIT["Wait for existing cache-writer capacity"] QUEUE --> WAIT WAIT --> ACTIVE["Mark active and dispatch to the dedicated S3 pool"] ACTIVE --> COMPLEMENT["Take the complement of covered bytes"] COMPLEMENT --> COALESCE["Coalesce block-local holes"] COALESCE --> GET["Issue one or more block-local S3 GETs"] GET --> HANDOFF["Hand off the completed 1 MiB buffer"] HANDOFF --> WRITER["Existing cache writer"]For example, if a block already contains
[0, 256 KiB),[384 KiB, 416 KiB), and[544 KiB, 1024 KiB), its holes are[256 KiB, 384 KiB)and[416 KiB, 544 KiB). Their 32 KiB separation meets the gap and amplification limits, so they become one S3 GET[256 KiB, 544 KiB). The 32 KiB already present between the holes is overwritten with identical remote data, trading a small byte increase for one fewer object-storage request.flowchart LR C1["Covered<br/>0 to 256 KiB"] --> H1["Hole 1<br/>256 to 384 KiB"] H1 --> C2["Covered gap<br/>384 to 416 KiB"] C2 --> H2["Hole 2<br/>416 to 544 KiB"] H2 --> C3["Covered<br/>544 to 1024 KiB"] H1 --> MERGED["Coalesce across the 32 KiB covered gap"] H2 --> MERGED MERGED --> GET["One S3 GET<br/>256 to 544 KiB"] GET --> BLOCK["Completed 1 MiB block"]Failure and compatibility boundaries
READER_QUERYsegment scans whenenable_query_read_ahead=true; the default remainsfalse.CachedRemoteFileReaderadds the exact no-write read and consumed-range writeback integration.Configuration defaults
enable_query_read_aheadfalseread_ahead_io_workers_per_be64read_ahead_eager_high_watermark_bytes8 MiBread_ahead_eager_low_watermark_bytes4 MiBread_ahead_lazy_high_watermark_bytes256 KiBread_ahead_lazy_low_watermark_bytes128 KiBread_ahead_max_gap_bytes64 KiBread_ahead_max_range_bytes2 MiBread_ahead_max_read_amplification_ratio2.0read_ahead_block_fill_min_coverage0.5hole_fill_max_gap_bytes32 KiBhole_fill_max_range_bytes1 MiBhole_fill_max_read_amplification_ratio2.0hole_fill_max_pending_bytes_per_be256 MiBhole_fill_workers_per_be32Implementation structure
The 21 commits are intentionally split by component: generic range coalescing, cache-aware planning, concurrent range execution, exact cache reads, column windows, segment coordination, query ownership, physical/nested-column integration, fixed-block handoff, consumed-range dispatch, block-local hole planning, background writeback, BE lifecycle wiring, production writeback routing, and option-validation ownership. Each core algorithm and concurrency boundary has focused BE unit tests.
Release note
Add an opt-in range read-ahead and File Cache writeback pipeline for cold cloud queries. The feature is disabled by default.
Check List (For Author)
Test
./run-be-ut.sh -j100 --run '--filter=FileRangeCoalescerTest.*:FileRangePlannerTest.*:AsyncFileRangeReaderTest.*:ColumnReadAheadTest.*:SegmentReadAheadTest.*:SegmentIteratorReadAheadTest.*:RangeWritebackDispatcherTest.*:HoleFillPlannerTest.*:RangeCacheWritebackTest.*:PartialBlockWritebackOptionsTest.*:PartialBlockWritebackManagerTest.*:QueryContextReadAheadTest.*:AsyncCachedRemoteFileReaderTest.*:AsyncCacheWriteManagerTest.*:ColumnReaderTest.*ReadAhead*'build-support/check-format.shgit diff --check upstream/master...HEADBehavior changed:
Does this need documentation?
Draft validation still pending
Check List (For Reviewer who merge this PR)