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


##########
be/src/exec/scan/simplified_scan_scheduler.cpp:
##########
@@ -34,7 +38,108 @@ Status 
TaskExecutorSimplifiedScanScheduler::schedule_scan_task(
 Status ThreadPoolSimplifiedScanScheduler::schedule_scan_task(
         std::shared_ptr<ScannerContext> scanner_ctx, std::shared_ptr<ScanTask> 
current_scan_task,
         std::unique_lock<std::mutex>& transfer_lock) {
-    std::unique_lock<std::shared_mutex> wl(_lock);
-    return scanner_ctx->schedule_scan_task(current_scan_task, transfer_lock, 
wl);
+    // Unlike TaskExecutor, ThreadPool queues a Context runnable. It later 
admits one pending task
+    // under transfer_lock. This bounds queue entries to one per Context even 
when many scanners
+    // become runnable together.
+    DORIS_CHECK(transfer_lock.owns_lock());
+    if (current_scan_task != nullptr) {
+        // The operator has consumed a non-EOS result, making this scanner 
eligible for another
+        // scan attempt. Queue the scanner first; the Context runnable chooses 
it later.
+        scanner_ctx->push_pending_scan_task(std::move(current_scan_task), 
transfer_lock);
+    }
+    if (scanner_ctx->is_context_queued(transfer_lock)) {
+        // A queued runnable will see all pending scanners added before it 
obtains transfer_lock.
+        // Submitting another runnable would only duplicate work and distort 
Context queue latency.
+        return Status::OK();
+    }
+    if (!scanner_ctx->can_admit_scan_task(transfer_lock)) {
+        // No runnable is needed when the Context has no pending scanner or 
its concurrency slots
+        // are occupied. Completion or operator consumption will retry 
scheduling when state changes.
+        return Status::OK();
+    }
+
+    // transfer_lock prevents another producer from submitting concurrently. 
The worker callback
+    // also waits for this lock, so it cannot run between successful 
submission and marking queued.
+    if (_is_stop) {
+        // Shutdown must surface: progressing tasks may never complete on a 
stopped pool.
+        return Status::InternalError<false>("scanner pool {} is shutdown.", 
_sched_name);
+    }
+    Status status =
+            _scan_thread_pool->submit_func([this, scanner_ctx] { 
_run_context(scanner_ctx); });
+    if (status.ok()) {
+        // Start the Context wait interval only after submission succeeds. 
This excludes failed
+        // submit_func() calls, which never waited for a worker and must not 
affect the profile.
+        scanner_ctx->set_context_queued(true, transfer_lock);
+        return Status::OK();
+    }
+    // No worker can dequeue a rejected runnable. The Context remains 
unqueued, so a later
+    // scheduling attempt can submit it again without clearing state or 
accounting queue time.
+    LOG(WARNING) << fmt::format("Failed to submit scanner context {}, reason: 
{}",
+                                scanner_ctx->debug_string(), 
status.to_string());
+    if (scanner_ctx->has_progressing_task(transfer_lock) ||
+        scanner_ctx->is_shared_scan_limit_exhausted()) {

Review Comment:
   [P1] Do not discard the only wakeup after shared-LIMIT rejection
   
   This branch can also run during the Context's initial scheduling, when 
another parallel instance has already exhausted the shared LIMIT. In that state 
`can_admit_scan_task()` intentionally says yes because this Context still has 
pending scanners and no completed/in-flight task: one worker must publish EOS 
and wake its per-local-state scan dependency. If the pool is full (or 
first-worker creation fails), returning OK here leaves `_is_context_queued` 
false with no progress or retry owner. The dependency starts blocked, and after 
`open()` extracts it the pipeline stops before its first 
`get_block_from_queue()` call, so the claimed next call never happens and the 
query can hang. Please either finish/wake the Context synchronously or 
surface/preserve the failed submission when no task is progressing, and add a 
parallel-instance LIMIT-exhaustion plus rejection test.



##########
be/src/exec/scan/simplified_scan_scheduler.cpp:
##########
@@ -34,7 +38,108 @@ Status 
TaskExecutorSimplifiedScanScheduler::schedule_scan_task(
 Status ThreadPoolSimplifiedScanScheduler::schedule_scan_task(
         std::shared_ptr<ScannerContext> scanner_ctx, std::shared_ptr<ScanTask> 
current_scan_task,
         std::unique_lock<std::mutex>& transfer_lock) {
-    std::unique_lock<std::shared_mutex> wl(_lock);
-    return scanner_ctx->schedule_scan_task(current_scan_task, transfer_lock, 
wl);
+    // Unlike TaskExecutor, ThreadPool queues a Context runnable. It later 
admits one pending task
+    // under transfer_lock. This bounds queue entries to one per Context even 
when many scanners
+    // become runnable together.
+    DORIS_CHECK(transfer_lock.owns_lock());
+    if (current_scan_task != nullptr) {
+        // The operator has consumed a non-EOS result, making this scanner 
eligible for another
+        // scan attempt. Queue the scanner first; the Context runnable chooses 
it later.
+        scanner_ctx->push_pending_scan_task(std::move(current_scan_task), 
transfer_lock);
+    }
+    if (scanner_ctx->is_context_queued(transfer_lock)) {
+        // A queued runnable will see all pending scanners added before it 
obtains transfer_lock.
+        // Submitting another runnable would only duplicate work and distort 
Context queue latency.
+        return Status::OK();
+    }
+    if (!scanner_ctx->can_admit_scan_task(transfer_lock)) {
+        // No runnable is needed when the Context has no pending scanner or 
its concurrency slots
+        // are occupied. Completion or operator consumption will retry 
scheduling when state changes.
+        return Status::OK();
+    }
+
+    // transfer_lock prevents another producer from submitting concurrently. 
The worker callback
+    // also waits for this lock, so it cannot run between successful 
submission and marking queued.
+    if (_is_stop) {
+        // Shutdown must surface: progressing tasks may never complete on a 
stopped pool.
+        return Status::InternalError<false>("scanner pool {} is shutdown.", 
_sched_name);
+    }
+    Status status =
+            _scan_thread_pool->submit_func([this, scanner_ctx] { 
_run_context(scanner_ctx); });
+    if (status.ok()) {
+        // Start the Context wait interval only after submission succeeds. 
This excludes failed
+        // submit_func() calls, which never waited for a worker and must not 
affect the profile.
+        scanner_ctx->set_context_queued(true, transfer_lock);
+        return Status::OK();
+    }
+    // No worker can dequeue a rejected runnable. The Context remains 
unqueued, so a later
+    // scheduling attempt can submit it again without clearing state or 
accounting queue time.
+    LOG(WARNING) << fmt::format("Failed to submit scanner context {}, reason: 
{}",
+                                scanner_ctx->debug_string(), 
status.to_string());
+    if (scanner_ctx->has_progressing_task(transfer_lock) ||
+        scanner_ctx->is_shared_scan_limit_exhausted()) {
+        // Someone will retry: a progressing task completes, the operator 
consumes its result and
+        // reschedules; after shared LIMIT is exhausted, 
get_block_from_queue() finishes the
+        // Context on its next call. Pool saturation must not fail a query 
that still progresses.
+        return Status::OK();
+    }
+    // Nothing will retry this Context. Surface the failure, normalized like
+    // ScannerScheduler::submit() so both schedulers report saturation as 
TOO_MANY_TASKS.
+    return Status::TooManyTasks("Failed to submit scanner context {} to 
scanner pool, reason: {}",
+                                scanner_ctx->ctx_id, status.msg());
+}
+
+void 
ThreadPoolSimplifiedScanScheduler::_run_context(std::shared_ptr<ScannerContext> 
scanner_ctx) {
+    std::shared_ptr<ScanTask> scan_task;
+    Status admission_status = [&]() -> Status {
+        std::unique_lock<std::mutex> 
transfer_lock(scanner_ctx->transfer_lock());
+        // The worker has dequeued the Context. Clearing the marker also 
charges its queue latency:
+        // the interval from successful submit_func() to worker start, not 
scanner execution time.
+        scanner_ctx->set_context_queued(false, transfer_lock);
+
+        auto task_execution_lock = scanner_ctx->task_exec_ctx();
+        if (task_execution_lock == nullptr) {
+            return Status::OK();
+        }
+#ifndef BE_TEST
+        // Attach before admission: allocations below (for example the 
resubmitted
+        // FunctionRunnable) must charge the query rather than the orphan 
tracker. Scoped to this
+        // lambda so it detaches before execute_scan_task(), whose 
_scanner_scan() attaches again.
+        SCOPED_ATTACH_TASK(scanner_ctx->state());
+#endif
+        RETURN_IF_CATCH_EXCEPTION({
+            // Admission checks completed results, active tasks, adaptive 
limits, and shared LIMIT
+            // while holding transfer_lock. A null task means the Context may 
not run one now.
+            scan_task = scanner_ctx->try_get_next_scan_task(transfer_lock);
+            if (scan_task != nullptr) {
+                
DBUG_EXECUTE_IF("ThreadPoolSimplifiedScanScheduler._run_context.inject_failure",
 {
+                    throw Exception(ErrorCode::INTERNAL_ERROR, "injected 
admission failure");
+                });
+                // Queue the next Context runnable before executing this task. 
Example: with a
+                // concurrency limit of two, the next worker may admit scanner 
B while this worker
+                // scans scanner A. Holding transfer_lock keeps the admission 
decision atomic.
+                Status resubmit_status = schedule_scan_task(scanner_ctx, 
nullptr, transfer_lock);
+                if (!resubmit_status.ok()) {
+                    LOG(WARNING) << fmt::format("Failed to resubmit scanner 
context {}, reason: {}",
+                                                scanner_ctx->ctx_id, 
resubmit_status.to_string());
+                }
+            }
+        });
+        return Status::OK();
+    }();
+    if (scan_task == nullptr) {
+        return;

Review Comment:
   [P1] Surface admission errors before the null-task return
   
   `try_get_next_scan_task()` can throw before assigning `scan_task`, but this 
return discards the resulting non-OK `admission_status` after the callback has 
already cleared the Context's queued marker. This has a concrete production 
path: adaptive admission calls `MemLimiter::available_scanner_count()`, whose 
checked `cast_set<int>(per_count)` throws for an accepted extreme 
`exec_mem_limit` (for example 1 EiB with the default 0.3 scan ratio and 64 MiB 
initial estimate). The query tracker is clamped to process memory, but 
`MemShareArbitrator` is built from the raw option, so this overflow remains 
reachable. With no completed/in-flight task, pending scanners and the initially 
blocked dependency are then left with no error publication or retry owner, so 
the query can hang. Please handle `admission_status` before the null return and 
wake/publish a Context-level failure; add an injection before task assignment 
as well as the post-assignment case.



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