github-actions[bot] commented on code in PR #67292:
URL: https://github.com/apache/doris/pull/67292#discussion_r4278847462


##########
be/src/storage/segment/column_reader.cpp:
##########
@@ -1518,6 +1590,15 @@ Status MapFileColumnIterator::next_batch(size_t* n, 
MutableColumnPtr& dst, bool*
             column_offsets.get_data().back() - column_offsets.get_data()[start 
- 1]; // -1 is valid
 
     if (num_items > 0) {
+        if (!read_offset_only()) {

Review Comment:
   [P1] Avoid comparing ordinals of pruned map children. For a supported 
map_keys/map_values scan, set_access_paths marks the other child SKIP, and 
FileColumnIterator::seek_to_ordinal/next_batch leave that child's ordinal 
unchanged. After one nonempty batch (or a seek to a nonzero item offset), the 
active child and skipped child therefore differ and this DORIS_CHECK aborts the 
BE. This runs even with enable_query_read_ahead=false. Derive the item start 
from offsets/an active child and only compare participating children; keep the 
read-ahead preparation behind its context gate.



##########
be/src/io/cache/cached_remote_file_reader.cpp:
##########
@@ -132,6 +135,32 @@ 
CachedRemoteFileReader::CachedRemoteFileReader(FileReaderSPtr remote_file_reader
     }
 }
 
+std::shared_ptr<RangeCacheWriteback> 
CachedRemoteFileReader::make_range_cache_writeback(
+        const IOContext& io_context, PartialBlockWritebackManager* 
partial_block_manager) const {
+    DORIS_CHECK(_cache != nullptr);
+    DORIS_CHECK(partial_block_manager != nullptr);
+    if (_resolve_cache_write_mode(&io_context) == CacheWriteMode::NO_WRITE ||
+        use_remote_only_on_cache_miss(&io_context)) {
+        return nullptr;
+    }
+    auto* write_manager = _cache->async_write_manager();
+    DORIS_CHECK(write_manager != nullptr);
+    CacheContext cache_context(&io_context);
+    return std::make_shared<RangeCacheWriteback>(RangeCacheWritebackOptions {
+            .write_manager = write_manager,
+            .partial_block_manager = partial_block_manager,
+            .inflight_index = 
config::enable_async_file_cache_write_inflight_write_buffer_index
+                                      ? _cache->inflight_write_buffer_index()
+                                      : nullptr,
+            .source_reader = _remote_file_reader,

Review Comment:
   [P1] Keep the cached reader owner alive through delayed hole fills. 
Retaining _remote_file_reader alone does not keep it open: 
CachedRemoteFileReader's destructor unconditionally calls its close(). With 
disable_segment_cache=true, a short query can release its segment/cached 
wrapper before the default 2-second merge delay expires, leaving 
Task::source_reader closed when the hole worker reads it. This aborts at 
S3FileReader's !closed DCHECK; PackedFileReader instead rejects the reads even 
in release builds, so writeback is lost. Retain the cached wrapper separately 
for the lifetime of every queued/active task while using the raw reader for 
hole IO.



##########
be/src/io/cache/range_cache_writeback.cpp:
##########
@@ -0,0 +1,176 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "io/cache/range_cache_writeback.h"
+
+#include <utility>
+
+#include "common/logging.h"
+#include "io/cache/partial_block_writeback_manager.h"
+#include "io/fs/read_ahead_metrics.h"
+#include "io/fs/read_io_trace.h"
+#include "util/time.h"
+
+namespace doris::io {
+
+RangeCacheWriteback::RangeCacheWriteback(RangeCacheWritebackOptions options)
+        : _options(std::move(options)) {
+    DORIS_CHECK(_options.write_manager != nullptr);
+    DORIS_CHECK(_options.partial_block_manager != nullptr);
+    DORIS_CHECK(_options.source_reader != nullptr);
+    DORIS_CHECK(_options.file_size > 0);
+    DORIS_CHECK(_options.source_reader->size() == _options.file_size);
+    DORIS_CHECK(_options.block_size > 0);
+}
+
+std::optional<AsyncCacheWriteEpoch> RangeCacheWriteback::capture_write_epoch() 
const {
+    if (!_options.write_manager->accepting()) {

Review Comment:
   [P2] Start a cache writer before replacing the cache-populating read path. 
With the default enable_async_file_cache_write=false, BlockFileCache leaves its 
writer unstarted; only enabling that separate flag starts it. Turning on 
enable_query_read_ahead alone still forces range reads to NO_WRITE, but this 
guard then returns no epoch and the consumed-range callback skips every 
writeback. Successfully prefetched cold data pages consequently never populate 
File Cache (and repeat remote GETs whenever Page Cache does not retain them), 
whereas the original synchronous path did populate it. Arrange a functioning 
writer for read-ahead or preserve the normal cache-population path when the 
writer is unavailable; cover this default flag combination.



##########
be/src/storage/segment/segment_iterator.cpp:
##########
@@ -384,6 +385,33 @@ Status SegmentIterator::init(const StorageReadOptions& 
opts) {
     return status;
 }
 
+Status SegmentIterator::_init_query_read_ahead() {
+    if (!SegmentReadAhead::enabled() || _opts.io_ctx.reader_type != 
ReaderType::READER_QUERY) {
+        return Status::OK();
+    }
+
+    DORIS_CHECK(_opts.runtime_state != nullptr);
+    auto* exec_env = _opts.runtime_state->exec_env();
+    DORIS_CHECK(exec_env != nullptr);
+    auto* scheduler = exec_env->file_range_read_scheduler();
+    DORIS_CHECK(scheduler != nullptr);
+    auto* query_context = _opts.runtime_state->get_query_ctx();
+    DORIS_CHECK(query_context != nullptr);
+    auto read_context = 
query_context->get_or_create_file_range_read_context(scheduler);
+    const auto status = SegmentReadAhead::create_for_query(
+            _file_reader, exec_env, std::move(read_context), _opts, 
&_segment_read_ahead);
+    if (!status.ok()) {
+        LOG_EVERY_N(WARNING, 100)
+                << "failed to initialize query read-ahead; use the original 
read path: " << status;
+        return Status::OK();
+    }
+    DORIS_CHECK(_segment_read_ahead != nullptr);

Review Comment:
   [P1] Handle read-ahead being disabled during initialization. 
enable_query_read_ahead is mutable: this method can pass enabled(), then 
create_for_query can observe a concurrent disable and return OK with its output 
reset to nullptr. The new DORIS_CHECK then aborts the BE when an operator turns 
the feature off during a scan. The row-store point-query helper has the same 
assumption after creating its request context. Treat the factory's documented 
OK/null result as synchronous fallback, or keep a single initialization 
decision for the request.



##########
be/src/io/cache/partial_block_writeback_manager.cpp:
##########
@@ -0,0 +1,1035 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "io/cache/partial_block_writeback_manager.h"
+
+#include <algorithm>
+#include <chrono>
+#include <cstring>
+#include <iterator>
+#include <optional>
+#include <utility>
+#include <vector>
+
+#include "common/logging.h"
+#include "cpp/sync_point.h"
+#include "io/cache/hole_fill_planner.h"
+#include "io/cache/inflight_write_buffer_index.h"
+#include "io/fs/read_ahead_metrics.h"
+#include "io/fs/read_io_trace.h"
+#include "util/countdown_latch.h"
+#include "util/defer_op.h"
+#include "util/thread.h"
+#include "util/threadpool.h"
+#include "util/time.h"
+
+namespace doris::io {
+namespace {
+
+using namespace std::chrono_literals;
+
+constexpr auto kCapacityRetryInterval = 10ms;
+constexpr size_t kMaxHoleFillWorkerCount = 128;
+
+} // namespace
+
+size_t PartialBlockWritebackManager::BlockKeyHash::operator()(const BlockKey& 
key) const {
+    const size_t manager_hash = std::hash<AsyncCacheWriteManager*> 
{}(key.write_manager);
+    const size_t file_hash = KeyHash {}(key.cache_hash);
+    const size_t offset_hash = std::hash<size_t> {}(key.block_offset);
+    return manager_hash ^ (file_hash << 1) ^ (offset_hash << 2);
+}
+
+// Per-call diagnostics only. Keep the request identity alive across moves 
into a Task and emit
+// once, after every manager/fragment lock has left scope. The disabled path 
reads no clocks.
+struct PartialBlockWritebackManager::SubmitTrace : HoleSubmitTiming {
+    explicit SubmitTrace(const PartialBlockWritebackRequest& request) {
+        if (ReadIOTrace::enabled()) {
+            start_ns = MonotonicNanos();
+            context = request.io_context;
+            reader = request.source_reader;
+            offset = request.block_offset + request.fragment_offset;
+            size = request.data.size;
+        }
+    }
+
+    ~SubmitTrace() {
+        if (start_ns != 0) {
+            ReadIOTrace::record({.event = "hole_submit",
+                                 .context = &context.io_context,
+                                 .file = reader->path().native(),
+                                 .id = task_id,
+                                 .parent_id = context.io_context.read_trace_id,
+                                 .offset = offset,
+                                 .size = size,
+                                 .time_ns = MonotonicNanos(),
+                                 .start_ns = start_ns,
+                                 .outcome = outcome,
+                                 .hole_submit_timing = this});
+        }
+    }
+
+    int64_t now() const { return start_ns != 0 ? MonotonicNanos() : 0; }
+
+    PartialBlockSubmitResult finish(PartialBlockSubmitResult result) {
+        switch (result) {
+        case PartialBlockSubmitResult::QUEUED:
+            outcome = "queued";
+            break;
+        case PartialBlockSubmitResult::MERGED:
+            outcome = "merged";
+            break;
+        case PartialBlockSubmitResult::ACTIVE_DEDUPLICATED:
+            outcome = "active_deduplicated";
+            break;
+        case PartialBlockSubmitResult::CACHE_BLOCK_PRESENT:
+            outcome = "cache_block_present";
+            break;
+        case PartialBlockSubmitResult::STALE_EPOCH:
+            outcome = "stale_epoch";
+            break;
+        case PartialBlockSubmitResult::BUFFER_ALLOCATION_FAILED:
+            outcome = "allocation_failed";
+            break;
+        case PartialBlockSubmitResult::REJECTED:
+            outcome = "rejected";
+            break;
+        }
+        return result;
+    }
+
+    int64_t start_ns {0};
+    FileRangeReadIOContext context;
+    FileReaderSPtr reader;
+    size_t offset {0};
+    size_t size {0};
+    uint64_t task_id {0};
+    std::string_view outcome;
+};
+
+// One record per locked scan, not per entry. No JSON formatting or 
trace-writer locking is done
+// under the queue mutex. CV sleep and its mutex reacquisition are excluded 
from scan time.
+struct PartialBlockWritebackManager::QueueScanTrace : HoleQueueScanStats {
+    int64_t start_ns {0};
+    int64_t end_ns {0};
+
+    void record(bool activated) const {
+        if (start_ns != 0) {
+            ReadIOTrace::record({.event = "hole_queue_scan",
+                                 .time_ns = end_ns,
+                                 .start_ns = start_ns,
+                                 .outcome = activated ? "activated" : 
"no_runnable_task",
+                                 .queue_scan = this});
+        }
+    }
+};
+
+struct PartialBlockWritebackManager::Task {
+    // Allocate one tracked block buffer and copy the first fragment into it.
+    static TaskPtr create(PartialBlockWritebackRequest request, const 
BlockKey& task_key,
+                          size_t block_size, SubmitTrace& trace);
+
+    // A queued task may merge while the manager lock is free. Once activate() 
wins, later
+    // fragments are deduplicated and the worker obtains their bytes from the 
source read instead.
+    std::optional<PartialBlockSubmitResult> try_merge(size_t fragment_offset, 
Slice data,
+                                                      const IOContext& 
fragment_context,
+                                                      SubmitTrace& trace) {
+        const auto lock_start = trace.now();
+        std::lock_guard lock(fragment_mutex);
+        const auto locked_at = trace.now();
+        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()) {
+            const auto trace_start = trace.now();
+            trace_fragment("fragment_ignored_active", fragment_offset, 
data.size, fragment_context);
+            trace.lifecycle_trace_ns += trace.now() - trace_start;
+            return PartialBlockSubmitResult::ACTIVE_DEDUPLICATED;
+        }
+        if (!key.write_manager->is_current_write_epoch(write_epoch)) {
+            return std::nullopt;
+        }
+
+        
TEST_SYNC_POINT("PartialBlockWritebackManager::try_submit:before_merge_copy");
+        const auto copy_start = trace.now();
+        std::memcpy(buffer->data() + fragment_offset, data.data, data.size);
+        trace.copy_ns += trace.now() - copy_start;
+        trace.copied_bytes += data.size;
+        covered_intervals.emplace_back(

Review Comment:
   [P2] Coalesce coverage while merging queued fragments. Each merge appends 
another interval even when the same bytes are already covered. A hot partial 
block can stay queued indefinitely while other writes exhaust writer capacity; 
repeated point/scanner reads with Page Cache disabled or evicted keep growing 
this vector although pending_bytes still accounts for one block. Activation 
then copies and sorts the entire submission history in HoleFillPlanner, adding 
another allocation/CPU spike. Maintain bounded disjoint coverage (or charge/cap 
fragment metadata) so repeated identical fragments cannot bypass the queue's 
memory bound.



##########
be/src/io/fs/file_range_read_scheduler.cpp:
##########
@@ -0,0 +1,657 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "io/fs/file_range_read_scheduler.h"
+
+#include <exception>
+#include <limits>
+#include <utility>
+
+#include "common/exception.h"
+#include "common/logging.h"
+#include "core/allocator.h"
+#include "cpp/sync_point.h"
+#include "io/fs/read_ahead_metrics.h"
+#include "io/fs/read_io_trace.h"
+#include "runtime/memory/mem_tracker_limiter.h"
+#include "runtime/thread_context.h"
+#include "util/defer_op.h"
+#include "util/threadpool.h"
+#include "util/time.h"
+
+namespace doris::io {
+
+using FileRangeReadAllocator = Allocator<false, false, false, 
DefaultMemoryAllocator, true>;
+
+namespace {
+
+bool add_overflows(size_t left, size_t right) {
+    return right > std::numeric_limits<size_t>::max() - left;
+}
+
+} // namespace
+
+FileRangeReadSubmitResult 
FileRangeReadSubmitResult::rejected(FileRangeReadRejectReason reason,
+                                                              Status status) {
+    return FileRangeReadSubmitResult {
+            .reads = {},
+            .reject_reason = reason,
+            .status = std::move(status),
+    };
+}
+
+FileRangeReadBudget::FileRangeReadBudget(size_t max_bytes) : 
_max_bytes(max_bytes) {
+    DORIS_CHECK(_max_bytes > 0);
+}
+
+size_t FileRangeReadBudget::resident_bytes() const {
+    std::lock_guard lock(_mutex);
+    return _resident_bytes;
+}
+
+bool FileRangeReadBudget::_can_reserve_unlocked(size_t bytes) const {
+    return bytes <= _max_bytes - _resident_bytes;
+}
+
+void FileRangeReadBudget::_reserve_unlocked(size_t bytes) {
+    DORIS_CHECK(_can_reserve_unlocked(bytes));
+    _resident_bytes += bytes;
+}
+
+void FileRangeReadBudget::_release(size_t bytes) {
+    std::lock_guard lock(_mutex);
+    DORIS_CHECK(bytes <= _resident_bytes);
+    _resident_bytes -= bytes;
+}
+
+FileRangeReadContext::FileRangeReadContext(size_t max_bytes) : 
_budget(max_bytes) {}
+
+void FileRangeReadContext::cancel() {
+    if (_cancelled.exchange(true, std::memory_order_acq_rel)) {
+        return;
+    }
+
+    std::lock_guard lock(_reads_mutex);
+    for (const auto& weak_read : _reads) {
+        if (auto read = weak_read.lock(); read != nullptr) {
+            read->request_cancel();
+        }
+    }
+    _reads.clear();
+}
+
+void FileRangeReadContext::_register_reads(
+        const std::vector<std::shared_ptr<FileRangeRead>>& reads) {
+    bool cancel_reads = false;
+    {
+        std::lock_guard lock(_reads_mutex);
+        cancel_reads = _cancelled.load(std::memory_order_acquire);
+        if (!cancel_reads) {
+            std::erase_if(_reads, [](const auto& read) { return 
read.expired(); });
+            _reads.reserve(_reads.size() + reads.size());
+            for (const auto& read : reads) {
+                _reads.emplace_back(read);
+            }
+        }
+    }
+    if (cancel_reads) {
+        for (const auto& read : reads) {
+            read->request_cancel();
+        }
+    }
+}
+
+FileRangeReadReservation::FileRangeReadReservation(
+        std::shared_ptr<FileRangeReadContext> context,
+        std::shared_ptr<FileRangeReadBudget> global_budget, size_t bytes) 
noexcept
+        : _context(std::move(context)), 
_global_budget(std::move(global_budget)), _bytes(bytes) {
+    DORIS_CHECK(_context != nullptr);
+    DORIS_CHECK(_global_budget != nullptr);
+    DORIS_CHECK(_bytes > 0);
+}
+
+FileRangeReadReservation::FileRangeReadReservation(FileRangeReadReservation&& 
other) noexcept
+        : _context(std::move(other._context)),
+          _global_budget(std::move(other._global_budget)),
+          _bytes(std::exchange(other._bytes, 0)) {}
+
+FileRangeReadReservation& FileRangeReadReservation::operator=(
+        FileRangeReadReservation&& other) noexcept {
+    if (this != &other) {
+        _reset();
+        _context = std::move(other._context);
+        _global_budget = std::move(other._global_budget);
+        _bytes = std::exchange(other._bytes, 0);
+    }
+    return *this;
+}
+
+FileRangeReadReservation::~FileRangeReadReservation() {
+    _reset();
+}
+
+void FileRangeReadReservation::_reset() {
+    if (_context == nullptr) {
+        return;
+    }
+    _context->_budget._release(_bytes);
+    _global_budget->_release(_bytes);
+    read_ahead_bvars().resident_bytes << -static_cast<int64_t>(_bytes);
+    _context.reset();
+    _global_budget.reset();
+    _bytes = 0;
+}
+
+FileRangeRead::FileRangeRead(FileRange range, 
std::shared_ptr<MemTrackerLimiter> tracker,
+                             FileRangeReadReservation reservation,
+                             std::shared_ptr<ReadAheadStatistics> statistics)
+        : _range(range),
+          _trace_id(ReadIOTrace::next_id()),
+          _tracker(std::move(tracker)),
+          _reservation(std::move(reservation)),
+          _statistics(std::move(statistics)) {
+    FileRangeReadAllocator allocator;
+    _data = reinterpret_cast<char*>(allocator.alloc(_range.size));
+}
+
+FileRangeRead::~FileRangeRead() {
+    SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(_tracker);
+    FileRangeReadAllocator allocator;
+    allocator.free(_data, _range.size);
+}
+
+Status FileRangeRead::create(FileRange range, 
std::shared_ptr<MemTrackerLimiter> tracker,
+                             FileRangeReadReservation reservation,
+                             std::shared_ptr<FileRangeRead>* output,
+                             std::shared_ptr<ReadAheadStatistics> statistics) {
+    DORIS_CHECK(range.size > 0);
+    DORIS_CHECK(tracker != nullptr);
+    DORIS_CHECK(output != nullptr);
+
+    Status injected_status;
+    TEST_SYNC_POINT_CALLBACK("FileRangeRead::create:inject_failure", 
&injected_status);
+    if (!injected_status.ok()) {
+        return injected_status;
+    }
+
+    SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(tracker);
+    try {
+        output->reset(new FileRangeRead(range, std::move(tracker), 
std::move(reservation),
+                                        std::move(statistics)));
+    } catch (const Exception& exception) {
+        return exception.to_status();
+    } catch (const std::exception& exception) {
+        return Status::MemoryAllocFailed("allocate file range read buffer 
failed: {}",
+                                         exception.what());
+    }
+    return Status::OK();
+}
+
+bool FileRangeRead::_is_terminal(State state) {
+    return state == State::READY || state == State::FAILED || state == 
State::CANCELLED;
+}
+
+Status FileRangeRead::wait(bool* ready_on_entry) {
+    std::unique_lock lock(_mutex);
+    if (ready_on_entry != nullptr) {
+        *ready_on_entry = _state == State::READY;
+    }
+    _cv.wait(lock, [this]() { return _is_terminal(_state); });
+    return _status;
+}
+
+void FileRangeRead::request_cancel() {
+    bool publish_cancelled = false;
+    {
+        std::lock_guard lock(_mutex);
+        if (_is_terminal(_state)) {
+            return;
+        }
+        _cancel_requested = true;
+        publish_cancelled = _state == State::QUEUED;
+        if (publish_cancelled) {
+            _state = State::CANCELLED;
+            _status = Status::Cancelled("asynchronous file range read 
cancelled");
+        }
+    }
+    if (publish_cancelled) {
+        _cv.notify_all();
+    }
+}
+
+FileRangeRead::State FileRangeRead::state() const {
+    std::lock_guard lock(_mutex);
+    return _state;
+}
+
+Slice FileRangeRead::data() const {
+    return slice(0, _range.size);
+}
+
+Slice FileRangeRead::slice(size_t buffer_offset, size_t size) const {
+    std::lock_guard lock(_mutex);
+    DORIS_CHECK(_state == State::READY);
+    DORIS_CHECK(buffer_offset <= _range.size);
+    DORIS_CHECK(size <= _range.size - buffer_offset);
+    return {_data + buffer_offset, size};
+}
+
+FileRangeReadStats FileRangeRead::stats() const {
+    std::lock_guard lock(_mutex);
+    return _stats;
+}
+
+bool FileRangeRead::_mark_running() {
+    std::lock_guard lock(_mutex);
+    if (_state == State::CANCELLED) {
+        return false;
+    }
+    DORIS_CHECK(_state == State::QUEUED);
+    _state = State::RUNNING;
+    return true;
+}
+
+bool FileRangeRead::_is_cancel_requested() const {
+    std::lock_guard lock(_mutex);
+    return _cancel_requested;
+}
+
+void FileRangeRead::_publish_ready(FileRangeReadStats stats) {
+    _publish_from_running(State::READY, Status::OK(), std::move(stats));
+}
+
+void FileRangeRead::_publish_failed(Status status, FileRangeReadStats stats) {
+    DORIS_CHECK(!status.ok());
+    _publish_from_running(State::FAILED, std::move(status), std::move(stats));
+}
+
+void FileRangeRead::_publish_submit_failure(Status status) {
+    DORIS_CHECK(!status.ok());
+    {
+        std::lock_guard lock(_mutex);
+        if (_state == State::CANCELLED) {
+            return;
+        }
+        DORIS_CHECK(_state == State::QUEUED);
+        _state = State::FAILED;
+        _status = std::move(status);
+    }
+    _cv.notify_all();
+}
+
+void FileRangeRead::_publish_cancelled(FileRangeReadStats stats) {
+    _publish_from_running(State::CANCELLED,
+                          Status::Cancelled("asynchronous file range read 
cancelled"),
+                          std::move(stats));
+}
+
+void FileRangeRead::_publish_from_running(State state, Status status, 
FileRangeReadStats stats) {
+    {
+        std::lock_guard lock(_mutex);
+        DORIS_CHECK(_state == State::RUNNING);
+        DORIS_CHECK(_is_terminal(state));
+        if (_cancel_requested) {
+            state = State::CANCELLED;
+            status = Status::Cancelled("asynchronous file range read 
cancelled");
+        }
+        _state = state;
+        _status = std::move(status);
+        _stats = std::move(stats);
+        // Publish accounting before waking a consumer that may immediately 
close its scanner.
+        if (_statistics != nullptr) {
+            auto& outcome = state == State::READY    ? 
_statistics->completed_ranges
+                            : state == State::FAILED ? 
_statistics->failed_ranges
+                                                     : 
_statistics->cancelled_ranges;
+            COUNTER_UPDATE(&outcome, 1);
+            COUNTER_UPDATE(&_statistics->queue_wait_time, 
_stats.queue_wait_ns);
+            COUNTER_UPDATE(&_statistics->read_time, _stats.read_ns);
+            COUNTER_UPDATE(&_statistics->source_bytes, 
static_cast<int64_t>(_stats.bytes_read));
+            COUNTER_UPDATE(&_statistics->successful_source_bytes,
+                           
static_cast<int64_t>(_stats.successful_source_bytes));
+            COUNTER_UPDATE(&_statistics->remote_bytes, 
_stats.file_cache.bytes_read_from_remote);
+            COUNTER_UPDATE(&_statistics->local_bytes, 
_stats.file_cache.bytes_read_from_local);
+            COUNTER_UPDATE(&_statistics->remote_requests, 
_stats.file_cache.num_remote_io_total);
+        }
+        auto& metrics = read_ahead_bvars();
+        metrics.source_bytes << static_cast<int64_t>(_stats.bytes_read);
+        metrics.successful_source_bytes << 
static_cast<int64_t>(_stats.successful_source_bytes);
+        metrics.remote_bytes << _stats.file_cache.bytes_read_from_remote;
+        metrics.remote_requests << _stats.file_cache.num_remote_io_total;
+        if (state == State::FAILED) {
+            read_ahead_bvars().failed_ranges << 1;
+        }
+    }
+    _cv.notify_all();
+}
+
+FileRangeReadIOContext FileRangeReadIOContext::from_caller(const IOContext& 
source) {
+    FileRangeReadIOContext result;
+    result.io_context = source;
+    if (source.query_id != nullptr) {
+        result.query_id = std::make_shared<TUniqueId>(*source.query_id);
+    }
+    result.io_context.file_cache_stats = nullptr;
+    result.io_context.file_reader_stats = nullptr;
+    result.io_context.remote_scan_cache_write_limiter = nullptr;
+    result.io_context.condition_cache_filtered_rows = 0;
+    result.io_context.predicate_filtered_rows = 0;
+    result.io_context.cache_write_mode_override = CacheWriteMode::NO_WRITE;
+    result.io_context.query_id = result.query_id.get();
+    return result;
+}
+
+FileRangeReadScheduler::FileRangeReadScheduler(FileRangeReadSchedulerOptions 
options,
+                                               ThreadPool* executor)
+        : _options(options),
+          
_global_budget(std::make_shared<FileRangeReadBudget>(options.max_bytes_per_be)),
+          
_mem_tracker(MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::CACHE,
+                                                        
"FileRangeReadScheduler")),
+          _executor(executor) {
+    DORIS_CHECK(_executor != nullptr);
+    read_ahead_bvars().resident_bytes << 0;
+}
+
+FileRangeReadScheduler::~FileRangeReadScheduler() {
+    shutdown();
+}
+
+Status FileRangeReadSchedulerOptions::validate() const {
+    if (max_bytes_per_query == 0) {
+        return Status::InvalidArgument("file range read query byte limit must 
be positive");
+    }
+    if (max_bytes_per_be == 0) {
+        return Status::InvalidArgument("file range read BE byte limit must be 
positive");
+    }
+    return Status::OK();
+}
+
+Status FileRangeReadScheduler::create(const FileRangeReadSchedulerOptions& 
options,
+                                      ThreadPool* executor,
+                                      std::unique_ptr<FileRangeReadScheduler>* 
output) {
+    DORIS_CHECK(output != nullptr);
+    DORIS_CHECK(executor != nullptr);
+    RETURN_IF_ERROR(options.validate());
+    output->reset(new FileRangeReadScheduler(options, executor));
+    return Status::OK();
+}
+
+std::shared_ptr<FileRangeReadContext> FileRangeReadScheduler::create_context() 
const {
+    return 
std::make_shared<FileRangeReadContext>(_options.max_bytes_per_query);
+}
+
+FileRangeReadSubmitResult FileRangeReadScheduler::try_submit(
+        const std::vector<FileRange>& ranges, FileReaderSPtr reader,
+        FileRangeReadIOContext io_context, const 
std::shared_ptr<FileRangeReadContext>& context) {
+    const auto reject = [&](FileRangeReadRejectReason reason, Status status = 
Status::OK()) {
+        read_ahead_bvars().rejected_batches << 1;
+        if (io_context.statistics != nullptr) {
+            COUNTER_UPDATE(&io_context.statistics->rejected_batches, 1);
+            if (reason == FileRangeReadRejectReason::QUERY_BYTE_LIMIT) {
+                
COUNTER_UPDATE(&io_context.statistics->query_budget_rejected_batches, 1);
+            } else if (reason == FileRangeReadRejectReason::GLOBAL_BYTE_LIMIT) 
{
+                
COUNTER_UPDATE(&io_context.statistics->be_budget_rejected_batches, 1);
+            }
+        }
+        return FileRangeReadSubmitResult::rejected(reason, std::move(status));
+    };
+    size_t total_bytes = 0;
+    Status validation_status = _validate_request(ranges, reader, context, 
&total_bytes);
+    if (!validation_status.ok()) {
+        return reject(FileRangeReadRejectReason::INVALID_REQUEST, 
std::move(validation_status));
+    }
+    if (!accepting()) {
+        return reject(FileRangeReadRejectReason::SHUTTING_DOWN,
+                      Status::Cancelled("file range read scheduler is shutting 
down"));
+    }
+    if (context->cancelled()) {
+        return reject(FileRangeReadRejectReason::QUERY_CANCELLED,
+                      Status::Cancelled("query cancelled before file range 
submission"));
+    }
+
+    std::vector<FileRangeReadReservation> reservations;
+    reservations.reserve(ranges.size());
+    const auto reserve_reason = _reserve_batch(context, ranges, total_bytes, 
&reservations);
+    if (reserve_reason != FileRangeReadRejectReason::NONE) {
+        return reject(reserve_reason);
+    }
+
+    std::vector<std::shared_ptr<FileRangeRead>> reads;
+    reads.reserve(ranges.size());
+    std::vector<ReadTask> tasks;
+    tasks.reserve(ranges.size());
+    for (size_t index = 0; index < ranges.size(); ++index) {
+        std::shared_ptr<FileRangeRead> read;
+        Status status =
+                FileRangeRead::create(ranges[index], _mem_tracker, 
std::move(reservations[index]),
+                                      &read, io_context.statistics);
+        if (!status.ok()) {
+            return reject(FileRangeReadRejectReason::ALLOCATION_FAILED, 
std::move(status));
+        }
+        tasks.push_back(ReadTask {
+                .reader = reader, .read = read, .context = context, 
.io_context = io_context});
+        reads.emplace_back(std::move(read));
+    }
+    context->_register_reads(reads);
+
+    FileRangeReadRejectReason submit_reason = FileRangeReadRejectReason::NONE;
+    Status submit_status = _submit_tasks(std::move(tasks), &submit_reason);
+    if (!submit_status.ok()) {
+        return reject(submit_reason, std::move(submit_status));
+    }
+    return {.reads = std::move(reads),
+            .reject_reason = FileRangeReadRejectReason::NONE,
+            .status = Status::OK()};
+}
+
+Status FileRangeReadScheduler::_validate_request(
+        const std::vector<FileRange>& ranges, const FileReaderSPtr& reader,
+        const std::shared_ptr<FileRangeReadContext>& context, size_t* 
total_bytes) const {
+    DORIS_CHECK(total_bytes != nullptr);
+    if (reader == nullptr) {
+        return Status::InvalidArgument("file range reader is null");
+    }
+    if (context == nullptr) {
+        return Status::InvalidArgument("file range read context is null");
+    }
+    if (ranges.empty()) {
+        return Status::InvalidArgument("file range read batch is empty");
+    }
+
+    *total_bytes = 0;
+    for (const auto& range : ranges) {
+        if (range.size == 0) {
+            return Status::InvalidArgument("file range at offset {} is empty", 
range.offset);
+        }
+        if (add_overflows(range.offset, range.size)) {
+            return Status::InvalidArgument("file range at offset {} with size 
{} overflows",
+                                           range.offset, range.size);
+        }
+        if (range.end() > reader->size()) {
+            return Status::InvalidArgument("file range [{}, {}) exceeds file 
size {}", range.offset,
+                                           range.end(), reader->size());
+        }
+        if (add_overflows(*total_bytes, range.size)) {
+            return Status::InvalidArgument("file range read batch byte size 
overflows");
+        }
+        *total_bytes += range.size;
+    }
+    return Status::OK();
+}
+
+FileRangeReadRejectReason FileRangeReadScheduler::_reserve_batch(
+        const std::shared_ptr<FileRangeReadContext>& context, const 
std::vector<FileRange>& ranges,
+        size_t total_bytes, std::vector<FileRangeReadReservation>* 
reservations) {
+    DORIS_CHECK(reservations != nullptr);
+    DORIS_CHECK(reservations->capacity() >= ranges.size());
+    auto& query_budget = context->_budget;
+    std::scoped_lock lock(query_budget._mutex, _global_budget->_mutex);
+
+    if (total_bytes > query_budget._max_bytes - query_budget._resident_bytes) {
+        return FileRangeReadRejectReason::QUERY_BYTE_LIMIT;
+    }
+    if (total_bytes > _global_budget->_max_bytes - 
_global_budget->_resident_bytes) {
+        return FileRangeReadRejectReason::GLOBAL_BYTE_LIMIT;
+    }
+
+    query_budget._reserve_unlocked(total_bytes);
+    _global_budget->_reserve_unlocked(total_bytes);
+    read_ahead_bvars().resident_bytes << static_cast<int64_t>(total_bytes);
+    for (const auto& range : ranges) {
+        reservations->push_back(FileRangeReadReservation(context, 
_global_budget, range.size));
+    }
+    return FileRangeReadRejectReason::NONE;
+}
+
+Status FileRangeReadScheduler::_submit_tasks(std::vector<ReadTask> tasks,
+                                             FileRangeReadRejectReason* 
reject_reason) {
+    DORIS_CHECK(!tasks.empty());
+    DORIS_CHECK(reject_reason != nullptr);
+    std::lock_guard lock(_mutex);
+    if (!_accepting) {
+        *reject_reason = FileRangeReadRejectReason::SHUTTING_DOWN;
+        return Status::Cancelled("file range read scheduler is shutting down");
+    }
+
+    size_t submitted_tasks = 0;
+    Status first_failure;
+    for (auto& task : tasks) {
+        auto read = task.read;
+        DORIS_CHECK(_inflight_reads.emplace(read).second);
+        read_ahead_bvars().inflight_ranges << 1;
+        task.submit_ns = MonotonicNanos();
+        Status status = _executor->submit_func([this, task = std::move(task), 
read]() mutable {
+            Defer task_finished {[this, read]() { _task_finished(read); }};
+            _run_task(std::move(task));
+        });
+        if (status.ok()) {
+            ++submitted_tasks;
+            if (read->_statistics != nullptr) {
+                COUNTER_UPDATE(&read->_statistics->submitted_ranges, 1);
+                COUNTER_UPDATE(&read->_statistics->submitted_bytes,
+                               static_cast<int64_t>(read->range().size));
+            }
+            continue;
+        }
+        DORIS_CHECK(_inflight_reads.erase(read) == 1);
+        read_ahead_bvars().inflight_ranges << -1;
+        if (first_failure.ok()) {
+            first_failure = status;
+        }
+        if (read->_statistics != nullptr) {
+            COUNTER_UPDATE(&read->_statistics->submit_failed_ranges, 1);
+        }
+        read->_publish_submit_failure(std::move(status));
+    }
+
+    if (submitted_tasks == 0) {
+        DORIS_CHECK(!first_failure.ok());
+        *reject_reason = FileRangeReadRejectReason::EXECUTOR_REJECTED;
+        return first_failure;
+    }
+    return Status::OK();
+}
+
+void FileRangeReadScheduler::_task_finished(const 
std::shared_ptr<FileRangeRead>& read) {
+    std::lock_guard lock(_mutex);
+    DORIS_CHECK(_inflight_reads.erase(read) == 1);
+    read_ahead_bvars().inflight_ranges << -1;
+    if (_inflight_reads.empty()) {
+        _tasks_cv.notify_all();
+    }
+}
+
+void FileRangeReadScheduler::_run_task(ReadTask task) const {
+    if (!task.read->_mark_running()) {
+        // Count queued cancellation only for tasks accepted by the executor. 
A handle may be
+        // cancelled before submission, including a submission the executor 
later rejects.
+        if (task.read->_statistics != nullptr) {
+            COUNTER_UPDATE(&task.read->_statistics->cancelled_ranges, 1);
+        }
+        return;
+    }
+
+    FileRangeReadStats stats;
+    stats.queue_wait_ns = MonotonicNanos() - task.submit_ns;
+    if (task.context->cancelled() || !accepting()) {
+        task.read->_publish_cancelled(std::move(stats));
+        return;
+    }
+    task.io_context.io_context.file_cache_stats = &stats.file_cache;
+    task.io_context.io_context.file_reader_stats = &stats.file_reader;
+    task.io_context.io_context.read_trace_source = 
FileReadTraceSource::READ_AHEAD;
+    task.io_context.io_context.read_trace_id = task.read->trace_id();
+    Status status;
+    const int64_t read_start_ns = MonotonicNanos();
+    try {
+        status = task.reader->read_at(task.read->range().offset,

Review Comment:
   [P2] Preserve workload-group context for scheduled query reads. The scanner 
previously reached S3FileReader with SCOPED_ATTACH_TASK(ctx->state()), but 
these reads now run in a plain shared-prefetch-pool task with no retained 
ResourceContext or task attachment. S3FileReader's LIMIT_REMOTE_SCAN_IO only 
obtains the workload-group throttle and updates its I/O bytes when 
thread_context()->is_attach_task() is true, so admitted read-ahead GETs bypass 
remote_read_bytes_per_second and disappear from group I/O accounting. These are 
the query's required page reads. Retain the appropriate resource/workload-group 
owner and attach it at the executor callback before source I/O.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to