Skip to content

[opt](file cache) Add PageIO read-ahead, I/O coalescing, and cache block hole filling - #67292

Merged
bobhan1 merged 21 commits into
apache:masterfrom
bobhan1:feature/read-ahead-io-coalescing-phase2
Oct 30, 2026
Merged

bobhan1 merged 21 commits into
apache:masterfrom
bobhan1:feature/read-ahead-io-coalescing-phase2

Conversation

@bobhan1

@bobhan1 bobhan1 commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: None

Related PR: #67579 (validation and follow-ups)

Problem Summary:

Doris data pages are commonly much smaller than a 1 MiB File Cache block. On a cache miss, the existing query path expands a page read to the block boundary. This behaves like a fixed 1 MiB read-ahead for continuous scans, but the size and shape cannot follow the actual scan. Sparse point-query data-page reads can therefore download a full block for a small page, while sequential scans still expose one remote page dependency at a time through FileColumnIterator.

This PR adds an opt-in cloud query path that prepares each eligible top-level scalar scan column's compressed data pages once after pruning, advances its window on physical data-page reads, merges nearby pages into bounded file ranges, and serves the original exact page slices to the existing PageIO and decoder. Only a range that PageIO actually consumes becomes eligible for File Cache writeback. Complete cache blocks use the existing asynchronous cache writer; partial blocks enter a bounded background pipeline that fills their holes and then hands a complete block to the same writer.

The implementation covers top-level scalar columns in regular segment scans and short-circuit point queries, including scalar row-store reads. ARRAY/MAP/STRUCT and their offsets, null maps and children retain the original readers and legacy segment prefetchers; variant paths remain on the original path. TopN/global delayed-materialization second-stage read-ahead is outside the current PRs; that path stays identical to master. The feature is disabled by default and requires both enable_query_read_ahead=true and enable_async_file_cache_write=true. Planning, admission, asynchronous read, checksum, decode, and best-effort writeback failures preserve the original query result path.

Entry paths and component boundaries

flowchart TB
    SCAN["Regular SegmentIterator scan"]
    POINT["PointQueryExecutor<br/>row store or missing columns"]

    SCAN --> COLUMNS["After pruning: initialize scalar column windows once"]
    POINT --> LOOKUP["Collect exact row IDs and required scalar columns"]

    COLUMNS --> SEGMENT["SegmentReadAhead"]
    LOOKUP --> SEGMENT
    SCAN --> COMPLEX["Complex columns: original reader and prefetch"]
    POINT --> COMPLEX

    SEGMENT --> PLAN["FileRangePlanner"]
    PLAN --> SCHED["FileRangeReadScheduler"]
    SCHED --> POOL["Existing SegmentPrefetchThreadPool"]
    POOL --> READER["SegmentReadAheadFileReader"]
    READER --> PAGEIO["Existing PageIO and decoder"]
    READER --> WRITEBACK["Consumed-range writeback"]
Loading
Component Responsibility
ColumnReadAhead / ColumnReadAheadPlan Prepare eligible physical pages once and advance byte windows from page reads; the plan owns reset and timing aggregation.
SegmentReadAhead Combine all column plans for a segment, probe Page Cache, plan ranges, submit one batch, and register exact page locations.
FileRangePlanner Merge sorted page intervals and optionally complete high-coverage boundary cache blocks. It performs no I/O.
FileRangeReadScheduler Admit ranges that fit query/BE budgets in one batch pass; provide cancellation and concurrent execution for already planned file ranges.
SegmentReadAheadFileReader Serve an exact page slice from a ready range, immediately retire that page, and fall back to the original reader when needed.
RangeWritebackDispatcher / RangeCacheWriteback Split a consumed range at 1 MiB cache-block boundaries and route complete blocks and partial fragments.
PartialBlockWritebackManager Bound and deduplicate partial-block work, fill single ranges on block workers or multiple ranges on a shared remote-read pool, and submit complete owned buffers to the existing cache writer.

Read-ahead window

Columns are classified by dependency: eager columns have no earlier filtering dependency; lazy columns depend on an earlier filter or expression result. After segment pruning, initialize all currently resolvable eager and lazy windows together. Build each column's candidate-page indexes once from the pruned selection. Later data-page reads advance the window using the already resolved physical page index, before Page Cache lookup. Small scanner batches no longer rebuild rowid plans for every column.

Each window selects eligible pages until their compressed bytes reach the configured size; its final page can cross that size. The second selected page triggers the next window, or the only page for a one-page window. First access and a jump beyond planned coverage start at the current page. Reverse scans retain predictions while reading an ascending batch and retire them when the next lower batch begins.

Scalar point reads keep exact-row preparation, grouping sorted row IDs by distinct pages. Complex columns keep the original dependent-child reading path. Page completion updates ownership/accounting; the page-read position drives scan-window extension. Initial planning remains on the scanner thread; no separate planning pool is introduced.

Configuration Default Reason for the default
read_ahead_eager_window_bytes 4 MiB An early trigger exposes the next window while the current one is read, bridging small scanner batches.
read_ahead_lazy_window_bytes 1 MiB Expose more dependent-column I/O while retaining a smaller window than eager columns.
flowchart TD
    Init["After pruning: select candidate pages once<br/>initialize eager and lazy windows together"] --> Read
    Read["Column reader resolves a physical data page"] --> Advance["Retire passed predictions<br/>include the current page"]
    Advance --> Outside{"At or beyond planned coverage?"}
    Outside -- Yes --> Start["Start a byte window at this page<br/>mark its second selected page as the trigger"]
    Outside -- No --> Trigger
    Start --> Trigger{"Current page reaches the trigger?"}
    Trigger -- Yes --> Append["Append the next byte window<br/>move the trigger into it"]
    Trigger -- No --> Apply
    Append --> Apply["Submit newly planned pages<br/>reuse existing buffered pages"]
    Apply --> PageIO["Existing Page Cache / PageIO / decoder"]
    PageIO --> Read
Loading

The window is a planning unit, not a resident-memory limit; query and BE budgets bound retained range buffers. ReadAheadColumnInitTime covers initialization, ReadAheadPageAdvanceTime covers page-driven maintenance, and ReadAheadCurrentBatchPlanTime covers the retained exact-row path. Window discard and extension remain sub-timers under ReadAheadColumnPlanTime.

Range planning and concurrent I/O

The range merge uses a left-to-right algorithm inspired by Velox. Here, “merge” means combining nearby requested page intervals into one physical object-store GET. SegmentReadAhead supplies sorted, disjoint, non-empty page intervals; one linear pass adds the next interval only when every configured limit remains satisfied.

Configuration Default Meaning
read_ahead_max_gap_bytes 64 KiB Largest unrequested gap that one foreground GET may cross.
read_ahead_max_range_bytes 2 MiB Largest range produced by merging or foreground block completion. A single indivisible input page may exceed it and remains a standalone read.
read_ahead_max_read_amplification_ratio 1.0 Largest ratio of physical range bytes to requested page bytes.

After the base ranges are formed, FileRangePlanner examines their boundary File Cache blocks. read_ahead_block_fill_min_coverage=1.0 requires full requested-page coverage, so the default adds no foreground bytes to complete a partial block. Lowering this threshold retains the existing optional completion algorithm within the 2 MiB range limit.

flowchart LR
    PAGES["Sorted unique data-page intervals"] --> MERGE["Linear three-limit merge"]
    MERGE --> BASE["Base ranges"]
    BASE --> COVER{"Boundary block coverage<br/>at least 50 percent?"}
    COVER -->|"Yes and final range at most 2 MiB"| COMPLETE["Extend to block boundary"]
    COVER -->|No| KEEP["Keep base range"]
    COMPLETE --> BATCH["One atomic submission batch"]
    KEEP --> BATCH
    BATCH -->|Accepted| WORKERS["Concurrent range reads"]
    BATCH -->|Rejected| FALLBACK["Original synchronous path"]
Loading

FileRangeReadScheduler walks the planned ranges once under both query and BE budget locks, admitting each range that fits and continuing past skipped ranges so later smaller ranges can still proceed. Returned handles preserve the original range indices, with empty slots for budget skips; only admitted ranges receive buffers and tasks. It adds no range-count or concurrency quota. Accepted tasks run on the existing segment-prefetch thread pool, whose configured thread count controls physical read concurrency; the scheduler does not own or stop that shared pool. A byte reservation follows the query-owned buffer lifetime and is released with the last reference.

Scheduled reads suppress direct cache writes. A ready range is indexed back to every exact page it contains, so PageIO still receives the original offset and size and keeps the existing checksum and decoder behavior.

sequenceDiagram
    participant Scan as Scanner
    participant Column as ColumnReadAhead
    participant Segment as SegmentReadAhead
    participant Scheduler as FileRangeReadScheduler
    participant PageIO

    Scan->>Column: initialize candidates and first windows after pruning
    Column->>Segment: eager and lazy initial plans
    Segment->>Scheduler: page-cache probe, merge, complete blocks, submit ranges
    loop Physical data-page reads
        Scan->>Column: resolved page index
        Column->>Segment: new and released pages when the window changes
        Segment->>Scheduler: submit newly planned ranges
        Scan->>PageIO: exact page offset and size
        PageIO->>Segment: read buffered page
        Segment-->>PageIO: ready slice or original-reader fallback
    end
Loading

Point queries create one request-scoped range-read context and plan either the row-store column or all missing column-store values before decoding; existing row-cache and result-padding behavior remains unchanged. The point-query component owns the shared exact-row page helpers and their tests.

Consumed-range writeback

A ready range that read remote bytes becomes eligible for writeback when PageIO successfully reads its first registered page, and it is published at most once. Pure File Cache/inflight hits skip writeback. A prefetched range whose pages are all passed without use is released without a cache write.

flowchart TD
    CONSUMED["Consumed ready range"] --> ORIGIN{"Range read remote bytes?"}
    ORIGIN -- No --> SKIP["Skip duplicate writeback"]
    ORIGIN -- Yes --> SPLIT["Split at 1 MiB File Cache boundaries"]
    SPLIT --> FULL{"Fragment covers the complete valid block?"}
    FULL -->|Yes| ASYNC["Existing AsyncCacheWriteManager"]
    FULL -->|No| PARTIAL["PartialBlockWritebackManager queue"]
    PARTIAL --> DISPATCH["HoleFillDispatch: ordered deadline and capacity checks"]
    DISPATCH --> ELIGIBLE{"Aggregation deadline elapsed<br/>and cache writer has capacity?"}
    ELIGIBLE -- No --> WAIT["Dispatcher waits on condition variable<br/>task remains queued and mergeable"]
    WAIT --> ELIGIBLE
    ELIGIBLE -- Yes --> READY["Bounded worker handoff"]
    READY --> WORKER["Block worker: recheck cache and plan block-local holes"]
    WORKER --> COUNT{"Number of merged hole ranges?"}
    COUNT -- Zero --> OWNED["Complete owned block buffer"]
    COUNT -- One --> INLINE["Read directly on the block worker"]
    COUNT -- Multiple --> REMOTE["HoleFillRemoteReadPool: one task per range"]
    REMOTE --> JOIN["Block worker joins all accepted reads"]
    INLINE --> RESULT{"All reads succeeded?"}
    JOIN --> RESULT
    RESULT -- Yes --> OWNED
    RESULT -- No --> DROP["Drop best-effort writeback"]
    OWNED --> ASYNC
    ASYNC --> CACHE["Existing File Cache write path"]
Loading

Complete 1 MiB fragments, including a shorter physical EOF block, enter the existing asynchronous cache writer directly. Partial fragments are copied into a BE-level queue. Fragments for the same queued block merge under that task's fragment lock; an already active fill is deduplicated. A combined inflight-buffer and File Cache probe skips cached/downloading blocks before admission and once when a worker starts the task, outside the queue mutex. The task then executes its planned GETs and retains the final cache-writer checks. Its source pointer reads directly from the inner remote reader while retaining the cached wrapper as the shared owner, keeping the source open through delayed hole filling.

Queued partial blocks retain a mutable minimum aggregation interval, hole_fill_merge_delay_ms=2000 (milliseconds), to keep queued fragments mergeable for at least two seconds before dispatch. Timing starts at first admission; merging later fragments preserves the timestamp. The queue remains admission-time ordered, with stale replacements moving to the tail. One HoleFillDispatch thread checks due entries and stops at the first future deadline. It can bypass a due task whose writer lacks capacity, then waits until the earliest deadline or capacity retry. Runtime delay updates wake this dispatcher; tail arrivals that cannot advance its wait leave the timer alone. Waiting releases the mutex and preserves fragment merging. Complete blocks still enter the asynchronous cache writer directly.

The queue accounts one full cache-block buffer per queued or active entry and is bounded by hole_fill_max_pending_bytes_per_be=4 GiB. When full, the oldest queued task is evicted. If every retained task is already active, new cache-only work is rejected. The query read result is unaffected in either case.

Block workers, dynamically resized by hole_fill_workers_per_be=32, consume ready tasks. The total of dispatched and executing tasks is bounded by that worker count, so excess entries stay in the mergeable queue. Dispatch moves existing list nodes into the handoff queue. Shrinking lets already-dispatched work drain before admitting more. Writer-capacity accounting includes the handoff, and final write submission still uses spare-capacity-only admission. Active-task metrics include both dispatched and executing work.

Hole-fill algorithm

For one partial cache block, HoleFillPlanner unions the covered fragments, takes their complement inside that block, and applies the same three-limit merge algorithm to the missing intervals. Hole ranges never cross a 1 MiB block boundary. A single merged range is read directly on the block worker, avoiding task submission and wakeup overhead. Multiple ranges are submitted concurrently to the BE-wide HoleFillRemoteReadPool, controlled by mutable hole_fill_remote_read_threads_per_be=64. This pool limit applies only to multi-range reads; single-range reads also run on the block workers.

Each block worker creates and reuses one concurrent token for its lifetime. Before writeback or failure cleanup it joins all accepted GETs, including when a submission or read fails. Shutdown stops admission, joins the dispatcher and discards queued/undelivered work without its GETs, then joins executing block workers and releases their tokens before shutting down the remote-read pool. Block-worker and asynchronous cache-writer pools start with their configured worker counts; the remote-read pool retains at least one thread.

The hole-fill limits are tighter because crossing already covered bytes consumes background bandwidth without reducing the foreground query latency.

Configuration Default Meaning
hole_fill_max_gap_bytes 32 KiB Largest already-covered gap crossed while combining adjacent holes.
hole_fill_max_range_bytes 1 MiB Largest block-local hole-fill GET.
hole_fill_max_read_amplification_ratio 1.0 Largest ratio of physical GET bytes to missing bytes; the default forbids rereading covered gaps.

Example: all offsets below are KiB within one 1 MiB block.

  • Covered fragments: [0, 256), [384, 416), [544, 1024)
  • Complement: hole1 [256, 384), hole2 [416, 544)
  • The holes are separated by 32 KiB of covered data. Merging would read 288 KiB for 256 KiB of missing data (1.125 amplification), exceeding the default limit of 1.0. Submit two parallel GETs, [256, 384) and [416, 544): 256 KiB total, with no covered bytes reread.
flowchart LR
    C1["covered 0 to 256 KiB"] --> H1["hole1 256 to 384 KiB"]
    H1 --> GAP["covered gap 384 to 416 KiB"]
    GAP --> H2["hole2 416 to 544 KiB"]
    H2 --> C2["covered 544 to 1024 KiB"]
    H1 --> GET1["GET1: 256 to 384 KiB"]
    H2 --> GET2["GET2: 416 to 544 KiB"]
    GET1 --> JOIN["Join both GETs"]
    GET2 --> JOIN
    JOIN --> BLOCK["complete block buffer"]
    BLOCK --> WRITER["existing async cache writer"]
Loading

Failure and compatibility boundaries

  • The scheduler and hole-fill manager follow the Cloud BE lifetime. New query read-ahead paths require both enable_query_read_ahead=true and enable_async_file_cache_write=true; runtime updates affect newly initialized reads. Disabling asynchronous cache writes retains the original query reader and cache-filling path.
  • The legacy segment File Cache prefetcher is skipped only after the new context is successfully created, avoiding duplicate prefetch work.
  • Planning failure or a completely rejected submission releases the planned pages for the original reader. Partial budget admission registers only accepted ranges and completes skipped pages for synchronous fallback; only admitted, consumed buffers trigger writeback.
  • A range read failure or cancellation retires its window entries and falls back when PageIO requests the page.
  • If either enable switch closes during row-store point-read initialization, reuse the loaded segment with the original synchronous iterator.
  • A checksum or decode failure on prefetched bytes evicts the suspect Page Cache entry and retries through the original reader.
  • File Cache epoch changes, queue pressure, hole-read failure, and cache-writer rejection discard only best-effort writeback work.
  • Variant-specific read-ahead forwarding remains outside this PR; existing Variant behavior is preserved.

Configuration defaults

Configuration Default Meaning
enable_query_read_ahead false Dynamically enable cloud query range read-ahead and consumed-range writeback for newly initialized reads; also requires enable_async_file_cache_write=true.
read_ahead_max_bytes_per_query 256 MiB Maximum resident range-buffer bytes retained by one query.
read_ahead_max_bytes_per_be 1 GiB Maximum resident range-buffer bytes retained across the BE.
read_ahead_eager_window_bytes 4 MiB Compressed-page bytes selected for each eager-column window; its second selected page triggers the next window.
read_ahead_lazy_window_bytes 1 MiB Compressed-page bytes selected for each dependency-delayed column window, using the same trigger rule.
read_ahead_max_gap_bytes 64 KiB Foreground range-merge gap limit.
read_ahead_max_range_bytes 2 MiB Foreground merged/block-completed range limit.
read_ahead_max_read_amplification_ratio 1.0 Foreground merge amplification limit.
read_ahead_block_fill_min_coverage 1.0 Minimum requested-page coverage for foreground cache-block completion.
hole_fill_max_gap_bytes 32 KiB Background block-local hole-merge gap limit.
hole_fill_max_range_bytes 1 MiB Background block-local GET limit.
hole_fill_max_read_amplification_ratio 1.0 Background hole-fill amplification limit; covered gaps remain unread by default.
hole_fill_max_pending_bytes_per_be 4 GiB BE-wide memory bound for queued and active partial blocks.
hole_fill_merge_delay_ms 2000 ms Mutable minimum time from successful admission before a queued partial block can activate; 0 disables it. Merges keep the original timestamp.
hole_fill_workers_per_be 32 Dynamically adjustable block workers for planning, single-range reads, joining multi-range reads, and writeback.
hole_fill_remote_read_threads_per_be 64 Dynamically adjustable shared pool size for parallel multi-range GETs; single-range reads execute on block workers outside this limit.

Read IO diagnostics

enable_read_io_trace=false dynamically enables actual S3 GET intervals/outcomes, range/task provenance, read-only cache/inflight coverage snapshots and typed timing events. Records are batched in memory and flushed by a dedicated writer to standalone JSONL files; they do not go to be.INFO and have no cumulative event cap. Normal and fast shutdown drain submitted records.

Configuration Default Meaning
enable_read_io_trace false Mutable diagnostic switch; leave disabled for performance measurements
read_io_trace_dir empty BE log directory's read_io_trace/ subdirectory, unless explicitly set
read_io_trace_flush_bytes 1 MiB Memory-batch flush threshold
read_io_trace_flush_interval_ms 1000 Periodic flush interval

doris_read_io_trace_record_time_ns exposes diagnostic overhead. The implementation records consumed-range writeback, queued/active fragments and dispatcher scan timing. Profile separates complete/partial foreground submission even with tracing disabled. The offline analyzer under tools/read_io_trace is no longer included.

S3 upload-buffer configuration updates now enforce the existing File Cache startup alignment constraint: when File Cache is enabled, the buffer must cover at least one cache block and be an exact multiple of its size. Invalid updates return INVALID_ARGUMENT and retain the previous value. Cache probing, read-ahead and hole-fill mechanisms are preserved.

Writeback deduplication also handles historical non-aligned cache blocks restored from disk. It checks actual contiguous DOWNLOADED/DOWNLOADING intervals, retaining the inflight shortcut; the existing grid-only probe(), read-ahead windows and hole-fill scheduling are unchanged.

Commit structure

The branch is based on upstream master fc1b82283cb and contains 21 commits: 18 components, one unity-build fix, one S3 configuration validation fix, and one restored-cache coverage fix. Shared BE diagnostics enter once as component 6; commit 7 scopes filesystem error-code imports for no-PCH builds. The RLE reverse-reach baseline increases only by the feature's six translation-unit users (828 to 834; measured reach: 910 to 916 TUs); existing global expression/reader header dependencies are retained. Hole-fill and consumed-range follow-ups remain in components 15, 17 and 18.

# Component Commit
1 Range coalescer 7654cf77cd1
2 Cache-aware planner and shared metrics 90cf5b6ce61
3 Asynchronous range scheduler 85894d0c6ed
4 Exact no-write cached reads c64e719ee91
5 Query runtime ownership 563b5ba7aac
6 Shared BE read IO tracing e2a4012ad85
7 Scope filesystem error-code imports for unity builds 8a08ae19993
8 Page-driven column windows 4271da9a80e
9 Segment coordination / PageIO e7dbfa1d450
10 Scalar column-reader integration 00fa56b67a2
11 Scalar scan initialization and legacy complex-column routing 8190e0f7d49
12 Complete-block handoff and admission d03f2742018
13 Cache-block range splitting a5477002892
14 Hole-fill planner 7cfe76cbbd3
15 Partial-block writeback manager a7e38995a3c
16 Hole-fill runtime wiring 999f5c673a1
17 Consumed-range writeback 6fd7bb6550a
18 Query writeback wiring 1ae1272fe26
19 Point queries and exact-row helpers fd6df2c335c
20 Validate S3 upload-buffer alignment on config updates a881473fa15
21 Handle restored irregular cache blocks in writeback deduplication b237ebcd675

Performance benchmarks

Time is FE Profile Total; S3 bytes include background I/O through drain.

Hardware
Item FE BE
Nodes 1 1
Instance ecs.g9i.xlarge ecs.g9i.8xlarge
CPU Intel Xeon 6982P-C Intel Xeon 6982P-C
vCPU / RAM 4 / 16 GiB 32 / 128 GiB
System disk 200 GiB 200 GiB
Data disks 1 × 300 GiB 4 × 800 GiB ESSD PL2
Test settings

Baseline is a cold read with asynchronous cache writes and page read-ahead disabled. Enabled is a cold read with asynchronous cache writes and page read-ahead/hole filling enabled. Local File Cache reads from warmed disk File Cache with both optimizations disabled and the OS page cache cleared.

Setting Baseline Enabled Local File Cache
enable_async_file_cache_write false true false
enable_query_read_ahead false true false
File Cache Empty Empty Warm
BE before each query Restarted Restarted Restarted
OS page cache Not explicitly dropped Not explicitly dropped Dropped
Key configuration Enabled group
read_ahead_eager_window_bytes / read_ahead_lazy_window_bytes 4 MiB / 1 MiB
read_ahead_max_gap_bytes / read_ahead_max_range_bytes 64 KiB / 2 MiB
read_ahead_max_read_amplification_ratio / read_ahead_block_fill_min_coverage 1 / 1
read_ahead_max_bytes_per_query / read_ahead_max_bytes_per_be 256 MiB / 1 GiB
hole_fill_merge_delay_ms 2000 ms
hole_fill_max_pending_bytes_per_be 4 GiB
hole_fill_max_gap_bytes / hole_fill_max_range_bytes 32 KiB / 1 MiB
hole_fill_max_read_amplification_ratio 1
hole_fill_workers_per_be / hole_fill_remote_read_threads_per_be 32 / 64
async_file_cache_write_workers_per_disk / async_file_cache_write_max_pending_bytes 16 / -1 (automatic)
segment_prefetch_thread_pool_thread_num_min / segment_prefetch_thread_pool_thread_num_max 32 / 2000
file_cache_each_block_size 1 MiB
disable_storage_page_cache / storage_page_cache_limit false / 20%
enable_query_segment_file_cache_prefetch / enable_compaction_segment_file_cache_prefetch false / false
enable_read_io_trace false
enable_sql_cache / enable_query_cache false / false
parallel_pipeline_task_num 0 (automatic)
Summary
Benchmark Queries Baseline (s) Enabled (s) Local File Cache (s) Baseline / Enabled
SSB Flat SF1000 13 767.878 498.556 159.685 1.54×
TPCH SF1000 22 2405.090 1224.448 542.209 1.96×
SSB SF1000 13 1645.817 597.873 298.043 2.75×
Benchmark Baseline S3 (GiB) Enabled S3 (GiB) Change Local File Cache S3 (GiB)
SSB Flat SF1000 255.118 264.763 +3.78% 0.000
TPCH SF1000 859.982 908.671 +5.66% 0.000
SSB SF1000 457.434 467.413 +2.18% 0.000
BE CPU and memory during S3 cold reads

The table compares Baseline (both optimizations off) with Enabled (Async Write + Read-Ahead, 256 MiB per query and 1 GiB per BE). Both groups started each query with an empty File Cache after a BE restart. Average BE CPU is in logical cores. CPU time is estimated from one-second BE process samples during the FE query window, using query pairs with continuous samples and at least 3 s in both runs. The RSS column reports the median of per-query sampled BE process peaks, including short queries.

Benchmark CPU query pairs Average BE CPU, Baseline → Enabled Estimated BE CPU time change Median peak BE RSS, Baseline → Enabled
SSB Flat SF1000 12 / 13 2.14 cores → 3.44 cores +4.97% 23.941 GiB → 25.755 GiB
TPC-H SF1000 22 / 22 3.08 cores → 6.83 cores +13.42% 26.594 GiB → 27.810 GiB
SSB SF1000 13 / 13 1.70 cores → 5.13 cores +11.99% 24.867 GiB → 27.668 GiB

The SSB Flat groups used different builds. On the same build and cluster, SSB q4.1 went from 236.000 s to 99.000 s, with estimated BE CPU time rising from 513.43 core-seconds to 590.46 core-seconds and peak RSS rising from 25.382 GiB to 28.041 GiB.

BE network and File Cache disk I/O

Values are Baseline → Enabled under the S3 cold-read settings above. We sampled BE eth0 and the four File Cache devices once per second during the FE query window. Host RX includes all interface traffic. The S3 totals above include background I/O through drain.

Benchmark Mean BE RX Cache disk query pairs Four-disk writes Median write await
SSB Flat SF1000 347.9 MiB/s → 551.6 MiB/s 13 / 13 221.567 GiB → 186.967 GiB 4.738 ms → 10.533 ms
TPC-H SF1000 378.5 MiB/s → 781.3 MiB/s 21 / 22 666.345 GiB → 617.876 GiB 5.759 ms → 10.547 ms
SSB SF1000 295.0 MiB/s → 813.0 MiB/s 12 / 13 358.653 GiB → 330.194 GiB 7.413 ms → 10.816 ms

Disk columns use pairs with complete samples on all four devices. For each query, iostat w_await is weighted by write operations before taking the median across queries. The write totals cover the query window, with the rest of the background drain outside that window.

The disk comparison omits SSB q2.2 and TPC-H q9 because one device sample was missing for each query. TPC-H q18 peaked at 2464.3 MiB/s RX with the optimization enabled.

The SSB Local File Cache group read 417.627 GiB from the four cache disks after clearing the OS page cache, with 0 B of S3 reads.

Per-query results

The dataset details below include CPU and RSS for every reported group. A dash in a CPU cell marks an FE query time under 3 s. RSS still shows the observed peak for those queries.

SSB Flat SF1000: per-query results
Query Baseline (s) Enabled (s) Local File Cache (s) Baseline S3 (GiB) Enabled S3 (GiB) Local File Cache S3 (GiB)
q1.1 16.024 12.499 2.706 4.009 4.056 0.000
q1.2 2.667 1.973 0.316 0.598 0.598 0.000
q1.3 7.367 5.359 1.687 1.560 2.028 0.000
q2.1 101.000 69.000 21.226 33.437 34.762 0.000
q2.2 88.000 66.000 18.662 30.061 30.700 0.000
q2.3 89.000 64.000 18.571 30.060 30.698 0.000
q3.1 81.000 58.410 17.170 27.608 29.360 0.000
q3.2 99.000 60.000 21.089 33.836 35.629 0.000
q3.3 79.000 49.984 16.107 27.837 28.730 0.000
q3.4 3.398 3.008 0.350 0.868 0.894 0.000
q4.1 126.000 70.000 26.058 41.061 42.393 0.000
q4.2 35.946 19.131 7.233 11.255 11.638 0.000
q4.3 39.476 19.192 8.510 12.929 13.277 0.000
Total 767.878 498.556 159.685 255.118 264.763 0.000

BE CPU (average / sampled peak / estimated CPU time)

Query Baseline Enabled Local File Cache
q1.1 1.78 cores / 2.71 cores / 30.28 core-s 2.21 cores / 3.52 cores / 28.76 core-s —
q1.2 — — —
q1.3 2.81 cores / 4.59 cores / 22.44 core-s 3.93 cores / 6.14 cores / 23.59 core-s —
q2.1 2.14 cores / 2.96 cores / 220.49 core-s 3.29 cores / 4.74 cores / 229.98 core-s 5.78 cores / 8.06 cores / 127.07 core-s
q2.2 2.07 cores / 2.78 cores / 185.89 core-s 2.82 cores / 4.19 cores / 191.96 core-s 4.96 cores / 7.18 cores / 99.12 core-s
q2.3 2.02 cores / 2.74 cores / 181.73 core-s 2.84 cores / 3.91 cores / 187.48 core-s 4.71 cores / 6.94 cores / 94.16 core-s
q3.1 2.60 cores / 3.83 cores / 215.75 core-s 3.83 cores / 5.64 cores / 226.08 core-s 8.08 cores / 11.02 cores / 145.53 core-s
q3.2 2.11 cores / 3.03 cores / 211.03 core-s 3.64 cores / 5.53 cores / 225.55 core-s 5.22 cores / 7.71 cores / 120.02 core-s
q3.3 1.85 cores / 2.47 cores / 147.66 core-s 3.06 cores / 4.37 cores / 155.95 core-s 3.82 cores / 5.45 cores / 65.01 core-s
q3.4 0.89 cores / 1.87 cores / 4.47 core-s 0.99 cores / 2.09 cores / 3.98 core-s —
q4.1 2.24 cores / 3.06 cores / 284.85 core-s 4.24 cores / 6.22 cores / 301.36 core-s 6.55 cores / 9.29 cores / 176.74 core-s
q4.2 2.32 cores / 3.32 cores / 85.98 core-s 4.62 cores / 6.95 cores / 92.50 core-s 6.94 cores / 9.23 cores / 55.51 core-s
q4.3 1.96 cores / 2.81 cores / 80.36 core-s 4.34 cores / 6.62 cores / 86.78 core-s 4.43 cores / 6.45 cores / 39.87 core-s

BE memory (sampled peak RSS)

Query Baseline Enabled Local File Cache
q1.1 10.888 GiB 11.311 GiB 10.501 GiB
q1.2 1.901 GiB 2.052 GiB 2.082 GiB
q1.3 6.886 GiB 7.216 GiB 6.903 GiB
q2.1 24.099 GiB 25.868 GiB 23.292 GiB
q2.2 24.130 GiB 25.643 GiB 23.264 GiB
q2.3 24.107 GiB 25.755 GiB 23.240 GiB
q3.1 23.911 GiB 25.451 GiB 23.453 GiB
q3.2 24.126 GiB 26.043 GiB 23.459 GiB
q3.3 24.354 GiB 26.108 GiB 23.239 GiB
q3.4 2.244 GiB 2.350 GiB 2.391 GiB
q4.1 24.134 GiB 26.218 GiB 23.420 GiB
q4.2 23.756 GiB 26.027 GiB 23.221 GiB
q4.3 23.941 GiB 26.669 GiB 23.061 GiB
TPCH SF1000: per-query results
Query Baseline (s) Enabled (s) Local File Cache (s) Baseline S3 (GiB) Enabled S3 (GiB) Local File Cache S3 (GiB)
q1 123.000 55.869 32.193 36.204 37.280 0.000
q2 24.222 21.871 3.841 5.608 6.921 0.000
q3 139.000 82.000 24.373 45.455 47.923 0.000
q4 36.428 28.795 26.139 46.518 51.488 0.000
q5 221.000 102.000 47.868 76.312 78.798 0.000
q6 36.233 25.955 3.803 12.173 12.173 0.000
q7 118.000 69.000 19.579 41.299 42.104 0.000
q8 372.000 112.000 61.000 96.768 100.420 0.000
q9 349.000 133.000 75.000 104.408 109.942 0.000
q10 74.000 40.539 28.894 49.902 54.172 0.000
q11 18.481 11.135 3.591 5.293 11.127 0.000
q12 24.542 19.707 7.801 25.759 27.577 0.000
q13 144.000 57.676 23.791 34.590 37.258 0.000
q14 25.459 20.422 2.096 8.149 8.514 0.000
q15 26.637 18.529 2.872 9.000 9.136 0.000
q16 12.482 11.942 4.602 3.684 4.618 0.000
q17 148.000 100.000 32.692 50.098 50.552 0.000
q18 52.004 50.487 38.521 52.222 56.472 0.000
q19 170.000 84.000 34.844 58.621 61.535 0.000
q20 49.088 26.180 10.383 21.127 21.652 0.000
q21 198.000 120.000 52.502 62.617 64.747 0.000
q22 43.514 33.341 5.824 14.177 14.263 0.000
Total 2405.090 1224.448 542.209 859.982 908.671 0.000

BE CPU (average / sampled peak / estimated CPU time)

Query Baseline Enabled Local File Cache
q1 5.19 cores / 7.10 cores / 648.56 core-s 13.98 cores / 16.30 cores / 796.96 core-s 20.08 cores / 21.12 cores / 662.73 core-s
q2 2.01 cores / 5.87 cores / 52.36 core-s 2.58 cores / 5.65 cores / 56.86 core-s 6.97 cores / 11.21 cores / 34.86 core-s
q3 2.24 cores / 3.82 cores / 313.77 core-s 4.34 cores / 5.86 cores / 360.38 core-s 7.57 cores / 10.68 cores / 196.92 core-s
q4 8.33 cores / 11.99 cores / 308.13 core-s 13.66 cores / 20.67 cores / 409.79 core-s 7.37 cores / 12.05 cores / 198.93 core-s
q5 2.11 cores / 3.24 cores / 469.83 core-s 5.22 cores / 7.46 cores / 537.24 core-s 5.28 cores / 6.55 cores / 258.83 core-s
q6 1.89 cores / 2.12 cores / 69.76 core-s 3.02 cores / 3.32 cores / 78.54 core-s 6.88 cores / 9.97 cores / 34.42 core-s
q7 2.02 cores / 4.22 cores / 242.93 core-s 4.04 cores / 8.03 cores / 282.78 core-s 6.17 cores / 9.24 cores / 123.30 core-s
q8 1.52 cores / 4.05 cores / 566.41 core-s 5.34 cores / 9.08 cores / 603.33 core-s 3.73 cores / 6.91 cores / 234.88 core-s
q9 2.63 cores / 16.33 cores / 922.28 core-s 7.58 cores / 13.94 cores / 1015.74 core-s 8.82 cores / 28.99 cores / 670.39 core-s
q10 5.49 cores / 16.42 cores / 411.53 core-s 10.82 cores / 18.90 cores / 443.76 core-s 7.88 cores / 15.70 cores / 236.33 core-s
q11 3.12 cores / 4.52 cores / 62.48 core-s 7.23 cores / 9.42 cores / 86.79 core-s 11.87 cores / 17.42 cores / 59.33 core-s
q12 7.11 cores / 9.58 cores / 177.64 core-s 9.53 cores / 14.06 cores / 200.23 core-s 10.37 cores / 14.24 cores / 93.34 core-s
q13 3.02 cores / 8.84 cores / 437.44 core-s 8.31 cores / 11.13 cores / 482.02 core-s 14.62 cores / 19.11 cores / 365.48 core-s
q14 1.76 cores / 4.42 cores / 47.48 core-s 2.62 cores / 6.34 cores / 54.94 core-s —
q15 2.57 cores / 9.99 cores / 72.00 core-s 4.18 cores / 9.32 cores / 79.38 core-s —
q16 5.84 cores / 10.38 cores / 81.70 core-s 7.70 cores / 11.75 cores / 92.34 core-s 18.39 cores / 28.21 cores / 91.93 core-s
q17 2.07 cores / 2.87 cores / 307.82 core-s 3.45 cores / 5.99 cores / 348.44 core-s 5.17 cores / 8.66 cores / 175.83 core-s
q18 20.35 cores / 31.93 cores / 1078.69 core-s 22.73 cores / 30.19 cores / 1181.91 core-s 20.80 cores / 31.86 cores / 832.00 core-s
q19 1.87 cores / 2.48 cores / 319.23 core-s 4.30 cores / 6.97 cores / 369.78 core-s 4.08 cores / 5.93 cores / 146.80 core-s
q20 2.83 cores / 13.49 cores / 141.46 core-s 5.77 cores / 18.70 cores / 161.69 core-s 6.13 cores / 11.18 cores / 73.58 core-s
q21 3.31 cores / 19.83 cores / 662.87 core-s 6.17 cores / 20.67 cores / 752.30 core-s 8.97 cores / 20.99 cores / 484.57 core-s
q22 2.39 cores / 4.77 cores / 107.40 core-s 3.32 cores / 4.78 cores / 112.96 core-s 9.46 cores / 13.55 cores / 66.23 core-s

BE memory (sampled peak RSS)

Query Baseline Enabled Local File Cache
q1 26.800 GiB 27.469 GiB 26.047 GiB
q2 17.551 GiB 18.529 GiB 17.219 GiB
q3 30.889 GiB 32.955 GiB 30.005 GiB
q4 27.508 GiB 30.401 GiB 26.135 GiB
q5 27.229 GiB 30.131 GiB 26.307 GiB
q6 24.662 GiB 25.444 GiB 23.350 GiB
q7 32.800 GiB 35.111 GiB 32.914 GiB
q8 26.910 GiB 30.090 GiB 25.604 GiB
q9 44.249 GiB 45.514 GiB 43.812 GiB
q10 36.697 GiB 39.464 GiB 35.222 GiB
q11 20.102 GiB 21.443 GiB 19.892 GiB
q12 26.387 GiB 28.151 GiB 24.855 GiB
q13 36.933 GiB 35.408 GiB 37.027 GiB
q14 7.761 GiB 8.188 GiB 8.239 GiB
q15 15.745 GiB 15.857 GiB 15.358 GiB
q16 22.232 GiB 22.337 GiB 23.118 GiB
q17 24.514 GiB 26.482 GiB 23.031 GiB
q18 89.566 GiB 85.358 GiB 78.565 GiB
q19 24.450 GiB 26.392 GiB 23.360 GiB
q20 25.100 GiB 27.119 GiB 23.961 GiB
q21 31.730 GiB 33.892 GiB 30.539 GiB
q22 14.312 GiB 14.781 GiB 13.152 GiB
SSB SF1000: per-query results
Query Baseline (s) Enabled (s) Local File Cache (s) Baseline S3 (GiB) Enabled S3 (GiB) Local File Cache S3 (GiB)
q1.1 38.378 12.701 3.472 5.216 5.272 0.000
q1.2 17.907 13.255 3.509 5.079 5.250 0.000
q1.3 32.784 11.881 3.523 5.079 5.250 0.000
q2.1 242.000 69.000 36.688 56.297 56.979 0.000
q2.2 272.000 69.000 36.757 56.297 56.909 0.000
q2.3 185.000 71.000 36.774 56.293 57.149 0.000
q3.1 158.000 64.000 31.572 48.839 49.388 0.000
q3.2 164.000 62.000 31.556 48.851 49.387 0.000
q3.3 152.000 65.000 30.527 48.768 49.931 0.000
q3.4 29.399 17.252 3.958 7.772 8.404 0.000
q4.1 236.000 99.000 53.401 80.008 83.789 0.000
q4.2 59.550 22.417 13.188 19.480 19.909 0.000
q4.3 58.799 21.367 13.118 19.456 19.795 0.000
Total 1645.817 597.873 298.043 457.434 467.413 0.000

BE CPU (average / sampled peak / estimated CPU time)

Query Baseline Enabled Local File Cache
q1.1 1.10 cores / 1.75 cores / 43.04 core-s 3.18 cores / 4.19 cores / 44.47 core-s 6.86 cores / 8.59 cores / 27.42 core-s
q1.2 1.96 cores / 2.72 cores / 37.28 core-s 2.85 cores / 3.71 cores / 39.86 core-s 4.52 cores / 7.14 cores / 22.61 core-s
q1.3 1.12 cores / 1.60 cores / 37.96 core-s 3.08 cores / 3.93 cores / 40.04 core-s 4.66 cores / 7.33 cores / 23.32 core-s
q2.1 1.34 cores / 2.01 cores / 325.96 core-s 5.06 cores / 5.87 cores / 358.93 core-s 4.07 cores / 5.27 cores / 154.54 core-s
q2.2 1.17 cores / 1.99 cores / 321.37 core-s 4.95 cores / 5.90 cores / 346.25 core-s 3.80 cores / 4.96 cores / 140.55 core-s
q2.3 1.64 cores / 2.15 cores / 305.64 core-s 4.72 cores / 5.76 cores / 340.10 core-s 3.53 cores / 4.60 cores / 134.08 core-s
q3.1 2.33 cores / 2.92 cores / 370.47 core-s 6.28 cores / 7.80 cores / 414.69 core-s 7.19 cores / 8.58 cores / 237.23 core-s
q3.2 1.78 cores / 2.29 cores / 295.08 core-s 5.12 cores / 6.53 cores / 328.00 core-s 4.50 cores / 5.72 cores / 148.46 core-s
q3.3 1.80 cores / 2.32 cores / 275.43 core-s 4.74 cores / 6.42 cores / 312.92 core-s 4.06 cores / 5.33 cores / 129.95 core-s
q3.4 1.45 cores / 2.08 cores / 45.06 core-s 2.87 cores / 5.46 cores / 51.63 core-s 5.23 cores / 5.60 cores / 20.94 core-s
q4.1 2.17 cores / 2.70 cores / 513.43 core-s 5.85 cores / 7.11 cores / 590.46 core-s 5.02 cores / 6.19 cores / 276.20 core-s
q4.2 2.22 cores / 2.91 cores / 135.23 core-s 6.58 cores / 8.67 cores / 157.82 core-s 5.28 cores / 6.39 cores / 73.88 core-s
q4.3 1.96 cores / 2.45 cores / 117.46 core-s 5.95 cores / 8.06 cores / 136.88 core-s 4.11 cores / 4.93 cores / 57.47 core-s

BE memory (sampled peak RSS)

Query Baseline Enabled Local File Cache
q1.1 16.230 GiB 17.388 GiB 15.615 GiB
q1.2 16.179 GiB 17.388 GiB 15.579 GiB
q1.3 16.169 GiB 17.383 GiB 15.597 GiB
q2.1 25.077 GiB 27.720 GiB 23.013 GiB
q2.2 25.068 GiB 27.703 GiB 22.971 GiB
q2.3 25.035 GiB 27.602 GiB 22.910 GiB
q3.1 25.173 GiB 27.886 GiB 23.276 GiB
q3.2 24.867 GiB 27.668 GiB 22.898 GiB
q3.3 25.026 GiB 27.295 GiB 22.828 GiB
q3.4 13.631 GiB 14.712 GiB 12.531 GiB
q4.1 25.382 GiB 28.041 GiB 23.444 GiB
q4.2 24.771 GiB 27.957 GiB 22.985 GiB
q4.3 24.715 GiB 27.850 GiB 22.946 GiB

Release note

Add an opt-in range read-ahead and partial File Cache block hole-fill pipeline for cloud query data pages. The feature is disabled by default.

Check List (For Author)

  • Test:
    • Performance benchmark results are summarized above.
    • Rebase validation: 329 tests / 24 suites passed with run-be-ut.sh --run -j100 (ASAN/PCH), covering range planning/scheduling, page-position windows, scalar/legacy complex-column reads, count-emission fast paths, point queries, cache/inflight reuse, writeback, hole filling, tracing and configuration validation. A direct Release/PCH BE build passed with build.sh --be -j100. Full-checkout clang-format 16, build hygiene, gate self-tests and diff/range-diff checks passed; [fix](be) Fix Phase 2 no-PCH compilation (compile only) #67579 has the same final source tree. No cluster or clang-tidy run.
    • Regression test
    • Unit Test
    • Manual test
    • No need to test or manual test.
  • Behavior changed:
    • No.
    • Yes. When explicitly enabled, eligible cloud query data pages use bounded asynchronous range read-ahead and consumed-range File Cache writeback. The feature remains disabled by default; opt-in reads use the defaults listed above.
  • Does this need documentation?
    • No.
    • Yes. Public configuration documentation remains pending; this description lists the current configuration.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@bobhan1
bobhan1 force-pushed the feature/read-ahead-io-coalescing-phase2 branch from 7048235 to a5d11e4 Compare September 1, 2026 03:50
@bobhan1 bobhan1 changed the title [feature](be) Add range read-ahead for cold queries [feature](be) Add range read-ahead and cache block hole filling Sep 1, 2026
@bobhan1
bobhan1 force-pushed the feature/read-ahead-io-coalescing-phase2 branch from a5d11e4 to aeb3178 Compare September 1, 2026 07:35
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Read-ahead and background hole filling both need to combine sparse logical ranges into bounded physical reads. Add a pure one-pass coalescer whose contract requires validated options and sorted, disjoint, non-empty inputs. Each merge checks the maximum gap, resulting span, and cumulative read amplification. The range-size limit controls merging; an indivisible input larger than the limit remains one read.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh -j100 --run '--filter=FileRangeCoalescerTest.*'
- Behavior changed: No; this commit adds an internal planning primitive.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Read-ahead needs to coalesce sorted page ranges and opportunistically complete highly covered File Cache blocks without repeatedly rebuilding the whole plan. Add a planner that considers only the two boundary blocks of each base range, orders candidates by additional bytes, updates affected interval components, and produces compact input-to-buffer mappings. Its complexity is O(n log n), and an oversized atomic input remains standalone without preventing unrelated blocks from being completed.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh -j100 --run '--filter=FileRangePlannerTest.*'
- Behavior changed: No; this commit adds an internal range planner.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

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. Validate watermarks through the option type and use checked rowid conversion when querying Roaring ranks.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh -j100 --run '--filter=ColumnReadAheadTest.*'
- Behavior changed: No; this commit adds an internal read-ahead window.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Column byte windows need physical ordinal-index ranges and a safe page-decoding lifecycle. 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 -j100 --run '--filter=ColumnReaderTest.FileColumnIteratorPreparesCompressedPageByteWindow:ColumnReaderTest.FileColumnIteratorConsumesSubmittedReadAheadPage:ColumnReaderTest.FileColumnIteratorFallsBackAfterReadAheadChecksumFailure:SegmentReadAheadTest.*:ColumnReadAheadTest.*'
- Behavior changed: No; this commit connects internal components before production query integration.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Partial query ranges can cover unordered and overlapping fragments inside one File Cache block. Add a block-local planner that unions existing coverage, takes its complement to produce sorted holes, and applies the dedicated three-parameter coalescing policy without crossing the block boundary. The coalescing size limit controls combining holes; one mandatory hole may be larger and remains one remote read.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh -j100 --run '--filter=HoleFillPlannerTest.*'
- Behavior changed: No; this commit adds an internal hole-fill planner.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

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 manager that merges queued fragments, deduplicates active blocks, evicts the oldest queued block under pressure, waits for Phase 1 spare capacity, fills block-local holes through a dedicated remote-read pool, and hands completed buffers to Phase 1 without another copy. The hole-fill range limit governs coalescing and may be smaller than one cache block because a mandatory single hole is still readable.

### 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
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

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 PartialBlockWritebackOptions::validate(), including independent block size and hole-coalescing range limits.

### 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 connected to query writeback later.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Segment read-ahead must establish the range planner contract while preserving shared physical pages and cache invalidation state. Sort cache-miss pages at the segment boundary, collapse exact duplicate ranges while retaining all column owners, submit compact FileRange plans, and capture a per-submission consumer immediately before asynchronous IO. Only accepted and consumed ranges retain and invoke that consumer.

### 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; production writeback is connected by a later commit.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

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. Validate watermarks through the option type and use checked rowid conversion when querying Roaring ranks.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh -j100 --run '--filter=ColumnReadAheadTest.*'
- Behavior changed: No; this commit adds an internal read-ahead window.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Column byte windows need physical ordinal-index ranges and a safe page-decoding lifecycle. 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 -j100 --run '--filter=ColumnReaderTest.FileColumnIteratorPreparesCompressedPageByteWindow:ColumnReaderTest.FileColumnIteratorConsumesSubmittedReadAheadPage:ColumnReaderTest.FileColumnIteratorFallsBackAfterReadAheadChecksumFailure:SegmentReadAheadTest.*:ColumnReadAheadTest.*'
- Behavior changed: No; this commit connects internal components before production query integration.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Partial query ranges can cover unordered and overlapping fragments inside one File Cache block. Add a block-local planner that unions existing coverage, takes its complement to produce sorted holes, and applies the dedicated three-parameter coalescing policy without crossing the block boundary. The coalescing size limit controls combining holes; one mandatory hole may be larger and remains one remote read.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh -j100 --run '--filter=HoleFillPlannerTest.*'
- Behavior changed: No; this commit adds an internal hole-fill planner.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

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 manager that merges queued fragments, deduplicates active blocks, evicts the oldest queued block under pressure, waits for Phase 1 spare capacity, fills block-local holes through a dedicated remote-read pool, and hands completed buffers to Phase 1 without another copy. The hole-fill range limit governs coalescing and may be smaller than one cache block because a mandatory single hole is still readable.

### 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
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

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 PartialBlockWritebackOptions::validate(), including independent block size and hole-coalescing range limits.

### 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 connected to query writeback later.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Segment read-ahead must establish the range planner contract while preserving shared physical pages and cache invalidation state. Sort cache-miss pages at the segment boundary, collapse exact duplicate ranges while retaining all column owners, submit compact FileRange plans, and capture a per-submission consumer immediately before asynchronous IO. Only accepted and consumed ranges retain and invoke that consumer.

### 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; production writeback is connected by a later commit.
- Does this need documentation: No
@bobhan1
bobhan1 force-pushed the feature/read-ahead-io-coalescing-phase2 branch 2 times, most recently from 55ff2ba to 64fe07a Compare September 1, 2026 09:17
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Asynchronous read-ahead ranges must preserve their coalesced byte boundaries and avoid populating File Cache before consumption is known. Route per-read NO_WRITE requests through CachedRemoteFileReader's existing remote-only-on-cache-miss path: a fully cached range is served locally, while incomplete coverage issues one exact remote read for the entire planned range without creating cache cells, waiting for downloading blocks, reading from peers, or submitting cache writes. Keep cache-write mode resolution in the main reader dispatch because it selects among all read paths rather than belonging to the asynchronous cache-write implementation.

### Release note

None

### Check List (For Author)

- Test: Unit Test and BE build
    - ./run-be-ut.sh -j100 --run '--filter=AsyncCachedRemoteFileReaderTest.exact_no_write_*:BlockFileCacheTest.cached_remote_file_reader_remote_only_on_miss:BlockFileCacheTest.get_downloaded_blocks_if_fully_covered_is_read_only:BlockFileCacheTest.cache_write_mode_is_resolved_for_each_read_context:AsyncFileRangeReaderTest.*:PartialBlockWritebackManagerTest.*' (29 passed)
    - ./build.sh --be -j100
- Behavior changed: Yes; no-write asynchronous ranges reuse full cache hits and otherwise issue one exact remote read for the planned range.
- Does this need documentation: Yes; covered by the Phase 2 design document.
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

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. Validate watermarks through the option type and use checked rowid conversion when querying Roaring ranks.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh -j100 --run '--filter=ColumnReadAheadTest.*'
- Behavior changed: No; this commit adds an internal read-ahead window.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Column byte windows need physical ordinal-index ranges and a safe page-decoding lifecycle. 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 -j100 --run '--filter=ColumnReaderTest.FileColumnIteratorPreparesCompressedPageByteWindow:ColumnReaderTest.FileColumnIteratorConsumesSubmittedReadAheadPage:ColumnReaderTest.FileColumnIteratorFallsBackAfterReadAheadChecksumFailure:SegmentReadAheadTest.*:ColumnReadAheadTest.*'
- Behavior changed: No; this commit connects internal components before production query integration.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Partial query ranges can cover unordered and overlapping fragments inside one File Cache block. Add a block-local planner that unions existing coverage, takes its complement to produce sorted holes, and applies the dedicated three-parameter coalescing policy without crossing the block boundary. The coalescing size limit controls combining holes; one mandatory hole may be larger and remains one remote read.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh -j100 --run '--filter=HoleFillPlannerTest.*'
- Behavior changed: No; this commit adds an internal hole-fill planner.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

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 manager that merges queued fragments, deduplicates active blocks, evicts the oldest queued block under pressure, waits for Phase 1 spare capacity, fills block-local holes through a dedicated remote-read pool, and hands completed buffers to Phase 1 without another copy. The hole-fill range limit governs coalescing and may be smaller than one cache block because a mandatory single hole is still readable.

### 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
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

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 PartialBlockWritebackOptions::validate(), including independent block size and hole-coalescing range limits.

### 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 connected to query writeback later.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 1, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Segment read-ahead must establish the range planner contract while preserving shared physical pages and cache invalidation state. Sort cache-miss pages at the segment boundary, collapse exact duplicate ranges while retaining all column owners, submit compact FileRange plans, and capture a per-submission consumer immediately before asynchronous IO. Only accepted and consumed ranges retain and invoke that consumer.

### 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; production writeback is connected by a later commit.
- Does this need documentation: No
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 2, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: The initial query read-ahead implementation spread execution, page lifecycle, and submission state across several adapters and state machines. Rename the BE-global component to FileRangeReadScheduler, flatten its ownership and synchronization while executing work on SegmentPrefetchThreadPool, reduce column windows to pending-byte accounting, and make SegmentReadAhead the single cache-probe, coalescing, and submission boundary. SegmentReadAheadFileReader now retires pages at successful buffer reads, and background hole fill reuses its already-owned IO context.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - 61 focused read-ahead and writeback tests with ./run-be-ut.sh -j100
    - 15 CachedRemoteFileReader and file-cache tests with ./run-be-ut.sh -j100
    - ./build.sh --be -j100 (Release)
    - build-support/check-format.sh
    - build-support/check-build-hygiene.sh
- Behavior changed: Yes; successful prefetched bytes are consumed at the exact PageIO read, while later page decode failures retry the original reader.
- Does this need documentation: No
@bobhan1
bobhan1 force-pushed the feature/read-ahead-io-coalescing-phase2 branch from 29d8551 to 6954c33 Compare September 2, 2026 05:28
bobhan1 added a commit to bobhan1/doris that referenced this pull request Sep 2, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292

Problem Summary: Read-ahead and background hole filling both need to combine sparse logical ranges into bounded physical reads. Add a pure one-pass coalescer whose contract requires validated options and sorted, disjoint, non-empty inputs. Each merge checks the maximum gap, resulting span, and cumulative read amplification. The range-size limit controls merging; an indivisible input larger than the limit remains one read.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh -j100 --run '--filter=FileRangeCoalescerTest.*'
- Behavior changed: No; this commit adds an internal planning primitive.
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: Part of apache#67611

Related PR: apache#67579, apache#67292

Problem Summary: Prepare eligible top-level scalar pages once after pruning and advance scan windows at the data-page read entry before Page Cache lookup. Scalar point reads retain exact-row preparation. Report initialization and page-advancement time, then let ColumnReadAheadPlan accumulate its maintenance sub-timers. Preserve existing page decoding, cache hits and synchronous fallback. ARRAY/MAP/STRUCT, including offsets, null maps and nested children, keep the upstream reader implementation. Their children may have independent predicate/lazy ordinals, so this component adds no dependent-item preparation or cursor-equality assertion. Cover the nested STRUCT/MAP lazy-read crash and complex-column preparation boundary.

### Release note

Connect top-level scalar data-page reads to byte-window read-ahead.

### Check List (For Author)

- Test: Unit Test — 134 tests from 4 suites passed on the integrated apache#67579 tree with ./run-be-ut.sh -j100 --run (ASAN/PCH, no clean). Repository clang-format 16, header hygiene and git diff --check passed. No cluster or clang-tidy run. This intermediate component commit was not built separately.
- Behavior changed: Yes — introduces opt-in scalar read-ahead while preserving complex-column behavior.
- Does this need documentation: Yes — scope and synchronization are recorded in apache#67611 and the related PR descriptions.
### What problem does this PR solve?

Issue Number: Part of apache#67611

Related PR: apache#67579, apache#67292

Problem Summary: Initialize eager and lazy top-level scalar windows together after segment pruning instead of rebuilding rowid plans every scanner batch. Later physical page reads advance each window. Select the reader per column: eligible scalars use the read-ahead wrapper; complex and extracted variant columns retain the original reader and segment prefetchers. Keep reverse-scan behavior and configure compressed-byte windows, coalescing limits and optional foreground block completion. Test mixed scalar/complex planning and legacy prefetch initialization with the new context enabled or absent.

### Release note

Opt-in regular segment scans use page-driven read-ahead across scanner batches for top-level scalar columns.

### Check List (For Author)

- Test: Unit Test — 134 tests from 4 suites passed on the integrated apache#67579 tree with ./run-be-ut.sh -j100 --run (ASAN/PCH, no clean). Repository clang-format 16, header hygiene and git diff --check passed. No cluster or clang-tidy run. This intermediate component commit was not built separately.
- Behavior changed: Yes — introduces opt-in scalar scan initialization with a 4 MiB eager window and a 1 MiB lazy window; complex columns retain their existing read and prefetch path.
- Does this need documentation: Yes — scope and synchronization are recorded in apache#67611 and the related PR descriptions.
### What problem does this PR solve?

Issue Number: apache#67611 (tracking)

Related PR: apache#67292, apache#67579, apache#67612

Problem Summary: Expose complete-block submission, tracked owned-buffer handoff, and low-priority spare-capacity admission together as one cache-writer component. Reuse existing epoch checks and asynchronous disk writes. Background hole filling can pass a completed block without another data copy while normal foreground cache writes keep priority.

### Release note

Internal complete-block and zero-copy handoff for best-effort cache population.

### Check List (For Author)

- Test: Unit Test — component coverage is included with this change. The integrated source snapshot d5c427f passed 198 ASAN tests from 11 suites via run-be-ut.sh -j100. This individual intermediate commit was not built separately; target-branch validation is recorded on the PR. Repository clang-format v16, build hygiene and diff checks passed on the integrated tree. No cluster or clang-tidy run.
- Behavior changed: Yes — introduces the opt-in component; this synchronization preserves existing defaults and IO/writeback semantics.
- Does this need documentation: Yes — component scope and current behavior are tracked in apache#67611 and the related PR descriptions.
### 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: apache#67292

Problem Summary: Partial query ranges can cover unordered and overlapping fragments inside one File Cache block. Add a block-local planner that unions existing coverage, takes its complement to produce sorted holes, and applies the dedicated three-parameter coalescing policy without crossing the block boundary. The coalescing size limit controls combining holes; one mandatory hole may be larger and remains one remote read.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ./run-be-ut.sh -j100 --run '--filter=HoleFillPlannerTest.*'
- Behavior changed: No; this commit adds an internal hole-fill planner.
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: Part of apache#67611

Related PR: apache#67292, apache#67579

Problem Summary: Partial cache blocks need bounded background completion without stalling query reads. Keep queued fragments mergeable under a configurable aggregation delay and a BE byte budget. One HoleFillDispatch thread owns admission-time-ordered deadline and writer-capacity checks, stops at the first future deadline, bypasses busy writers for other due tasks, and hands off at most the configured block-worker slots. Workers execute ready tasks with reusable remote-read tokens: one hole range is read inline and multiple ranges use the shared remote pool. Retain cache/inflight reuse, epoch checks, eviction, dynamic worker/pool/delay updates, and complete-block handoff. Trace fragment lifecycles and submission/queue timings with serialization outside the queue mutex. Cancel queued and undelivered work at shutdown and drain executing reads before destroying the remote pool.

### Release note

Add opt-in background cache-block hole filling with queued-fragment aggregation, dedicated dispatch, bounded worker handoff and parallel hole reads.

### Check List (For Author)

- Test: Unit Test
    - Component tests are included with the implementation; rebase and synchronization validation is recorded in the related PRs and tracking issue.
    - Repository clang-format 16, header hygiene and git diff --check passed before folding.
    - No cluster, external-object-store benchmark or clang-tidy run during synchronization.
- Behavior changed: Yes, includes final component behavior and applicable diagnostic instrumentation; default enable switches remain off.
- Does this need documentation: Yes, tools/read_io_trace/README.md and related PR/tracking descriptions.
### What problem does this PR solve?

Issue Number: apache#67611 (tracking)

Related PR: apache#67292

Problem Summary: Background partial-block writeback needs production defaults and a BE-owned lifetime bounded by the File Cache lifecycle. Add validated hole-fill configuration, create the manager after File Cache initialization, and stop it before File Cache teardown. Configure a 4 GiB BE-wide queued/active buffer budget and a 1.0 hole-read amplification limit. Use hole_fill_workers_per_be for block workers (default 32, range 1 through 128) and hole_fill_remote_read_threads_per_be for the shared multi-range remote-read pool (default 64, positive values). Both settings resize their pools at runtime without rebuilding the manager. Single-range reads execute on block workers outside the remote-read pool limit. Add mutable hole_fill_merge_delay_ms with a 2000 ms default and nonnegative validation. Positive values delay activation from the first admission timestamp; dynamic updates apply to already queued tasks without resetting that timestamp.

### Release note

Support runtime resizing of BE-wide block workers and the shared multi-range remote-read pool. Configure minimum queued-fragment aggregation time through hole_fill_merge_delay_ms; its 2000 ms default keeps queued fragments mergeable before dispatch, and setting it to 0 disables deliberate waiting. Limit queued/active partial-block buffers to 4 GiB per BE and use a 1.0 background read-amplification limit.

### Check List (For Author)

- Test: Unit Test
    - Existing PartialBlockWritebackOptionsTest coverage via run-be-ut.sh -j100 with ASAN/PCH; no new tests for the default-value update.
- Behavior changed: Yes; configure background hole filling, resize its pools at runtime, and adjust queued partial-block aggregation delay.
- Does this need documentation: Yes; configuration and execution contracts are tracked in apache#67611 and apache#67292.
### What problem does this PR solve?

Issue Number: Part of apache#67611

Related PR: apache#67292, apache#67579

Problem Summary: Consumed read-ahead buffers can span complete and partial cache blocks. Split each range, submit complete blocks to the existing asynchronous cache writer, and enqueue partial fragments for background completion under the captured invalidation epoch. Preserve unified cache/inflight reuse and mixed outcomes. Carry the originating read identity into partial tasks and interval traces, and separate complete/partial foreground dispatch time in Profile counters. Read and persistence outcomes remain best effort.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - Component tests are included with the implementation; rebase and synchronization validation is recorded in the related PRs and tracking issue.
    - Repository clang-format 16, header hygiene and git diff --check passed before folding.
    - No cluster, external-object-store benchmark or clang-tidy run during synchronization.
- Behavior changed: Yes, includes final component behavior and applicable diagnostic instrumentation; default enable switches remain off.
- Does this need documentation: Yes, tools/read_io_trace/README.md and related PR/tracking descriptions.
### What problem does this PR solve?

Issue Number: Part of apache#67611

Related PR: apache#67292, apache#67579

Problem Summary: Centralize query read-ahead construction so regular scans and exact-row callers share range planning, byte windows, cache probes, scheduler ownership and writeback setup. Merge per-read IO statistics into caller accounting and publish each consumed range once. Submit writeback only for consumed ranges that read remote bytes, preserving no-write policy and avoiding redundant hole filling for File Cache or inflight hits. Carry the read identity through the consumed-range callback so offline traces can attribute background work to its foreground source.

Require both query read-ahead and asynchronous cache writes so disabling the writer preserves synchronous cache filling. Handle a switch changing during scan initialization through the original reader. Keep delayed hole-fill sources open with an aliasing shared_ptr that retains the cached wrapper; regression tests release both the foreground reader and writeback context before completing the queued fill.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - Component tests are included with the implementation; rebase and synchronization validation is recorded in the related PRs and tracking issue.
    - Repository clang-format 16, header hygiene and git diff --check passed before folding.
    - No cluster, external-object-store benchmark or clang-tidy run during synchronization.
- Behavior changed: Yes, includes final component behavior and applicable diagnostic instrumentation; default enable switches remain off.
- Does this need documentation: Yes, tools/read_io_trace/README.md and related PR/tracking descriptions.
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67579, apache#67292

Problem Summary: Short-circuit point queries read row-store data or missing column-store values through exact row IDs. Create one request-scoped range-read context, initialize the scalar row-store or missing top-level scalar iterators against the shared read-ahead reader, and submit the required pages before decoding. Complex columns and extracted variant paths retain Segment's original iterator/reader resolution. Preserve row-cache behavior, result padding, request-scoped constants and synchronous fallback. Append read-ahead Profile statistics to point-query diagnostics; BE cumulative counters retain unsampled totals. Include exact-row helpers and tests in this component. TopN second-stage integration remains identical to the master base and is outside this PR series.

Handle a dynamic switch closing after a point request creates its context: if create_for_query returns no read-ahead object, read row-store data synchronously through the already loaded segment. The deterministic row-store test closes either enable switch after segment resolution, verifies that no segment-loader call is made, and compares the result with the original reader.

Rebase compatibility: reuse the segment returned by primary-key lookup in both the read-ahead and synchronous row-store paths, preserving master's segment and acquired-rowset lifetime.

### Release note

Use query range read-ahead for scalar point-query data pages when enable_query_read_ahead and enable_async_file_cache_write are enabled.

### Check List (For Author)

- Test: Unit Test — pre-rebase integrated ASAN/PCH validation covered 225 tests from 10 suites through ./run-be-ut.sh --run -j100. Includes partial budget admission, original page offsets, cancellation, allocation rollback, executor rejection, consumed-range callbacks and row-store switch fallback. Individual intermediate commits are not built separately; target-branch validation is recorded on the PR. Clang-format 16, header hygiene and diff checks pass. No cluster or clang-tidy run.
- Behavior changed: Yes — adds scalar point-query prefetch without changing complex-column decoding, scheduling limits or query results.
- Does this need documentation: Yes — scope and synchronization are recorded in the related active PR descriptions.
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292, apache#67579

Problem Summary: File Cache initialization rejects an S3 upload buffer size that is smaller than a cache block or not a multiple of the block size. The buffer size is mutable, but runtime updates did not register the same validation, so a later update could violate the layout assumed by cached reads and hole-fill deduplication. Register the existing alignment constraint as a config validator and cover accepted updates, rejected updates with value rollback, invalid block sizes, and File Cache disabled behavior. Cache probing, read-ahead, and hole-fill algorithms remain unchanged.

### Release note

Reject S3 upload buffer configuration updates that violate File Cache block alignment while File Cache is enabled.

### Check List (For Author)

- Test: Unit Test — 66 tests from 4 suites pass with ./run-be-ut.sh --run -j100 (ASAN/PCH), including configuration validation and partial-block/async cache writeback. Direct Release/PCH BE build passes with ./build.sh --be -j100. Clang-format 16, build hygiene, and diff checks pass. No cluster or clang-tidy run.
- Behavior changed: Yes — invalid runtime S3 upload buffer updates return INVALID_ARGUMENT and retain the previous value; defaults and IO mechanisms are unchanged.
- Does this need documentation: No — applies the existing File Cache startup constraint to runtime configuration updates.
### What problem does this PR solve?

Issue Number: None

Related PR: apache#67292, apache#67579

Problem Summary: Older binaries could persist non-aligned File Cache blocks after a runtime S3 upload-buffer update. These intervals survive cache reopening even after the buffer setting is corrected. Partial-block writeback deduplication used a grid-only cache probe, so encountering a restored irregular interval could abort the BE. Check coverage using the actual cached intervals, accepting contiguous DOWNLOADED or DOWNLOADING blocks without allocating cache cells or changing LRU state. Keep the inflight shortcut and the existing grid probe unchanged.

### Release note

Avoid a BE abort during partial-block writeback when File Cache restores historical non-aligned blocks.

### Check List (For Author)

- Test: The restored-cache regression test reproduced SIGABRT before the fix. All 92 related tests in 5 suites passed with run-be-ut.sh --run -j100 (ASAN/PCH), including restored irregular cache gaps and mixed downloaded/downloading coverage. A direct Release/PCH BE build passed with build.sh --be -j100. Clang-format 16, build hygiene and git diff --check passed. No cluster or clang-tidy run.
- Behavior changed: Yes. Optional writeback deduplication supports actual cached block boundaries, including restored irregular blocks.
- Does this need documentation: No. The PR descriptions record this compatibility fix; public configuration semantics are unchanged.
@bobhan1

bobhan1 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

/review

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Selected model is at capacity. Please try a different model.
Workflow run: https://github.com/apache/doris/actions/runs/40844506228

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@bobhan1

bobhan1 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

/review

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Selected model is at capacity. Please try a different model.
Workflow run: https://github.com/apache/doris/actions/runs/40857016439

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@bobhan1

bobhan1 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed PR head b237ebc against the supplied authoritative diff under the static-only contract. The complete changed-file sweep and two-round risk-focused recheck converged with three new P2 findings: stale active partial-writeback tasks can absorb post-invalidation fragments; the new range-coverage helper treats deletion-marked blocks as usable coverage; and deferred hole-fill reads bypass workload-group remote-read throttling and accounting. Existing P1 4344901080 remains applicable and is carried in existing_blocking_comment_ids without duplication. Existing P1s 4278847462, 4278847468, and 4278847472 were revalidated as fixed at this head; existing P2/P3 threads were deduplicated. No builds, tests, or runtime failure injection were run; changed tests were inspected only. No additional user review focus was supplied. Review converged after round 2 with no unresolved candidates.

Review converged after 2 round(s).

Existing P0/P1 findings confirmed for this head: #67292 (comment)

trace.fragment_lock_wait_ns += locked_at - lock_start;
Defer hold_time {[&] { trace.fragment_lock_hold_ns += trace.now() - locked_at; }};
trace.task_id = io_context.io_context.read_trace_id;
if (is_active()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Do not deduplicate new-epoch fragments into a stale active task

try_merge() returns ACTIVE_DEDUPLICATED for any active task before checking whether its write epoch is still current, and _enqueue_or_get_existing() makes the same active-task assumption. Cache invalidation advances the key epoch while the old task remains in _tasks; _process_task() then drops that stale task. A read after invalidation can therefore be merged into the stale owner and its fragment is never queued for the new epoch, leaving the block uncached until another read. Treat an active task from an old epoch as non-deduplicating (or defer a new-generation task) and add an invalidate-then-read case.

return ranges;
}

bool BlockFileCache::is_range_downloaded_or_downloading(const UInt128Wrapper& hash, size_t offset,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Exclude deleting blocks from writeback coverage

is_range_downloaded_or_downloading() treats a DOWNLOADED block as coverage without checking is_deleting(). Eviction and cache-clear mark the block deleting while it remains visible, so should_skip_block_writeback() can discard the only writeback task; after the block is recycled, those consumed bytes are not cached. Exclude deleting blocks under the canonical cache lock order so a replacement writeback can proceed, and cover an eviction race.

read_ahead_bvars().hole_fill_remote_requests << 1;
{
SCOPED_RAW_TIMER(&read_ns);
status = source_reader->read_at(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve workload-group accounting for deferred hole fills

_read_hole() performs remote reads on manager and remote-token threads after copying IOContext, but those callbacks never attach the originating ResourceContext. S3FileReader gates LIMIT_REMOTE_SCAN_IO and its workload-group accounting on an attached task, so deferred hole-fill GETs bypass the query's remote-read limit and are absent from group IO accounting. Carry a safe resource-group owner into each callback and attach it before source IO, or route these reads through an explicitly throttled policy, with coverage for single- and multi-hole blocks.

@bobhan1

bobhan1 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

/review Please re-evaluate the existing blocking P1 finding: #67292 (comment).

This issue has been fixed in the current head, b237ebcd675c0b8a6b9a22ab49253d272b318acd: b237ebc.

AsyncCacheWriteManager::should_skip_block_writeback() no longer calls the grid-only BlockFileCache::probe(). It now calls is_range_downloaded_or_downloading(), which walks actual cached intervals, including historical non-aligned blocks restored after an upgrade. Complete contiguous DOWNLOADED/DOWNLOADING coverage skips writeback; incomplete coverage proceeds through the existing interval-aware get_or_set() persistence path.

The fix includes SkipsMixedIrregularCoverageWithoutAllocating and FillsRestoredIrregularCacheGap. The latter persists an irregular block, reopens the cache, and fills the adjacent gap, covering the historical-cache restoration scenario.

The latest review targeted this fixed head but still carried the original P1 in existing_blocking_comment_ids. Please trace the updated call chain and explicitly reassess its applicability. If the original P1 is fixed, mark it as fixed and omit it from the blocking findings. If it still applies, provide a concrete reachable call chain on this head that still reaches the grid-only probe.

This focus is additional to the normal review and does not ask to ignore other findings.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static exact-head review of PR 67292 at b237ebc against the supplied authoritative diff and base 2a4191c. I read the required AGENTS.md files, all changed paths, existing threads, user focus, and the shared ledger. Two new P2 findings are posted inline: a partial writeback fragment can merge into a task evicted during the unlocked lookup window, and read-ahead fallback counters include ordinary nested/complex data-page reads that never attempted the wrapper. Existing scheduler callback, trace buffering, workload-group, stale-epoch, deletion-state, worker startup/resize, metadata, dictionary, and cache-budget findings were independently revalidated and left in their existing threads without duplicates. The user-focus P1 is fixed for its reported read-ahead partial-writeback path: should_skip_block_writeback now uses interval-aware coverage and the irregular-cache tests cover the restored-gap case, so existing_blocking_comment_ids is empty. An unchanged direct ASYNC_WRITE reader path still contains a pre-existing grid-only probe, but it is outside the fixed read-ahead writeback call chain and is not reposted here. This was a static-only review; no builds, tests, or runtime injection were run, and changed tests were inspected only. Review converged after 2 rounds with no unresolved candidates.

Review converged after 2 round(s).

return trace.finish(PartialBlockSubmitResult::REJECTED);
}
}
if (existing != nullptr) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Do not merge fragments into an evicted partial-block task. try_submit() finds an existing queued task under _mutex, releases that lock, and then calls Task::try_merge() at this line. While the lock is released, another submission can fill _max_pending_tasks and evict this inactive task from both _tasks and _queue. The shared pointer keeps the orphan alive, so its still-current epoch passes and try_merge() copies the fragment and returns MERGED, but no worker can dispatch that task; the caller reports success while the consumed bytes are lost. The same window exists after the EXISTING merge at lines 448-455. Revalidate that the task is still the current queued owner under the manager lock, or retry enqueueing the candidate before accepting the merge.

#include "runtime/runtime_profile.h"
#include "storage/cache/page_cache.h"
#include "storage/olap_common.h"
#include "storage/segment/encoding_info.h"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Count only actual read-ahead fallbacks. read_ahead_stats is shared by the whole segment, but non-scalar/nested iterators are deliberately left on their original FileReader. When such a DATA_PAGE reaches this branch, the dynamic cast at line 178 returns null, yet the code still increments fallback_pages, fallback_bytes, and fallback_time before the ordinary read. ReadAheadFallback* profile counters therefore report nested reads as failed or rejected read-ahead fallbacks. Gate this accounting on a successful SegmentReadAheadFileReader attempt and add nested-column coverage.

@bobhan1

bobhan1 commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28261 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit b237ebcd675c0b8a6b9a22ab49253d272b318acd, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17590	5148	5032	5032
q2	2122	311	243	243
q3	10274	1370	808	808
q4	4674	470	334	334
q5	7431	783	513	513
q6	178	182	149	149
q7	742	795	615	615
q8	9263	1339	1113	1113
q9	5492	4205	4173	4173
q10	6831	1290	1009	1009
q11	435	246	220	220
q12	632	395	281	281
q13	18099	3593	3011	3011
q14	269	267	245	245
q15	q16	730	721	638	638
q17	1192	745	579	579
q18	6986	6138	6096	6096
q19	1106	958	635	635
q20	376	328	221	221
q21	4453	2090	2285	2090
q22	345	295	256	256
Total cold run time: 99220 ms
Total hot run time: 28261 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5624	5554	5552	5552
q2	716	625	523	523
q3	5190	5361	4902	4902
q4	1979	2124	1565	1565
q5	5260	5151	5375	5151
q6	274	203	153	153
q7	2395	2050	1870	1870
q8	3042	2675	2705	2675
q9	8041	8026	7925	7925
q10	3747	3866	3222	3222
q11	656	440	421	421
q12	745	701	535	535
q13	3564	3906	3293	3293
q14	306	300	287	287
q15	q16	699	705	622	622
q17	8060	6996	6865	6865
q18	12980	12186	12896	12186
q19	928	788	795	788
q20	2123	2178	1922	1922
q21	5591	4553	4656	4553
q22	487	433	404	404
Total cold run time: 72407 ms
Total hot run time: 65414 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151236 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit b237ebcd675c0b8a6b9a22ab49253d272b318acd, data reload: false

query5	4309	589	437	437
query6	431	202	192	192
query7	4825	457	224	224
query8	320	165	154	154
query9	8798	3188	3176	3176
query10	472	324	256	256
query11	5972	3867	3551	3551
query12	150	98	87	87
query13	1260	460	321	321
query14	6061	3967	3624	3624
query14_1	3377	3384	3379	3379
query15	205	195	176	176
query16	943	409	384	384
query17	849	638	513	513
query18	2417	418	298	298
query19	192	160	129	129
query20	82	80	80	80
query21	203	128	105	105
query22	12930	12917	12754	12754
query23	13419	12838	12378	12378
query23_1	12620	12670	12685	12670
query24	7361	812	463	463
query24_1	471	482	470	470
query25	510	397	351	351
query26	1262	251	132	132
query27	2754	444	280	280
query28	4607	2017	2033	2017
query29	1565	611	482	482
query30	302	220	189	189
query31	1001	927	806	806
query32	146	95	97	95
query33	538	301	244	244
query34	877	876	504	504
query35	732	765	642	642
query36	811	835	739	739
query37	121	99	86	86
query38	1765	1759	1656	1656
query39	792	791	749	749
query39_1	748	738	725	725
query40	227	114	97	97
query41	76	67	66	66
query42	86	85	84	84
query43	356	370	310	310
query44	1301	696	703	696
query45	180	177	169	169
query46	840	967	574	574
query47	2969	2927	2870	2870
query48	309	314	223	223
query49	589	406	300	300
query50	679	278	213	213
query51	10696	10732	10369	10369
query52	88	82	75	75
query53	194	211	162	162
query54	255	202	208	202
query55	73	72	65	65
query56	258	232	241	232
query57	1634	1627	1554	1554
query58	297	275	258	258
query59	2260	2307	2012	2012
query60	293	255	240	240
query61	174	191	169	169
query62	391	333	293	293
query63	197	162	160	160
query64	2879	981	730	730
query65	3327	3259	3268	3259
query66	1762	415	296	296
query67	20076	20191	20043	20043
query68	3001	1005	607	607
query69	393	280	247	247
query70	857	790	801	790
query71	316	240	226	226
query72	2684	2621	2404	2404
query73	525	537	295	295
query74	4597	4507	4348	4348
query75	2333	2272	1890	1890
query76	2322	1014	615	615
query77	330	377	266	266
query78	9626	9728	9141	9141
query79	998	891	524	524
query80	767	422	342	342
query81	549	314	268	268
query82	572	135	104	104
query83	283	195	178	178
query84	307	118	106	106
query85	941	428	379	379
query86	360	238	227	227
query87	1977	1953	1807	1807
query88	3763	2756	2750	2750
query89	331	284	241	241
query90	1714	169	164	164
query91	163	153	127	127
query92	98	89	87	87
query93	1007	1022	578	578
query94	515	286	266	266
query95	562	378	307	307
query96	680	479	244	244
query97	2458	2455	2355	2355
query98	156	154	147	147
query99	762	768	639	639
Total cold run time: 234381 ms
Total hot run time: 151236 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.25 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit b237ebcd675c0b8a6b9a22ab49253d272b318acd, data reload: false

query1	0.00	0.00	0.00
query2	0.09	0.06	0.04
query3	0.26	0.09	0.09
query4	1.60	0.10	0.10
query5	0.24	0.24	0.24
query6	1.17	0.68	0.67
query7	0.04	0.01	0.00
query8	0.06	0.04	0.04
query9	0.38	0.34	0.36
query10	0.57	0.59	0.57
query11	0.19	0.14	0.14
query12	0.19	0.14	0.15
query13	0.47	0.51	0.48
query14	1.03	1.01	1.01
query15	0.63	0.62	0.60
query16	0.31	0.34	0.34
query17	1.15	1.18	1.13
query18	0.21	0.19	0.19
query19	2.05	1.90	2.00
query20	0.02	0.01	0.01
query21	15.51	0.19	0.13
query22	4.90	0.06	0.06
query23	16.28	0.32	0.13
query24	3.01	0.57	0.44
query25	0.09	0.07	0.04
query26	1.10	0.23	0.16
query27	0.05	0.05	0.05
query28	2.95	0.80	0.37
query29	12.47	4.12	3.20
query30	0.27	0.13	0.09
query31	2.79	0.57	0.33
query32	3.22	0.60	0.50
query33	3.16	3.29	3.20
query34	15.38	4.03	3.41
query35	3.35	3.34	3.36
query36	0.55	0.45	0.42
query37	0.09	0.07	0.06
query38	0.06	0.03	0.03
query39	0.04	0.03	0.03
query40	0.16	0.15	0.14
query41	0.09	0.04	0.03
query42	0.04	0.03	0.03
query43	0.05	0.03	0.04
Total cold run time: 96.27 s
Total hot run time: 24.25 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 34.42% (53/154) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 67.40% (32822/48695)
Line Coverage 52.36% (348614/665739)
Region Coverage 47.73% (283239/593478)
Branch Coverage 49.41% (133796/270768)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 55.94% (80/143) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.68% (36658/47189)
Line Coverage 63.04% (415273/658694)
Region Coverage 59.47% (351641/591317)
Branch Coverage 60.32% (162924/270108)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants