github-actions[bot] commented on code in PR #66985:
URL: https://github.com/apache/doris/pull/66985#discussion_r3820398280
##########
be/src/exec/scan/scanner_context.cpp:
##########
@@ -559,10 +573,89 @@ void ScannerContext::_set_scanner_done() {
_dependency->set_always_ready();
}
-bool ScannerContext::_is_shared_scan_limit_exhausted() const {
+bool ScannerContext::is_shared_scan_limit_exhausted() const {
return limit >= 0 && _shared_scan_limit->load(std::memory_order_acquire)
<= 0;
}
+bool ScannerContext::is_context_queued(const std::unique_lock<std::mutex>&
transfer_lock) const {
+ DORIS_CHECK(transfer_lock.owns_lock());
+ return _is_context_queued;
+}
+
+void ScannerContext::set_context_queued(bool queued,
+ const std::unique_lock<std::mutex>&
transfer_lock) {
+ DORIS_CHECK(transfer_lock.owns_lock());
+ DORIS_CHECK(_is_context_queued != queued);
+ if (queued) {
+ // A Context is deduplicated while queued, so this timestamp covers
exactly one submitted
+ // runnable rather than the wait time of any particular scanner it may
later choose.
+ DORIS_CHECK(_context_wait_worker_start_ns == 0);
+ _context_wait_worker_start_ns = MonotonicNanos();
+ } else {
+ // A worker clears the state immediately after dequeueing the
runnable. Record the elapsed
+ // time here so queue-state changes and profiling cannot diverge.
Failed submissions never
+ // set this state, and therefore never enter this branch.
+ DORIS_CHECK(_context_wait_worker_start_ns != 0);
+#ifndef BE_TEST
+ DORIS_CHECK(_context_wait_worker_timer != nullptr);
+ COUNTER_UPDATE(_context_wait_worker_timer,
+ MonotonicNanos() - _context_wait_worker_start_ns);
+#endif
+ _context_wait_worker_start_ns = 0;
+ }
+ _is_context_queued = queued;
+}
+
+void ScannerContext::push_pending_scan_task(std::shared_ptr<ScanTask>
scan_task,
+ const
std::unique_lock<std::mutex>& transfer_lock) {
+ DORIS_CHECK(transfer_lock.owns_lock());
+ DORIS_CHECK(scan_task != nullptr);
+ DORIS_CHECK(scan_task->cached_block == nullptr);
+ DORIS_CHECK(!scan_task->is_eos());
+ // The state transition documents that this is an admission queue, not a
completed-result queue.
+ scan_task->set_state(ScanTask::State::PENDING);
+ _pending_tasks.push(std::move(scan_task));
+}
+
+std::shared_ptr<ScanTask> ScannerContext::try_get_next_scan_task(
+ const std::unique_lock<std::mutex>& transfer_lock) {
+ DORIS_CHECK(transfer_lock.owns_lock());
+ if (done() || _pending_tasks.empty()) {
Review Comment:
[P1] Recompute adaptive concurrency on the ThreadPool path
`expected_scanners` starts at 0 and is assigned only by
`_available_pickup_scanner_count()`, which is reached from the TaskExecutor
`_get_margin()` path. The new default ThreadPool path calls
`try_get_next_scan_task()` directly, so this branch always substitutes
`_max_scan_concurrency` and never feeds updated block estimates back through
`MemLimiter`/`MemShareArbitrator`. Because `enable_adaptive_scan` defaults to
true and file scans default to 16 scanners per context, a context whose memory
budget permits one scanner can still admit all 16, defeating the adaptive OOM
protection. Please compute/refresh the adaptive limit during ThreadPool
admission and cover it with a real ThreadPool adaptive test.
##########
be/src/exec/scan/simplified_scan_scheduler.cpp:
##########
@@ -34,7 +36,73 @@ 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();
+ }
+
+ // 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.
+ Status status;
+ if (_is_stop) {
+ status = Status::InternalError<false>("scanner pool {} is shutdown.",
_sched_name);
+ } else {
Review Comment:
[P2] Preserve the scanner queue-rejection error contract
This direct `submit_func()` path bypasses `ScannerScheduler::submit()`,
which intentionally converts pool submission failures to `TOO_MANY_TASKS` and
adds the scanner storage type. With both scheduler defaults changed to
ThreadPool, ordinary local and remote scan saturation now surfaces raw
`SERVICE_UNAVAILABLE` instead, so error handling and diagnostics depend on the
selected scheduler. Please normalize capacity rejection consistently here
(while keeping true shutdown distinct if intended) and add a saturated-pool
parity test.
##########
be/src/exec/scan/simplified_scan_scheduler.cpp:
##########
@@ -34,7 +36,73 @@ 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);
+ }
Review Comment:
[P1] Submit a context only when it can admit work
The last EOS is consumed before `get_block_from_queue()` performs its
terminal check, and that path calls `schedule_scan_task(ctx, nullptr)`. Here,
with `_pending_tasks` empty and `_is_context_queued` false, we still call
`submit_func()`. If the workload-group scan pool is at capacity,
`ThreadPool::do_submit()` returns `SERVICE_UNAVAILABLE`, which propagates from
`get_block_from_queue()` and fails a query whose scan has already completed.
The same missing can-admit predicate also creates a no-op callback after each
admission that fills the Context concurrency cap. Please submit only when
pending work can currently be admitted under `transfer_lock`, and test final
EOS with a rejecting/full pool plus concurrency-one dispatch counts.
##########
be/src/exec/scan/simplified_scan_scheduler.cpp:
##########
@@ -34,7 +36,73 @@ 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();
+ }
+
+ // 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.
+ Status status;
+ if (_is_stop) {
+ status = Status::InternalError<false>("scanner pool {} is shutdown.",
_sched_name);
+ } else {
+ 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);
+ } else {
+ // 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());
+ }
+ return status;
+}
+
+void
ThreadPoolSimplifiedScanScheduler::_run_context(std::shared_ptr<ScannerContext>
scanner_ctx) {
+ std::shared_ptr<ScanTask> scan_task;
+ {
+ 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;
+ }
+
+ // Admission checks completed results, active tasks, adaptive limits,
and shared LIMIT while
+ // holding transfer_lock. A null task means the Context is currently
not allowed to run one.
+ scan_task = scanner_ctx->try_get_next_scan_task(transfer_lock);
+ if (scan_task == nullptr) {
+ return;
+ }
+
+ // 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.
+ // Releasing transfer_lock only after resubmission 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());
+ }
+ }
+ // The scan runs without transfer_lock so the operator and other Context
workers can continue
+ // consuming results and admitting work. Completion reacquires the lock
before publishing.
Review Comment:
[P2] Exclude consumer backpressure from scanner worker-wait time
On this path no code resets the scanner's wait watch at admission. The prior
attempt's `pause()` starts that watch when its block is produced, and the next
`resume()` adds everything since then, including time the completed block
waited for operator consumption. The aggregate scanner worker-wait counter and
`PerScannerWaitTime` therefore report downstream/consumer delay as thread-pool
queue delay; the new Context timer does not stop those existing counters.
Please reset or suppress the legacy scanner timer at the ThreadPool admission
boundary and test with a deliberately blocked consumer.
##########
be/src/exec/scan/simplified_scan_scheduler.cpp:
##########
@@ -34,7 +36,73 @@ 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();
+ }
+
+ // 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.
+ Status status;
+ if (_is_stop) {
+ status = Status::InternalError<false>("scanner pool {} is shutdown.",
_sched_name);
+ } else {
+ 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);
+ } else {
+ // 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());
+ }
+ return status;
+}
+
+void
ThreadPoolSimplifiedScanScheduler::_run_context(std::shared_ptr<ScannerContext>
scanner_ctx) {
+ std::shared_ptr<ScanTask> scan_task;
+ {
+ 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;
+ }
+
+ // Admission checks completed results, active tasks, adaptive limits,
and shared LIMIT while
+ // holding transfer_lock. A null task means the Context is currently
not allowed to run one.
+ scan_task = scanner_ctx->try_get_next_scan_task(transfer_lock);
+ if (scan_task == nullptr) {
+ return;
+ }
+
+ // 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.
+ // Releasing transfer_lock only after resubmission keeps the admission
decision atomic.
Review Comment:
[P1] Attach and catch the callback-side resubmission scope
Pinning `task_exec_ctx()` does not attach this worker to the query. After
admission increments `_in_flight_tasks_num`, this call allocates the next
`FunctionRunnable` while the thread is still on the Orphan tracker; the first
`SCOPED_ATTACH_TASK` and `RETURN_IF_CATCH_EXCEPTION` are only reached later
inside `_scanner_scan()`. With orphan checking enabled this violates the
callback-entry memory invariant, and an allocation/tracker exception escapes
`ThreadPool::dispatch_thread()` (which has no catch), terminating the process
without publishing the admitted task or releasing its slot. Please attach the
pre-execution callback scope and convert any failure after admission into a
completed error task, with fault-injected coverage.
##########
be/src/exec/scan/simplified_scan_scheduler.cpp:
##########
@@ -34,7 +36,73 @@ 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();
+ }
+
+ // 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.
+ Status status;
+ if (_is_stop) {
+ status = Status::InternalError<false>("scanner pool {} is shutdown.",
_sched_name);
+ } else {
+ 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);
+ } else {
Review Comment:
[P1] Handle submissions that fail after retaining the runnable
A non-OK `submit_func()` does not always mean that no worker can ever
dequeue this callback. `ThreadPool::do_submit()` first appends the task, then
creates a needed worker; if creation fails while the pool has zero workers, it
returns that error without removing the queued task. This branch therefore
leaves `_is_context_queued` false, but a later successful worker creation can
run the stale callback, whose `set_context_queued(false)` hits the
queue-state/start-time `DORIS_CHECK`s and crashes the BE. Please make
failed-after-enqueue submission transactional (or expose an accepted/queued
result that lets this code mark the callback) and add a deterministic
worker-creation failure/recovery test.
--
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]