HappenLee commented on code in PR #67762:
URL: https://github.com/apache/doris/pull/67762#discussion_r4313174588


##########
be/src/exec/rowid_fetcher.cpp:
##########
@@ -312,104 +509,153 @@ Status RowIdStorageReader::read_by_rowids(const 
PMultiGetRequestV2& request,
         set_topn_lazy_materialization_file_cache_stats(
                 stats.file_cache_stats,
                 
response->mutable_topn_lazy_materialization_file_cache_stats());
+
+        return Status::OK();
     }
+};
 
-    return Status::OK();
-}
+namespace {
 
-Status RowIdStorageReader::read_batch_doris_format_row(
-        const PRequestBlockDesc& request_block_desc, 
std::shared_ptr<IdFileMap> id_file_map,
-        std::vector<SlotDescriptor>& slots, const TUniqueId& query_id, Block& 
result_block,
-        OlapReaderStatistics& stats, int64_t* acquire_tablet_ms, int64_t* 
acquire_rowsets_ms,
-        int64_t* acquire_segments_ms, int64_t* lookup_row_data_ms,
-        io::FileCacheMissPolicy file_cache_miss_policy) {
-    if (result_block.is_empty_column()) [[likely]] {
-        result_block = Block(slots, request_block_desc.row_id_size());
-    }
-    TabletSchema full_read_schema;
-    for (const ColumnPB& column_pb : request_block_desc.column_descs()) {
-        full_read_schema.append_column(TabletColumn(column_pb));
+// Both storage reads and pool submission can throw. Convert exceptions on the
+// same pthread where they originated, before publishing completion.
+template <typename Func>
+Status rowid_read_status(Func&& func) {
+    Status status;
+    try {
+        ASSIGN_STATUS_IF_CATCH_EXCEPTION(status = func(), status);
+    } catch (const std::exception& e) {
+        status = Status::InternalError("Row id fetch failed because {}", 
e.what());
     }
+    return status;
+}
 
-    std::unordered_map<IteratorKey, IteratorItem, HashOfIteratorKey> 
iterator_map;
-    std::unordered_map<SegKey, SegItem, HashOfSegKey> seg_map;
-    std::string row_store_buffer;
-    RowStoreReadStruct row_store_read_struct(row_store_buffer);
-    if (request_block_desc.fetch_row_store()) {
-        for (int i = 0; i < request_block_desc.slots_size(); ++i) {
-            
row_store_read_struct.serdes.emplace_back(slots[i].get_data_type_ptr()->get_serde());
-            row_store_read_struct.col_uid_to_idx[slots[i].col_unique_id()] = i;
-            
row_store_read_struct.default_values.emplace_back(slots[i].col_default_value());
+struct RowIdReadDispatch {
+    size_t task_count = 0;
+    std::function<Status(size_t)> run_task;
+    std::function<void(Status)> on_complete;
+    std::atomic<size_t> next_task = 0;
+    // The submitting thread owns one reference until all submissions finish.
+    // Even inline/fast workers cannot finish the RPC during submission.
+    std::atomic<size_t> remaining = 1;
+    AtomicStatus status;
+
+    ~RowIdReadDispatch() {
+        // Pool shutdown may destroy queued closures without executing them.
+        // The last closure can disappear only after all running reads have 
exited.
+        if (on_complete) {
+            SCOPED_INIT_THREAD_CONTEXT();
+            std::optional<AttachTask> task_context;
+            if (!thread_context()->is_attach_task()) {
+                
task_context.emplace(ExecEnv::GetInstance()->rowid_storage_reader_tracker());
+            }
+            status.update(Status::Cancelled("Row id read tasks discarded by 
pool shutdown"));
+            complete();
         }
     }
 
-    // Phase 1: Group all row_ids by their (tablet_id, rowset_id, segment_id) 
key.
-    // Unlike the old code which only batched adjacent rows with the same 
file_id,
-    // this merges non-contiguous same-segment requests into a single batch,
-    // maximizing the number of rows read per seek_and_read_by_rowid call.
-    std::vector<DorisFormatReadBatch> scan_batches;
-    std::unordered_map<SegKey, size_t, HashOfSegKey> batch_idx_by_seg;
-    // (batch_idx, position_in_batch) for each row in the original request.
-    std::vector<std::pair<size_t, size_t>> 
row_id_block_idx(request_block_desc.row_id_size());
-    for (int j = 0; j < request_block_desc.row_id_size(); ++j) {
-        auto file_id = request_block_desc.file_id(j);
-        auto file_mapping = id_file_map->get_file_mapping(file_id);
-        if (!file_mapping) {
-            return Status::InternalError(
-                    "Backend:{} file_mapping not found, query_id: {}, file_id: 
{}",
-                    BackendOptions::get_localhost(), print_id(query_id), 
file_id);
-        }
+    void complete() {
+        // Release request buffers while the rowid tracker is attached, even if
+        // the pool retains finished task closures for longer.
+        auto completion = std::move(on_complete);
+        completion(status.status());
+        run_task = {};
+    }
 
-        // Derive segment key and group by it — rows from the same segment are 
batched together
-        // even if they are interleaved with rows from other segments in the 
request.
-        auto [tablet_id, rowset_id, segment_id] = 
file_mapping->get_doris_format_info();
-        SegKey seg_key {.tablet_id = tablet_id, .rowset_id = rowset_id, 
.segment_id = segment_id};
-        auto [it, inserted] = batch_idx_by_seg.emplace(seg_key, 
scan_batches.size());
-        if (inserted) {
-            // First time seeing this segment, create a new batch for it.
-            scan_batches.emplace_back();
-            scan_batches.back().file_mapping = file_mapping;
+    void finish_worker() {
+        if (remaining.fetch_sub(1) == 1) {
+            complete();
         }
-        // Record (row_id, original_request_index) for later sorting and 
scattering.
-        
scan_batches[it->second].row_ids_with_positions.emplace_back(request_block_desc.row_id(j),
-                                                                     j);
     }
+};
+
+} // namespace
 
-    // Phase 2: For each segment, sort row_ids ascending (required by 
ColumnIterator),
-    // deduplicate, then read all rows in a single batch call.
-    std::vector<Block> scan_blocks(scan_batches.size());
-    for (size_t batch_idx = 0; batch_idx < scan_batches.size(); ++batch_idx) {
-        auto& scan_batch = scan_batches[batch_idx];
-        auto& row_ids_with_positions = scan_batch.row_ids_with_positions;
-        std::sort(row_ids_with_positions.begin(), row_ids_with_positions.end(),
-                  [](const auto& lhs, const auto& rhs) { return lhs.first < 
rhs.first; });
-
-        // Column iterators read rowids monotonically. Deduplicate consecutive 
identical row_ids
-        // (different file_ids may map to the same row), then scatter rows 
back to their original
-        // request positions.
-        std::vector<uint32_t> row_ids;
-        row_ids.reserve(row_ids_with_positions.size());
-
-        // Also builds the scatter map: row_id_block_idx[original_request_idx] 
->
-        // (batch_idx, deduplicated_position_in_batch).
-        for (const auto& [row_id, result_idx] : row_ids_with_positions) {
-            if (row_ids.empty() || row_ids.back() != row_id) {
-                row_ids.emplace_back(row_id);
+void RowIdStorageReader::submit_internal_read_tasks(FifoThreadPool* pool, 
size_t task_count,
+                                                    int concurrency,
+                                                    
std::function<Status(size_t)> run_task,
+                                                    
std::function<void(Status)> on_complete) {
+    std::shared_ptr<RowIdReadDispatch> dispatch;
+    auto setup_status = rowid_read_status([&]() {
+        dispatch = std::make_shared<RowIdReadDispatch>();
+        dispatch->task_count = task_count;
+        dispatch->run_task = std::move(run_task);
+        dispatch->on_complete = std::move(on_complete);
+        return Status::OK();
+    });
+    if (!setup_status.ok()) {
+        on_complete(setup_status);
+        return;
+    }
+    // Launch only a bounded number of workers, each pulling the next segment 
range.
+    // Never block a pool thread waiting for work submitted to its own pool.
+    DCHECK_GT(concurrency, 0);
+    const size_t workers = std::min(task_count, 
static_cast<size_t>(concurrency));
+    for (size_t i = 0; i < workers; ++i) {
+        dispatch->remaining.fetch_add(1);
+        auto status = rowid_read_status([&]() {
+            // FifoThreadPool has no fallible bookkeeping after publishing a 
task.
+            // A rejection or exception therefore means this worker will never 
run.
+            if (!pool->try_offer([dispatch]() {
+                    std::optional<AttachTask> task_context;
+                    dispatch->status.update(rowid_read_status([&]() {
+                        task_context.emplace(
+                                
ExecEnv::GetInstance()->rowid_storage_reader_tracker());
+                        while (dispatch->status.ok()) {
+                            const auto idx = dispatch->next_task.fetch_add(1);
+                            if (idx >= dispatch->task_count) {
+                                break;
+                            }
+                            RETURN_IF_ERROR(dispatch->run_task(idx));
+                        }
+                        return Status::OK();
+                    }));
+                    dispatch->finish_worker();
+                })) {
+                return Status::Error<ErrorCode::SERVICE_UNAVAILABLE>(
+                        "Row id fetch queue full or pool stopped: {}", 
pool->get_info());
             }
-            row_id_block_idx[result_idx] = std::make_pair(batch_idx, 
row_ids.size() - 1);
+            return Status::OK();
+        });
+        if (!status.ok()) {
+            dispatch->status.update(status);

Review Comment:
   Fixed in ad135bf83bfcdbe6995b1cdb2229e51da3119b8f.
   
   The dispatch now moves the first error through a noexcept path and retains a 
preallocated cancellation status if reporting an error itself throws. Rejected 
submissions retire through RAII, and post-admission exceptions retire the 
submitter through the dispatch instead of unwinding into premature RPC 
completion. The parent error path releases only its shared guard ownership; 
accepted reads retain the RPC. FIFO dequeue now moves the published callable 
instead of copying it.
   
   Validation: 59 related ASAN BE unit tests passed, including an injected 
exception at error publication with accepted workers, the same fault with an 
active worker in a real saturated FIFO, and a callable whose copy constructor 
throws after publication. Completion occurs once and only after accepted reads 
exit.



##########
be/src/service/internal_service.cpp:
##########
@@ -2006,33 +2012,54 @@ void 
PInternalService::multiget_data_v2(google::protobuf::RpcController* control
         return;
     }
 
-    doris::TaskScheduler* exec_sched = nullptr;
-    ScannerScheduler* scan_sched = nullptr;
-    ScannerScheduler* remote_scan_sched = nullptr;
-    wg->get_query_scheduler(&exec_sched, &scan_sched, &remote_scan_sched);
-    DCHECK(remote_scan_sched);
-
-    st = remote_scan_sched->submit_scan_task(
-            SimplifiedScanTask(
-                    [request, response, done]() {
-                        
SCOPED_ATTACH_TASK(ExecEnv::GetInstance()->rowid_storage_reader_tracker());
-                        signal::set_signal_task_id(request->query_id());
-                        // multi get data by rowid
-                        MonotonicStopWatch watch;
-                        watch.start();
-                        brpc::ClosureGuard closure_guard(done);
-                        response->mutable_status()->set_status_code(0);
-                        Status st = 
RowIdStorageReader::read_by_rowids(*request, response);
-                        st.to_protobuf(response->mutable_status());
-                        LOG(INFO) << "multiget_data finished, cost(us):"
-                                  << watch.elapsed_time() / 1000;
-                        return true;
-                    },
-                    nullptr, nullptr),
-            fmt::format("{}-multiget_data_v2", print_id(request->query_id())));
+    // Retain the RPC until every asynchronous internal read finishes.
+    auto closure_guard = std::make_shared<brpc::ClosureGuard>(done);
+    // A queued parent task may also be discarded during pool shutdown.
+    Status::Cancelled("Row id fetch task discarded before execution")
+            .to_protobuf(response->mutable_status());
+    auto task = [this, request, response, closure_guard]() {
+        
SCOPED_ATTACH_TASK(ExecEnv::GetInstance()->rowid_storage_reader_tracker());
+        signal::set_signal_task_id(request->query_id());
+        MonotonicStopWatch watch;
+        watch.start();
+        Status status;
+        try {
+            ASSIGN_STATUS_IF_CATCH_EXCEPTION(
+                    RowIdStorageReader::read_by_rowids(
+                            *request, response, &_rowid_fetch_pool,
+                            [response, closure_guard, watch](Status result) {
+                                result.to_protobuf(response->mutable_status());
+                                LOG(INFO) << "multiget_data finished, 
cost(us):"
+                                          << watch.elapsed_time() / 1000;
+                                // Complete while the rowid tracker is 
attached.
+                                closure_guard->reset(nullptr);
+                            }),
+                    status);
+        } catch (const std::exception& e) {
+            status = Status::InternalError("Row id fetch failed because {}", 
e.what());
+        }
+        if (!status.ok()) {
+            status.to_protobuf(response->mutable_status());
+            closure_guard->reset(nullptr);
+        }
+    };
+    // Use the dedicated pool for both preparation and internal reads. 
Preparation
+    // returns after submitting its workers, so even a one-thread pool can 
progress.
+    try {
+        ASSIGN_STATUS_IF_CATCH_EXCEPTION(
+                {
+                    if (!_rowid_fetch_pool.try_offer(std::move(task))) {

Review Comment:
   This finding is not fixed by ad135bf83bfcdbe6995b1cdb2229e51da3119b8f; that 
revision addresses P1-1 and P1-3 only.
   
   The BE-wide rowid pool still does not use the request Workload Group's 
CgroupCpuCtl or per-group admission limits, including the default serial path. 
The new external-rejection test proves that a rejected child no longer pins a 
shared worker; it does not establish Workload Group CPU/admission isolation. 
This concern still needs a separate execution/admission change and a two-group 
isolation test.



##########
be/src/service/internal_service.cpp:
##########
@@ -2006,33 +2012,54 @@ void 
PInternalService::multiget_data_v2(google::protobuf::RpcController* control
         return;
     }
 
-    doris::TaskScheduler* exec_sched = nullptr;
-    ScannerScheduler* scan_sched = nullptr;
-    ScannerScheduler* remote_scan_sched = nullptr;
-    wg->get_query_scheduler(&exec_sched, &scan_sched, &remote_scan_sched);
-    DCHECK(remote_scan_sched);
-
-    st = remote_scan_sched->submit_scan_task(
-            SimplifiedScanTask(
-                    [request, response, done]() {
-                        
SCOPED_ATTACH_TASK(ExecEnv::GetInstance()->rowid_storage_reader_tracker());
-                        signal::set_signal_task_id(request->query_id());
-                        // multi get data by rowid
-                        MonotonicStopWatch watch;
-                        watch.start();
-                        brpc::ClosureGuard closure_guard(done);
-                        response->mutable_status()->set_status_code(0);
-                        Status st = 
RowIdStorageReader::read_by_rowids(*request, response);
-                        st.to_protobuf(response->mutable_status());
-                        LOG(INFO) << "multiget_data finished, cost(us):"
-                                  << watch.elapsed_time() / 1000;
-                        return true;
-                    },
-                    nullptr, nullptr),
-            fmt::format("{}-multiget_data_v2", print_id(request->query_id())));
+    // Retain the RPC until every asynchronous internal read finishes.
+    auto closure_guard = std::make_shared<brpc::ClosureGuard>(done);
+    // A queued parent task may also be discarded during pool shutdown.
+    Status::Cancelled("Row id fetch task discarded before execution")
+            .to_protobuf(response->mutable_status());
+    auto task = [this, request, response, closure_guard]() {
+        
SCOPED_ATTACH_TASK(ExecEnv::GetInstance()->rowid_storage_reader_tracker());
+        signal::set_signal_task_id(request->query_id());
+        MonotonicStopWatch watch;
+        watch.start();
+        Status status;
+        try {
+            ASSIGN_STATUS_IF_CATCH_EXCEPTION(
+                    RowIdStorageReader::read_by_rowids(
+                            *request, response, &_rowid_fetch_pool,

Review Comment:
   Fixed in ad135bf83bfcdbe6995b1cdb2229e51da3119b8f.
   
   TimeSharingTaskExecutor now propagates admission failures and retires 
rejected splits from executor/handle running state, closing their futures 
outside the executor mutex. The one-callback scanner entry also removes the 
rejected task handle. Future-result bookkeeping is allocated before admission, 
and a queued split is withdrawn before reporting failure to create its first 
worker thread.
   
   Validation: 59 related ASAN BE unit tests passed. The real-scheduler 
saturation test accepts one child and rejects the next; the parent waits for 
the accepted child, returns an error, and a subsequent request on the shared 
FIFO progresses. Additional tests cover stopped schedulers, closed task 
handles, and first-thread creation failure without later execution of the 
rejected split.



-- 
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