mrhhsg commented on code in PR #68610:
URL: https://github.com/apache/doris/pull/68610#discussion_r4280990015


##########
be/src/exec/scan/scanner.cpp:
##########
@@ -87,8 +89,17 @@ Status Scanner::init(RuntimeState* state, const 
VExprContextSPtrs& conjuncts) {
     return Status::OK();
 }
 
+// Keep the single-use padding loop inline with its projection and LIMIT 
accounting.
+// NOLINTNEXTLINE(readability-function-cognitive-complexity)
 Status Scanner::get_block_after_projects(RuntimeState* state, Block* block, 
bool* eos) {
     
SCOPED_CONCURRENCY_COUNT(ConcurrencyStatsManager::instance().vscanner_get_block);
+    // Peer scanners may have exhausted the shared LIMIT while this scanner 
waited to be
+    // scheduled. Admission does not look at the LIMIT, so the scanner returns 
an empty block
+    // instead of reading on. It does not report EOS: the operator already 
holds enough rows and
+    // stops the scanners after cutting the LIMIT itself.
+    if (_shared_scan_limit_exhausted()) {

Review Comment:
   Fixed in 62afc6b03b7. The exhausted scanner is now parked instead of 
resubmitted: `ScannerContext::get_block_from_queue()` pushes a consumed non-EOS 
task back to `_pending_tasks` without calling `schedule_scan_task()` when 
`_is_shared_scan_limit_exhausted()`. This covers both schedulers, since the 
ThreadPool path (`push_pending_scan_task()` + Context runnable, where admission 
ignores the LIMIT by design) and the TaskExecutor path 
(`_pull_next_scan_task()` returns the current task unconditionally) both 
resubmit from this one call.
   
   Progress is still guaranteed from the same call: the branch is only reached 
when the finish check right above it did not fire, i.e. there is a completed or 
in-flight task left for this context. Its completion wakes the operator 
(`push_completed_scan_task()` → `set_ready()`), and the context finishes once 
the counter is exhausted with nothing in flight, exactly as before. The parked 
scanner never runs again; the scanner itself still only stops reading and never 
reports EOS for the LIMIT.
   
   Test: 
`ScannerContextTest.exhausted_shared_limit_parks_empty_task_until_in_flight_drains`
 drives this interleaving through `get_block_from_queue()`: with the 
operator-wide counter at 0, scanner B in flight and scanner A completing empty 
without EOS, A ends up PENDING, the scheduler's `schedule_scan_task()` is 
called zero times, and the context finishes (`eos == true`) when B's completion 
drains with A still parked. The scan UT filter passed, and `query_p0/limit` 
plus `correctness_p0/test_shared_scan_limit_pending_tasks` pass on a local 
cluster with the ThreadPool default.
   



##########
be/test/exec/rowid_fetcher_test.cpp:
##########
@@ -429,4 +438,64 @@ TEST_F(SubmitExternalScanTasksTest, 
ThrownExceptionReachesTheCaller) {
     EXPECT_NE(result.to_string().find("scanner threw"), std::string::npos);
 }
 
+// multiget_data_v2 runs read_by_rowids() on the remote scan scheduler, which 
by default is a
+// ThreadPool whose worker has no exception boundary. A request whose file 
mapping exists but
+// whose slot carries a PTypeNode this BE cannot decode makes SlotDescriptor's 
constructor throw;
+// the RPC helper must turn that into an error status on the response instead 
of letting the
+// exception leave the worker and terminate the process.
+TEST_F(RowIdStorageReaderTest, 
RpcHelperReportsThrownExceptionOnThreadPoolPath) {
+    IdManager id_manager;
+    ExecEnv* env = ExecEnv::GetInstance();
+    IdManager* saved_id_manager = env->get_id_manager();
+    env->set_id_manager(&id_manager);
+    Defer restore_id_manager {[&] { env->set_id_manager(saved_id_manager); }};
+
+    PMultiGetRequestV2 request;
+    request.mutable_query_id()->set_hi(68610);
+    request.mutable_query_id()->set_lo(1);
+    request.set_wg_id(1);
+    auto id_file_map = id_manager.add_id_file_map(request.query_id(), 1024);
+    const uint32_t file_id = id_file_map->get_file_mapping_id(
+            std::make_shared<FileMapping>(/*tablet_id=*/1, RowsetId(), 
/*segment_id=*/0));
+
+    PRequestBlockDesc* block_desc = request.add_request_block_descs();
+    block_desc->add_file_id(file_id);
+    block_desc->add_row_id(0);
+    PSlotDescriptor* slot = block_desc->add_slots();
+    slot->set_id(0);
+    slot->set_parent(0);
+    slot->set_column_pos(0);
+    slot->set_byte_offset(0);
+    slot->set_null_indicator_byte(0);
+    slot->set_null_indicator_bit(-1);
+    slot->set_col_name("c");
+    slot->set_slot_idx(0);
+    // A type node kind unknown to this BE, as a newer FE could send.
+    slot->mutable_slot_type()->add_types()->set_type(99);
+
+    std::shared_ptr<CgroupCpuCtl> cgroup_cpu_ctl = 
std::make_shared<CgroupV2CpuCtl>(1);
+    ThreadPoolSimplifiedScanScheduler scheduler("rowid_rpc_test", 
cgroup_cpu_ctl);
+    ASSERT_TRUE(scheduler.start(1, 1, 4, 1).ok());
+    Defer stop_scheduler {[&] { scheduler.stop(); }};

Review Comment:
   Fixed in 62afc6b03b7. `response` and `finished` are now declared before the 
scheduler and its `stop_scheduler` guard in 
`RowIdStorageReaderTest.RpcHelperReportsThrownExceptionOnThreadPoolPath`, so on 
every exit, including a failed `ASSERT_TRUE(finished.wait_for(...))`, 
`scheduler.stop()` joins the worker before either captured object is destroyed. 
The test still passes on the real ThreadPool scheduler.
   



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