Skip to content

[improvement](be) Use thread pool scan scheduler by default - #68610

Merged
mrhhsg merged 9 commits into
apache:masterfrom
mrhhsg:improve/default-thread-pool-scan-scheduler
Oct 19, 2026
Merged

mrhhsg merged 9 commits into
apache:masterfrom
mrhhsg:improve/default-thread-pool-scan-scheduler

Conversation

@mrhhsg

@mrhhsg mrhhsg commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: None

Related PR: #51690, #58144, #58273, #65814, #67070

Problem Summary: Workload groups pick the scan scheduler for internal and
external table scans from enable_task_executor_in_internal_table and
enable_task_executor_in_external_table. Both configs currently default to
true on master, so every workload group creates
TaskExecutorSimplifiedScanScheduler (the time-sharing task executor) for
local (ls_*) and remote (rs_*) scans. The time-sharing executor is still
maturing (for example the self-deadlock fixed in #58273; the internal-table
default was already reverted once in #58144), so the default should be the
ThreadPoolSimplifiedScanScheduler for both internal and external tables.

This change sets both configs' default values to false. The task executor
can still be enabled explicitly through be.conf.

Making the thread pool path the default exposed four gaps.

  1. Pending scanners after an exhausted shared LIMIT. For a SELECT ... LIMIT n
    over many tablets, the thread pool admission (ScannerContext::can_admit_scan_task,
    rewritten in [improvement](be) Refactor thread-pool scan scheduling #67070) keeps admitting the remaining pending scanners after peer
    scanners have exhausted the shared scan LIMIT. Admission is kept that way on
    purpose: holding pending scanners back based on the in-flight or completed
    task counters, as [fix](be) Finish pending scanners after shared limit #65814 does on the TaskExecutor path, ties the schedule to
    bookkeeping that is easy to get wrong and can leave a context with nothing to
    wake the operator. Instead, Scanner::get_block_after_projects() now checks
    the shared LIMIT before it reads: once the counter is exhausted, the scanner
    returns an empty block, so an admitted scanner never reads its range for rows
    the operator will cut anyway. It does not report EOS for that: EOS means the
    underlying read is exhausted, while the LIMIT is applied by the scan operator,
    which stops the scanners after reached_limit();
    ScannerContext::get_block_from_queue() also finishes the context once the
    counter is exhausted with nothing in flight. The two pre-existing checks in
    Scanner::get_block() that set eof from the shared counter are removed for
    the same reason, so the shared LIMIT is handled in one place.

  2. Rows held in scanner padding under a shared LIMIT. Scanner::get_block()
    charges the shared LIMIT counter as soon as rows pass the filters, but with
    projections get_block_after_projects() may keep a small block in
    _padding_block and emit a later block first. Once peer scanners exhaust the
    counter, the context reaches EOS without running that scanner again, and the
    charged rows it holds are lost; for example, a parallel projected
    LIMIT 4096 could return 4086 rows. The counter is now decremented only by
    the rows a call actually returns, so held padding is never charged and the
    counter equals "LIMIT minus returned rows" between calls. The padding batch
    target is capped by the remaining LIMIT when a call starts, so a scanner does
    not read on just to fill half a batch when fewer rows are needed, and the
    read loop stops as soon as peers exhaust the LIMIT, returning the rows it
    already holds. Projected scans with selective filters keep coalescing small
    blocks. The scan does not cap its output at the LIMIT (the operator cuts
    extra rows), so no further accounting is needed.

  3. Exceptions in the row-ID fetch RPC callback. multiget_data_v2 submits
    its callback to the remote scan scheduler; on a ThreadPool worker there is
    no exception boundary, and RowIdStorageReader::read_by_rowids() constructs
    SlotDescriptor(pslot) before its local try, so an unknown
    PTypeNode.type would throw doris::Exception and terminate the BE. The
    callback now calls RowIdStorageReader::read_by_rowids_for_rpc(), which
    converts a thrown exception into an error status on the response before the
    closure completes the RPC. ExecEnv::set_id_manager() is added under
    BE_TEST for the unit test.

  4. The shared LIMIT pointer of a scanner initialized while the counter is
    negative. ScanLocalState::shared_scan_limit_ptr() returned null whenever
    the live counter was negative, and Scanner::init() caches that pointer
    once. Concurrent scanners can return more than the LIMIT before the operator
    makes its final cut, leaving the counter negative, so a scanner whose local
    state opened at that moment never checked or charged the shared LIMIT and
    could read a long selective tail after peers had satisfied the query.
    The pointer is now returned whenever the operator has a SQL LIMIT, which is
    how ScannerContext already decides it.

Release note

The BE configs enable_task_executor_in_internal_table and
enable_task_executor_in_external_table now default to false; internal
and external table scans use the thread pool scan scheduler by default.

Check List (For Author)

  • Test:
    • Unit Test: added WorkloadGroupManagerTest.DefaultScanSchedulersUseThreadPool,
      ScannerContextTest.thread_pool_admission_ignores_shared_limit,
      ScannerProjectionTest.shared_limit_exhausted_before_call_returns_empty_block_without_eos,
      ScannerProjectionTest.shared_limit_counts_only_emitted_rows,
      ScannerProjectionTest.shared_limit_drops_padding_after_peer_satisfies_limit,
      ScannerProjectionTest.shared_limit_flushes_padding_before_reading_more,
      ScannerProjectionTest.shared_limit_flushes_merged_padding_at_limit,
      ScannerProjectionTest.shared_limit_counts_returned_rows_without_projection,
      ScannerProjectionTest.shared_limit_counts_returned_rows_without_padding,
      ScannerProjectionTest.shared_limit_keeps_padding_small_blocks,
      ScannerProjectionTest.shared_limit_exhausted_by_own_rows_stops_reading_without_eos,
      ScannerProjectionTest.shared_limit_exhausted_by_peer_mid_read_returns_held_rows_without_eos,
      ScannerProjectionTest.shared_limit_observed_by_scanner_initialized_under_negative_counter,
      ScannerProjectionTest.no_shared_limit_without_sql_limit
      and RowIdStorageReaderTest.RpcHelperReportsThrownExceptionOnThreadPoolPath
      (runs the RPC helper on a real ThreadPoolSimplifiedScanScheduler;
      the test process aborts without the fix);
      ran ScannerProjectionTest.*, ScannerContextTest.*,
      ScannerLateArrivalRfTest.*, WorkloadGroupManagerTest.*,
      RowIdStorageReaderTest.*, SubmitExternalScanTasksTest.*
    • Regression test: ran on a local cluster with the new
      default: query_p0/limit (19 suites) and
      correctness_p0/test_shared_scan_limit_pending_tasks
  • Behavior changed: Yes (default scan scheduler for internal and external
    table scans changes from the task executor to the thread pool; it can be
    switched back via the two BE configs)
  • Does this need documentation: No

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@mrhhsg

mrhhsg commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

/review-light

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complete static review of PR #68610 at head 7e6d7e5764fa24c2fe7c74a9607a8ee2c02b010e. Request changes: two new P1 findings below; no existing P0/P1 inline finding applies. The focus file contains only -light and specifies no additional technical point, so I reviewed all four changed files and their relevant call paths.

Goal, scope, and tests. The two configuration defaults select ThreadPool schedulers for newly created local and remote workload-group scans, and the admission guard tries to avoid opening scanners after a shared LIMIT is exhausted. The patch is small, but the default switch exposes all ordinary scans and direct remote scheduler callers. The new unit tests assert registered defaults, scheduler types, and admission states; they do not execute a remote RPC under worker-creation failure or produce projected rows with buffered padding. No test result files changed. This review did not build or run tests, as the review contract prohibits both; author-reported test runs were not independent validation.

Correctness, error handling, and memory safety. The guard can end a projected parallel scan with rows already charged to the LIMIT still held in a scanner's private padding buffer (P1 inline finding). The remote ThreadPool can report submission failure after retaining a multiget_data_v2 callback, so the RPC may complete and later run a callback using released request/response/completion pointers (P1 inline finding). Other changed-path statuses and the wakeup/EOS handoff were traced; no separate lost wakeup or silent error was substantiated. The suggested nested RowId stack-reference variant was rejected because it runs from an already active worker.

Concurrency and lifecycle. Scanner workers, Context runnables, and the pipeline operator share ScannerContext; its pending/completed/in-flight transitions and dependency wakeups use _transfer_lock, and the shared LIMIT is atomic. The row-loss issue is an ordering/accounting error rather than a data race. Workload-group scheduler creation, resizing, and stopping use its scheduler lock; ThreadPool protects its queue. The RPC finding is a callback ownership failure after a zero-worker submission error. No new cross-translation-unit static initialization dependency, ownership cycle, or independent lock-order issue was found.

Configuration, parallel paths, and compatibility. These are process-start DEFINE_Bool defaults; an explicit true still selects TaskExecutor, and no dynamic-update behavior is promised. I traced local scans, file scans, direct remote submissions, and both scheduler branches. The projected LIMIT example requires a nonserial scan, reachable with enable_adaptive_pipeline_task_serial_read_on_limit=false or eligible predicates. The existing TaskExecutor pending-scanner check does not justify the new ThreadPool guard when a scanner holds counted padding. The patch adds no FE-BE variable, wire or storage format, glibc ABI requirement, transaction/write/persistence path, or visible-version/delete-bitmap change. It changes no BE hub header, gate table, PCH, or explicit template list; the added header include is confined to a test source file.

Performance and observability. Avoiding unnecessary scanner setup is useful, and the new guard is constant time, but it must preserve buffered rows. Existing worker/queue/submission metrics and scan profiles offer operational context; neither prevents the retained callback or makes a successful short LIMIT result safe. No additional distinct resource-bound or logging defect was substantiated. Three review rounds completed, with the final normal and risk-focused reviews returning NO_NEW_VALUABLE_FINDINGS; all candidates were accepted, deduplicated, or dismissed with code evidence.

Comment thread be/src/common/config.cpp Resolved
Comment thread be/src/exec/scan/scanner_context.cpp Resolved
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: `ThreadPool::do_submit()` queues the task before it
creates a worker. If creating the worker fails and the pool has no thread
at all (for example a pool whose minimum workers could not be created in
`init()`), the submit returns the error but leaves the task queued. Callers
treat a failed submit as "the task will never run"; `multiget_data_v2`, for
instance, completes the RPC closure itself. A later successful submit then
starts a worker that runs the stale task too, using the released request,
response and closure and completing the RPC twice. This matters more now
that the thread pool scan scheduler is the default for workload groups.

On that failure path the submit now withdraws its own task from the token
queue (restoring the token/queue bookkeeping and moving the token back to
IDLE when it has no entries left) before returning the error. If a worker
started by a concurrent submit has already taken the task, the submit
reports success instead; a marker set when a worker takes the task tells
this apart from `shutdown()` having released the task without running it,
in which case the error is still returned. The failure path now also logs a warning and notifies
`_no_threads_cond`, so a `shutdown()` waiting for the last pending thread no
longer hangs when that thread fails to start. The scan scheduler comment
that relied on the old "error after retaining the runnable" behavior is
updated.

### Release note

None

### Check List (For Author)

- Test:
    - Unit Test: added `ThreadPoolTest.TestFailedSubmitWithoutWorkersNeverRuns`
      (injects thread creation failure through a new debug point, checks that
      failed submits through the pool and a serial token leave nothing queued
      and never run after later successful submits); ran `ThreadPoolTest.*`
- Behavior changed: No
- Does this need documentation: No
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610, apache#62222

Problem Summary: `Scanner::get_block()` subtracted every filtered block from
the shared scan LIMIT counter, but with projections
`Scanner::get_block_after_projects()` may keep a small block in
`_padding_block` and emit a later, larger block first. The buffered rows
were already charged although they had not left the scanner. Once peer
scanners exhausted the counter, `ScannerContext` stops admitting pending
scanners and declares EOS when no completed or in-flight task remains, so
the buffered rows were never emitted and a parallel projected
`SELECT ... LIMIT n` could return fewer than `n` rows. For example, with
LIMIT 4096, scanner A buffers 10 rows and emits 2500 (counter charged
2510), scanner B emits 1586 and exhausts the counter, and the query
returns 4086 rows.

The counter is now charged in `get_block_after_projects()` for the rows
that actually leave the scanner, and that call reports EOS when the counter
is exhausted. Rows still held in the padding block are no longer counted,
so an exhausted counter always means enough rows were emitted. The
trade-off is that rows a scanner is still padding (fewer than half a batch)
are not visible to its peers until they are emitted, so a scanner may read
up to that many rows more than before; results are unaffected.

### Release note

None

### Check List (For Author)

- Test:
    - Unit Test: added
      `ScannerProjectionTest.shared_limit_charges_only_emitted_rows` and
      `ScannerProjectionTest.shared_limit_reports_eos_when_emitted_rows_exhaust_it`; ran
      `ScannerProjectionTest.*`, `ScannerLateArrivalRfTest.*`,
      `ScannerContextTest.*`, `WorkloadGroupManagerTest.*`
    - Regression test: correctness_p0 (test_shared_scan_limit_pending_tasks,
      test_scan_topn_limit, test_subquery_with_limit) and query_p0/limit
      suites on a local cluster
- Behavior changed: No
- Does this need documentation: No
@mrhhsg

mrhhsg commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

/review-light

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complete static review of PR #68610 at 30617c27748ff789ca6adbee4fd0af7a7f8c305b

Two new P1 findings are inline: a successful ThreadPool submission can be stranded after overlapping worker-start failures, and projected small-LIMIT scans with residual filters can read complete ranges before buffered qualifying rows are charged. The two existing P1 threads (4129581696 and 4129581700) are resolved for their exact reported error-return/later-callback and counted-padding row-loss scenarios; these new findings have different outcomes and are not duplicate reports.

  • Goal and proof: The PR makes ThreadPool the default for new internal and external workload-group scan schedulers, aligns pending-task LIMIT admission, charges only emitted projection rows, and withdraws a single failed ThreadPool submission. The added tests cover scheduler selection, admission states, emitted-row accounting, and isolated failure/recovery. They do not cover overlapping failed starts or a small LIMIT with sparse residual predicates. I performed static inspection only; the review contract prohibits builds and tests, so the PR author's test claims are not independent validation here.
  • Scope, conditions, and parallel paths: Both defaults are existing non-dynamic flags; explicit true still selects TaskExecutor. Local and external scanners share ScannerScheduler::_scanner_scan(), while the remote scheduler also accepts multiget_data_v2 work. The new admission guard preserves its one-task wakeup escape. The shared ThreadPool helper affects all callers; M1 applies to accepted work. The projection wrapper is common to scanner types; M2 is reachable with an AGG-key scan, a residual non-key value predicate, and parallel scanners.
  • Concurrency and lifecycle: ThreadPool _lock protects queue, token, worker counts, and take markers, but worker creation occurs outside it; M1 is the resulting all-starts-fail interleaving. Context _transfer_lock protects admission and completion, and shared LIMIT progress is atomic. Cached scan results are published and drained before exhausted-counter EOS. No separate lock inversion, double release, ownership cycle, or cross-translation-unit static initialization issue was substantiated.
  • Errors, memory, performance, and observability: An isolated failed submit now removes its callback before returning an error, and read/projection errors propagate. In M1 an accepted callback has no worker or error path. No new large untracked allocation was identified. Per-output-block atomic accounting and rare queue scanning are bounded; M2 instead permits full-range read amplification for a tiny LIMIT. Existing warnings, metrics, and scanner counters do not prevent these failures.
  • Compatibility and data paths: No FE/BE wire field, function symbol, storage format, EditLog, write, transaction, visible-version, or MoW delete-bitmap path changes in this diff. The earlier counted-padding data-loss path is repaired for the reported case; operator reached_limit() trims overproduction. Header hygiene checkpoints found no new hub include, gate-table change, PCH edit, explicit-instantiation mismatch, or source-.cpp test include.
  • Tests and focus: No generated result files changed. No build or test was run during this review. The focus file contains only -light and names no additional area; all 11 changed files were reviewed.

Review completion: two full subagent rounds and the additional risk scans converged with NO_NEW_VALUABLE_FINDINGS from every subagent in round 2. I validated and deduplicated every candidate, swept all 11 changed files, refreshed the live base/head and inline comments, and found no unresolved suspicious point. These two P1 findings request changes.

Comment thread be/src/util/threadpool.cpp Resolved
Comment thread be/src/exec/scan/scanner.cpp Resolved
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: While a thread pool has no running worker, a submit could
return OK relying on a thread start that belongs to another submit. With zero
workers, submit A queues a task and reserves a thread start, and submit B does
the same (or, when max_threads is reached, only queues its task). If A fails to
create its thread while B's start is still pending, A returns OK and leaves its
task queued. If B fails as well, B withdraws only its own task, so A's accepted
task stays queued without any worker or pending start. A scan context waiting
for that task sleeps forever, and an RPC closure owned by it is never called.

do_submit() now waits for the outcome of pending thread starts while the pool
has no running worker, so a task is only queued when a worker is running or
when the submit starts a thread itself. A submit whose thread creation fails
withdraws its own task whenever no worker is running, instead of relying on
other pending starts. The wait only happens during the cold start of a pool
without workers and ends as soon as the first worker runs, a creation fails, or
the pool is shut down.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ThreadPoolTest.TestOverlappingFailedThreadStartsLeaveNoTask and the
      existing ThreadPoolTest cases
- Behavior changed: Yes (a submit to a thread pool without running workers
  waits until a pending thread start finishes)
- Does this need documentation: No
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: Scanners with a projection hold small blocks in a padding
block until half a batch is collected, and rows are charged to the shared scan
LIMIT only when they leave the scanner. With a projected `LIMIT 2` and two
scanners over selective predicates, each scanner can hold one matching row
below the padding threshold. Neither row is charged, so the shared counter
stays at 2 and both scanners read the filtered tail of their ranges before
returning rows that already satisfy the LIMIT.

The scan operator now also counts the rows held in padding blocks. A scanner
publishes its padding rows to this counter and stops padding as soon as it
holds rows and the rows held by all scanners cover the remaining LIMIT. The
held rows are emitted and charged as before, so rows that may still be dropped
are never charged, and the extra read is bounded by the get_block() call that
is in progress.

### Release note

None

### Check List (For Author)

- Test: Unit Test and Regression test
    - ScannerProjectionTest.shared_limit_stops_padding_when_buffered_rows_cover_it
      and the existing ScannerProjectionTest / ScannerContextTest cases
    - query_p0/limit, correctness_p0/test_shared_scan_limit_pending_tasks,
      test_query_limit
- Behavior changed: No
- Does this need documentation: No
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: A thread pool worker exits as soon as the running plus
pending threads exceed max_threads, even if it is the only running worker and
tasks are queued. With one busy worker and max_threads = 2, submit A reserves a
thread start and submit B is accepted without one. If max_threads is then
lowered to 1, the worker exits after its task because the pending start still
counts. If that start fails, submit A withdraws only its own task, and the task
accepted for submit B stays queued without any worker.

The only running worker now stays while tasks are queued. A pending start that
succeeds still brings the pool back to max_threads, because the new worker
exits by the same check.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ThreadPoolTest.TestOnlyWorkerKeepsQueuedTasksWhenMaxThreadsShrinks and
      the existing ThreadPoolTest cases
- Behavior changed: No
- Does this need documentation: No
@mrhhsg

mrhhsg commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

/review-light

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete for d29c213. The existing P1 padding read-amplification thread still applies; request changes on that basis. With a projected, filtered LIMIT 5 and batch size 8, scanner A can buffer 1 row and emit 4, leaving one row both buffered and needed. The thread pool can then run scanner B before A is requeued. B has no local padding, so the new guard in Scanner::_get_block_after_projects() never checks that A's buffered row covers the remaining LIMIT. B may repeatedly read an all-filtered tail until range EOS. This is the same issue as the existing thread, so I am not posting a duplicate inline comment. The other three existing P1 sequences are addressed on this head by emitted-row charging, failed-submit withdrawal, pending-start waiting, and last-worker queue draining.

Critical checkpoints: The goal is to default both internal and external scans to the thread pool and preserve LIMIT behavior; both defaults and workload-group selections are wired, but the remaining padding case means the LIMIT performance goal is incomplete. The change is focused on those scheduler and scanner paths. Concurrency was traced from Context admission through worker execution and result delivery: scanner counters are atomic, task queues use the transfer lock, and thread-pool tasks use the pool lock; no separate lock-order or lifecycle defect was substantiated. The new shared counter belongs to the scan operator and no cross-translation-unit static dependency was added. These are startup configuration defaults, with no new dynamic configuration, wire or storage format, FE/BE variable, transaction, or data-write path. Both local and remote scheduler paths, conditional admission, failure wake-up, and final operator LIMIT cut were reviewed. The added unit cases cover defaults, emitted rows, buffered rows, admission, and worker-start failures, but do not cover the zero-local-padding case above; no new end-to-end test was added. Existing status and metrics paths provide failure observability, and no other concrete performance or correctness issue was confirmed.

User focus -light: the full 13-file PR was reviewed. This was static inspection only, as required by the review prompt; no builds or tests were run, and author-reported test results were not treated as independent validation. One round of full-review and risk-focused subagents converged with no new distinct findings, followed by a final changed-file and candidate sweep.

Existing P0/P1 findings confirmed for this head: #68610 (comment)

mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
…IT rows

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: A scanner with a projection stops padding once the rows held
in the padding blocks of all scanners cover the remaining shared scan LIMIT,
but only if it holds rows itself. With a projected, filtered `LIMIT 5` and a
batch size of 8, scanner A can hold 1 row and emit 4, so the row it holds is
the one the LIMIT still needs. If scanner B runs before A is scheduled again
and B holds no row, B never checks the rows held by its peers and reads the
all-filtered tail of its range until eos, although A only has to emit its row
to finish the scan.

The padding loop now also checks the counter after each read that was merged
into the padding block, whether or not the scanner holds rows. A scanner
without rows returns an empty block after one `get_block()` call, which lets
its peers emit their rows. It always reads once before returning, so it still
progresses when a retired peer left its rows in the counter.

### Release note

None

### Check List (For Author)

- Test: Unit Test and Regression test
    - ScannerProjectionTest.shared_limit_stops_padding_when_peer_buffered_rows_cover_it,
      ScannerProjectionTest.shared_limit_scanner_progresses_when_buffered_rows_are_stale
      and the existing ScannerProjectionTest / ScannerContextTest /
      ScannerLateArrivalRfTest cases
    - query_p0/limit, correctness_p0/test_shared_scan_limit_pending_tasks
- Behavior changed: No
- Does this need documentation: No
@mrhhsg

mrhhsg commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

/review-light

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review complete for head 3abe4030fe5f4b1dd6993d05338e194112512dd4 by static inspection. Request changes: the existing P1 buffered-row read-amplification thread still applies; I am reusing its inline comment rather than posting a duplicate. There are no distinct new inline findings.

With projected LIMIT 5 and batch size 8, scanner A can emit four rows while holding one padding row. B is already in flight, and a busy scan pool caps this Context to one outstanding scanner. Consuming A queues it but cannot admit it while B runs. B reads filtered rows, returns an empty non-EOS block because A's buffered row covers the remaining LIMIT, and is then pushed above A on the LIFO pending stack. The next Context runnable selects B again. Under sustained scheduler pressure this repeats through B's filtered tail before A can emit the row already available. The latest reply on that thread assumes A runs after B's first empty block; the new scanner-level test manually makes that call and does not test Context scheduling order. This is the same performance defect reported in the existing P1, confirmed on the current head.

Critical checkpoints

  • Goal and proof: Both BE defaults now select ThreadPoolSimplifiedScanScheduler; the ThreadPool admission guard mirrors the TaskExecutor exhausted-LIMIT rule. Added unit tests cover default selection, admission, emitted-row accounting, buffered-row checks, and failed worker starts. They do not prove the scheduler-pressure sequence above.
  • Scope and clarity: The changed production code is confined to scheduler defaults, scan LIMIT accounting/admission, and ThreadPool failed-start handling. The added counters and token bookkeeping have local explanations; no unrelated source changes were found.
  • Concurrency and locks: Scanner tasks run on pool workers, while the pipeline task consumes completed results. _transfer_lock protects pending/completed/in-flight Context state, _lock protects ThreadPool queues and worker counts, and the shared LIMIT/buffered counters are atomic. Worker creation is outside the pool lock. No new lock-order deadlock was substantiated; the remaining concurrency defect is LIFO starvation under the one-scanner cap.
  • Lifecycle and ownership: Emitted rows, padding rows, scanner EOS, Context completion, and failed task withdrawal were traced to their release/wake paths. The prior missing-row P1 and both failed-worker-start P1s appear fixed at this head. No separate lifetime or memory-accounting issue was substantiated.
  • Configuration: Both existing static flags default to false and are read when workload groups construct local and remote scan schedulers. An explicit config value still chooses TaskExecutor. Already-constructed schedulers keep their selected type until reconstructed; no dynamic-change contract was added.
  • Compatibility and parallel paths: No storage format, FE-BE protocol, persistence, or externally versioned interface changes were found. Internal/external scheduler branches and projected/non-projected scan paths were inspected; TopN has no ordinary shared LIMIT.
  • Conditions and errors: The new exhausted-LIMIT admission guard leaves a completed/in-flight wake source, or admits one task when neither exists. Scanner projection and ThreadPool errors propagate through existing status paths. The buffered-row condition itself returns B after one read, but does not ensure A is scheduled next.
  • Tests and results: New assertions were inspected, including negative failed-start cases. No build, test, or generated result was run or modified in this review, as required by the review prompt; author-reported local passes are not independent verification. A Context-level regression with A pending, B returning empty, and effective concurrency one is still needed.
  • Observability and performance: The failed-start path logs a warning and uses existing pool metrics. The unresolved P1 can scan an entire filtered tail and delay a satisfied LIMIT; no other substantiated performance regression was found.
  • Transactions, writes, and variable transport: This PR changes neither transaction/persistence behavior nor data writes nor FE-to-BE variable transport; those checkpoints are not applicable.

User focus: -light named no additional technical area. I still reviewed all 13 changed paths, related call chains, existing threads, and the final candidate list. The one-round normal and risk-focused reviews converged with no new valuable findings; the sole still-applicable blocker is the existing P1 above.

Existing P0/P1 findings confirmed for this head: #68610 (comment)

mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
…ts for

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: Rows that a scanner holds in its padding block are not
charged to the shared scan LIMIT. When the rows held by all scanners cover
the remaining LIMIT, a scanner that holds no row returns an empty block
after one read, so that the scanner holding the rows can emit them.

The scan context did not take this into account when it chose the next
scanner. With a projected `LIMIT 5` and batch size 8, scanner A emits four
rows and keeps one in its padding block while scanner B is running. If the
context is limited to one running scanner (the scan pool has no slack), A
waits in the pending stack. B returns an empty block, the operator pushes B
back on top of A, and the context runs B again because the pending stack is
LIFO. This repeats until B has read its whole filtered range, and only then
A emits the row that finishes the scan. The TaskExecutor path has the same
order, because it resubmits the consumed scanner first.

The context now reads the buffered-row counter of the scan operator. While
the buffered rows cover the remaining LIMIT, both admission paths run the
most recently used pending scanner that holds padding rows. In all other
cases the order stays LIFO, including the case where no pending scanner
holds rows (the holder is running, its block is not consumed yet, or a
retired scanner left its rows counted). The pending scanners are stored in
a vector used as a stack so that the holder can be taken from the middle.
The context keeps the pending scanners that hold rows in a second vector,
so nothing is searched when no pending scanner holds rows: rows that a
retired scanner left counted do not make every admission scan the pending
scanners.

### Release note

None

### Check List (For Author)

- Test: Unit Test / Regression test
    - Unit Test: ScannerProjectionTest.shared_limit_context_runs_scanner_holding_buffered_rows
      runs two scanners through ScannerContext and the thread pool scan
      scheduler with one admitted scanner, and asserts that B reads one block
      of its filtered range. ScannerContextTest.admission_prefers_scanner_holding_buffered_rows
      covers both admission paths and the LIFO fallbacks. Both fail without
      the fix. ScannerProjectionTest.*, ScannerContextTest.*,
      ScannerLateArrivalRfTest.*, WorkloadGroupManagerTest.* and
      ThreadPoolTest.* pass (154 tests with the file and olap scanner tests).
    - Regression test: query_p0/limit and
      correctness_p0/test_shared_scan_limit_pending_tasks pass on a local
      cluster with the thread pool scheduler and with the task executor
      scheduler.
- Behavior changed: No
- Does this need documentation: No
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
…ed scan LIMIT

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: A scanner that holds no padding row returns after one
read while the rows held by its peers cover the remaining shared scan
LIMIT. The check was only made between two `Scanner::get_block()` calls.
Inside `get_block()`, the loop reads on while the block is empty, and it is
bounded by the rows that the reader returns. A reader that filters rows
itself (for example the ORC reader) returns empty blocks that are not
counted, so the scanner could still read its whole filtered range in one
call and keep its worker, while the scanner holding the last rows of the
LIMIT waited for it.

The loop in `get_block()` now also ends when the buffered rows cover the
remaining LIMIT. It is a do/while loop, so every call still reads once and
the scanner progresses when a retired scanner left its rows counted.

The test scanner no longer counts a full read of rows for an empty block,
which hid this loop from the existing tests.

### Release note

None

### Check List (For Author)

- Test: Unit Test / Regression test
    - Unit Test: ScannerProjectionTest.shared_limit_stops_padding_when_peer_buffered_rows_cover_it,
      shared_limit_scanner_progresses_when_buffered_rows_are_stale and
      shared_limit_context_runs_scanner_holding_buffered_rows read empty
      blocks that are not counted as rows read. They fail without the fix.
    - Regression test: query_p0/limit and
      correctness_p0/test_shared_scan_limit_pending_tasks pass on a local
      cluster with the thread pool scheduler and with the task executor
      scheduler.
- Behavior changed: No
- Does this need documentation: No
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: TestOverlappingFailedThreadStartsLeaveNoTask and
TestOnlyWorkerKeepsQueuedTasksWhenMaxThreadsShrinks built the interleaving
they test with a sleep in the injected thread creation failure and a
shorter sleep in the test. On a slow machine the first thread start could
fail before the second submit arrived, and the tests passed without
reaching the code they cover.

The injected failure now runs a callback of the test, which keeps the
thread start pending until the test releases it. A second debug point in
`ThreadPool::do_submit()` reports that a submit is about to wait for a
pending thread start. The tests release the failing start only after the
second submit waits, or after the only worker has run the queued tasks.
All waits in the tests are bounded.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - ThreadPoolTest.* pass. Both tests fail when the wait in `do_submit()`
      and the check that keeps the only worker are disabled.
- Behavior changed: No
- Does this need documentation: No
@mrhhsg

mrhhsg commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

/review-light

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review opinion

Complete static review of PR #68610 at 4dcf5397fe9831d1828081916466e99cdac6d74e: one new P2 performance finding is inline. No previously reported P0/P1 remains applicable on this head. The two normal full-review passes and separate risk scans converged in round 2; all 14 changed files and every candidate received a final disposition.

Critical checkpoints

  • Goal and proof: Both existing config defaults now select the ThreadPool scheduler for newly created internal and external workload-group scanners. The code also applies the shared-LIMIT admission guard, charges emitted rows after projection, and withdraws a failed ThreadPool submit. New unit tests exercise these paths; their runtime results were not independently verified here.
  • Scope and clarity: The changes stay within the affected BE scan, scheduler, pool, and test paths. The redundant zero-delta atomic operation in the inline finding is the remaining simplification.
  • Concurrency and locks: Scanner tasks publish results under _transfer_lock; pending-task collections are changed together under that lock. The LIMIT and buffered-row counters are atomic. ThreadPool start, withdrawal, token queues, and shutdown are serialized by _lock; a start outcome wakes waiting submitters. I found no remaining lock inversion or stranded accepted task in the traced paths.
  • Lifecycle and initialization: Context, scanner, callback, worker-start, and shutdown lifetimes were traced. Failed submits remove their runnable, while accepted work retains a worker. No new cross-translation-unit static initialization dependency was found.
  • Configuration: No new setting was introduced. Both existing defaults become false; explicit true still chooses TaskExecutor when a workload-group scheduler is created.
  • Compatibility: No wire, storage, function-symbol, or FE/BE variable format changes appear in the diff. No release ELF was built or audited for the glibc baseline under the review-only instructions.
  • Parallel paths and conditions: Local and remote workload-group selection, ThreadPool and TaskExecutor admission, projected and ordinary scan reads, buffered-row retirement, exhausted-LIMIT escape, and worker-start failure paths were checked. The special branches have concrete progress or cleanup paths.
  • Test coverage and results: The new BE tests cover scheduler defaults, LIMIT accounting, padding and holder scheduling, failed and overlapping worker starts, and max-thread shrink. No regression output file changed. This invocation ran no build, test, or style gate, as the review prompt forbids builds; author-reported runs are not independent validation.
  • Observability: Existing scan counters and the new worker-creation warning support diagnosis; no separate observability defect was substantiated.
  • Transactions, persistence, and writes: The PR changes read scheduling only; it adds no transaction, EditLog, persistent metadata, or data-write path.
  • Performance and other issues: The inline P2 identifies the redundant shared atomic RMW on ordinary LIMIT reads. The projected empty-read variant uses the same helper and fix, so it is one finding. No other distinct correctness, memory-safety, or performance issue survived validation.

Existing threads and focus

The four existing P1 threads were checked against this head: emitted-only LIMIT charging and buffered-row scheduling address the scan concerns; pending-start waiting and failed-task withdrawal address the ThreadPool concerns. None is repeated or included as an existing blocking ID. The focus file contains only -light; the full PR was reviewed and no additional focus-specific issue was found.

Comment thread be/src/exec/scan/scanner.cpp Resolved
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
…anged

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: Scanner::get_block_after_projects() publishes the rows held
in the padding block to the counter shared by all scanners of a scan operator
after every block. A scanner without projection never holds padding rows, and
a scanner with projection often holds the same number of rows as before, so
the published delta is zero. The code still executed
fetch_add(0, memory_order_acq_rel), which is a read-modify-write on a cache
line shared by all parallel scanners of a LIMIT scan, on every block and with
nothing to publish.

Return from Scanner::_publish_padding_rows() before the atomic operation when
the number of padding rows equals the number already published. The counter
is still updated whenever the number of padding rows changes, so the values
observed by peer scanners and by the scanner context are unchanged.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - run-be-ut.sh with ScannerProjectionTest.* and ScannerContextTest.* (53 tests passed), including the new case for a scanner without projection
    - Regression test: query_p0/limit (19 suites) and correctness_p0/test_shared_scan_limit_pending_tasks passed on a local cluster
- Behavior changed: No
- Does this need documentation: No
mrhhsg added a commit to mrhhsg/doris that referenced this pull request Sep 29, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: The debug point ThreadPool.do_submit.wait_for_thread_start
called DBUG_RUN_CALLBACK() unconditionally. DebugPoint::execute_callback()
throws std::invalid_argument when no callback is set, so enabling the point by
name without a callback (for example through the HTTP debug point action) made
every submit to a pool that is waiting for its first worker throw out of
ThreadPool::do_submit(). Check dp->callback.has_value() first, as the
ThreadPool.create_thread.inject_failure point next to it already does.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - run-be-ut.sh with ThreadPoolTest.* and ScannerProjectionTest.* (31 tests passed)
- Behavior changed: No
- Does this need documentation: No
@mrhhsg

mrhhsg commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

/review-light

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Complete static review of PR #68610 at head 2d3f59f. The main review, two complete independent passes, and the focused counter and pool/default passes converged in one round. All 14 changed files and their related call paths were reviewed. No new substantiated finding remains. The five existing inline concerns were checked against this head; their fixes are present, and none of the earlier P1 findings still applies.

Critical checkpoints:

  • Goal and proof: Both scheduler defaults are false, so new workload groups select the thread pool for internal and external scans. The scan path limits admission after shared LIMIT exhaustion, charges only emitted rows, and schedules a scanner holding needed padding rows. Failed worker starts withdraw rejected tasks. Added unit tests exercise these paths; the PR reports existing regression suites. This review did not run builds or tests, as instructed.
  • Scope and reuse: The changes are confined to scheduler defaults, shared scan accounting/admission, thread pool submission guarantees, and their tests. Both TaskExecutor and ThreadPool paths use the same pending-scanner selection helper.
  • Concurrency: Scanner workers publish progress with acquire/release atomics; the operator and Context workers serialize pending/completed task state under the transfer lock. Thread pool task pickup, withdrawal, and queue counters use the pool lock; pending-start waiters are notified on start, failure, and shutdown. No distinct lock-order or stranded accepted-task case was substantiated.
  • Lifecycle and errors: Accepted Context runnables retain their Context until execution; rejected submissions mark it failed and wake its dependency. Completed scan blocks drain before Context EOS. No new ownership cycle, cross-translation-unit initialization dependency, or silently ignored new error path was found.
  • Configuration and compatibility: The two existing BE configs change only their defaults; explicit configuration remains available. No new wire format, storage format, FE-to-BE variable, or rolling-upgrade protocol is introduced.
  • Parallel paths and conditions: Internal and external scheduler selection, projected and unprojected scans, both admission paths, empty blocks, cancellation, and exhausted-LIMIT wakeups were traced. The one-pending-task escape allows EOS publication when no other task can wake the operator; its condition is documented.
  • Tests and observability: New unit tests cover default selection, buffered-row scheduling, failed thread starts, withdrawal, and the last-worker case. No test output files changed. Existing counters and the failed-start warning cover the new failure path; no further metric need was identified. Author-reported test results were not treated as independent validation.
  • Data correctness and performance: The operator retains the final LIMIT cut; no transaction, persistence, or data-write path changes. Zero-delta padding publication is skipped and unused scanners are not opened after LIMIT exhaustion. No additional supported correctness or performance issue was found.
  • User focus: The focus file contains only “-light”; it names no additional code area. The full PR review is complete.

@mrhhsg

mrhhsg commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28096 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 2d3f59ff083204de0ed16b252c6dd8d68e2395fa, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17753	4070	4064	4064
q2	2201	379	325	325
q3	10034	1458	798	798
q4	4700	479	349	349
q5	7511	850	569	569
q6	186	180	139	139
q7	789	790	595	595
q8	9342	1603	1558	1558
q9	5498	4224	4197	4197
q10	6819	1344	1011	1011
q11	441	258	232	232
q12	631	426	293	293
q13	18067	2677	1990	1990
q14	269	265	235	235
q15	q16	738	719	655	655
q17	1778	1071	970	970
q18	6413	5558	5475	5475
q19	1378	1288	1085	1085
q20	468	397	263	263
q21	5798	3436	2986	2986
q22	448	368	307	307
Total cold run time: 101262 ms
Total hot run time: 28096 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4970	4791	4502	4502
q2	799	562	534	534
q3	4818	5176	4624	4624
q4	2322	2388	1591	1591
q5	4494	4515	4495	4495
q6	238	184	128	128
q7	1804	1694	1482	1482
q8	2359	2056	2026	2026
q9	7290	7308	7150	7150
q10	3685	3605	3062	3062
q11	509	365	337	337
q12	678	690	508	508
q13	2284	2584	1991	1991
q14	267	275	264	264
q15	q16	655	677	599	599
q17	7359	6775	6606	6606
q18	11719	10878	11490	10878
q19	1086	1021	992	992
q20	2190	2157	1885	1885
q21	4948	4094	4284	4094
q22	521	450	410	410
Total cold run time: 64995 ms
Total hot run time: 58158 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150321 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 2d3f59ff083204de0ed16b252c6dd8d68e2395fa, data reload: false

query5	4308	615	445	445
query6	419	210	190	190
query7	4804	549	296	296
query8	320	169	158	158
query9	8777	3115	3081	3081
query10	456	318	263	263
query11	5807	3505	3204	3204
query12	142	91	96	91
query13	1242	590	394	394
query14	6508	4352	4081	4081
query14_1	3819	3843	3838	3838
query15	204	205	176	176
query16	998	447	428	428
query17	910	665	524	524
query18	2446	460	342	342
query19	203	185	145	145
query20	81	84	81	81
query21	227	135	117	117
query22	13082	13021	13254	13021
query23	13712	12929	12159	12159
query23_1	12408	12371	12280	12280
query24	7585	1147	651	651
query24_1	744	676	716	676
query25	531	399	348	348
query26	1266	298	164	164
query27	2750	563	330	330
query28	4566	1925	1905	1905
query29	1545	702	509	509
query30	295	217	181	181
query31	882	729	626	626
query32	148	93	91	91
query33	520	304	240	240
query34	1184	1064	642	642
query35	719	740	627	627
query36	791	780	734	734
query37	140	98	91	91
query38	1808	1738	1740	1738
query39	689	689	664	664
query39_1	651	650	660	650
query40	230	121	95	95
query41	67	65	63	63
query42	102	97	95	95
query43	326	343	295	295
query44	1363	691	688	688
query45	181	170	165	165
query46	1038	1184	711	711
query47	1475	1476	1391	1391
query48	401	385	294	294
query49	584	410	280	280
query50	969	336	256	256
query51	10234	10723	10095	10095
query52	90	88	76	76
query53	240	252	176	176
query54	255	210	175	175
query55	78	73	68	68
query56	235	216	197	197
query57	1433	1430	1424	1424
query58	283	265	253	253
query59	1964	2025	1824	1824
query60	286	234	215	215
query61	142	149	144	144
query62	386	319	257	257
query63	214	177	179	177
query64	2807	998	830	830
query65	3470	3420	3432	3420
query66	1769	420	294	294
query67	20291	20180	20001	20001
query68	3328	1413	922	922
query69	396	301	253	253
query70	908	846	832	832
query71	301	238	215	215
query72	2639	2515	2193	2193
query73	812	794	409	409
query74	4708	4512	4323	4323
query75	2291	2289	1912	1912
query76	2392	1144	740	740
query77	365	412	284	284
query78	8992	8923	8421	8421
query79	1195	1200	736	736
query80	499	449	364	364
query81	536	332	284	284
query82	268	158	134	134
query83	209	214	191	191
query84	297	146	113	113
query85	760	466	372	372
query86	289	236	234	234
query87	1987	1958	1814	1814
query88	3587	2700	2662	2662
query89	321	284	245	245
query90	2127	171	163	163
query91	164	157	128	128
query92	103	100	89	89
query93	1398	1437	831	831
query94	524	343	289	289
query95	651	452	330	330
query96	1053	727	348	348
query97	2423	2412	2290	2290
query98	163	154	145	145
query99	713	726	609	609
Total cold run time: 234779 ms
Total hot run time: 150321 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 23.69 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 2d3f59ff083204de0ed16b252c6dd8d68e2395fa, data reload: false

query1	0.01	0.01	0.01
query2	0.10	0.05	0.04
query3	0.26	0.10	0.10
query4	1.60	0.10	0.10
query5	0.24	0.23	0.21
query6	1.16	0.94	0.96
query7	0.05	0.01	0.01
query8	0.06	0.04	0.04
query9	0.39	0.33	0.34
query10	0.55	0.52	0.58
query11	0.21	0.15	0.15
query12	0.18	0.15	0.14
query13	0.47	0.45	0.46
query14	0.97	0.95	0.95
query15	0.62	0.58	0.59
query16	0.29	0.32	0.31
query17	1.07	1.06	1.09
query18	0.21	0.20	0.19
query19	2.08	1.85	1.93
query20	0.02	0.02	0.01
query21	15.48	0.21	0.14
query22	4.85	0.05	0.05
query23	16.13	0.31	0.12
query24	2.99	0.41	0.28
query25	0.10	0.05	0.05
query26	0.73	0.21	0.15
query27	0.04	0.05	0.04
query28	3.53	0.83	0.35
query29	12.52	4.03	3.21
query30	0.28	0.13	0.11
query31	2.77	0.56	0.31
query32	3.23	0.60	0.50
query33	3.26	3.13	3.24
query34	15.66	3.94	3.28
query35	3.20	3.20	3.25
query36	0.54	0.43	0.43
query37	0.09	0.06	0.06
query38	0.05	0.04	0.04
query39	0.04	0.03	0.03
query40	0.17	0.15	0.15
query41	0.09	0.03	0.02
query42	0.04	0.02	0.03
query43	0.04	0.03	0.04
Total cold run time: 96.37 s
Total hot run time: 23.69 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 100.00% (4/4) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 64.81% (30542/47125)
Line Coverage 49.55% (320091/646010)
Region Coverage 44.99% (258008/573471)
Branch Coverage 46.69% (120939/259023)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (4/4) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 76.33% (34815/45610)
Line Coverage 61.48% (392639/638604)
Region Coverage 57.81% (330161/571119)
Branch Coverage 58.79% (151814/258211)

### What problem does this PR solve?

Issue Number: None

Related PR: apache#51690, apache#58144, apache#58273, apache#65814, apache#67070

Problem Summary: Workload groups pick the scan scheduler for internal and
external table scans from `enable_task_executor_in_internal_table` and
`enable_task_executor_in_external_table`. Both configs currently default to
`true` on master, so every workload group creates
`TaskExecutorSimplifiedScanScheduler` (the time-sharing task executor) for
local (`ls_*`) and remote (`rs_*`) scans. The time-sharing executor is still
maturing (for example the self-deadlock fixed in apache#58273; the internal-table
default was already reverted once in apache#58144), so the default should be the
`ThreadPoolSimplifiedScanScheduler` for both internal and external tables.

This change sets both configs' default values to `false`. The task executor
can still be enabled explicitly through `be.conf`.

With the thread pool path as the default, a `SELECT ... LIMIT n` over many
tablets keeps admitting its remaining pending scanners after peer scanners
have exhausted the shared scan LIMIT (`ScannerContext::can_admit_scan_task`,
rewritten in apache#67070, only applies the concurrency limit). Admission is kept
that way on purpose: holding pending scanners back based on the in-flight or
completed task counters, as apache#65814 does on the TaskExecutor path, ties the
schedule to bookkeeping that is easy to get wrong and can leave a context
with nothing to wake the operator. Instead, `Scanner::get_block_after_projects()`
now checks the shared LIMIT before it reads: once the counter is exhausted,
the scanner returns an empty block with EOS, so an admitted scanner never
reads its range for rows the operator will cut anyway.

### Release note

The BE configs `enable_task_executor_in_internal_table` and
`enable_task_executor_in_external_table` now default to `false`; internal
and external table scans use the thread pool scan scheduler by default.

### Check List (For Author)

- Test:
    - Unit Test: added `WorkloadGroupManagerTest.DefaultScanSchedulersUseThreadPool`
      (registered defaults are `false` and the workload group creates
      thread pool schedulers for local and remote scans),
      `ScannerContextTest.thread_pool_admission_ignores_shared_limit`
      (a pending scanner is still admitted after the shared LIMIT is
      exhausted while tasks are in flight or completed) and
      `ScannerProjectionTest.shared_limit_exhausted_before_call_reports_eos_without_reading`
      (the admitted scanner reports EOS with an empty block without reading);
      ran `WorkloadGroupManagerTest.*`, `ScannerContextTest.*`,
      `ScannerProjectionTest.*` and other scanner/scheduler/task-executor UTs
    - Regression test: ran existing suites on a local cluster with the new
      default: correctness_p0/test_shared_scan_limit_pending_tasks,
      query_p0 limit pushdown suites, query_p0/scan_range, and stream load
      CSV suites
- Behavior changed: Yes (default scan scheduler for internal and external
  table scans changes from the task executor to the thread pool; it can be
  switched back via the two BE configs)
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610, apache#62222

Problem Summary: `Scanner::get_block()` charges the shared scan LIMIT
counter as soon as rows pass the filters, but with projections
`Scanner::get_block_after_projects()` may keep a small block in
`_padding_block` and emit a later block first. Once peer scanners exhaust
the counter, `ScannerContext` stops admitting pending scanners and reaches
EOS when no completed or in-flight task remains, so rows that were charged
but still held in a pending scanner's padding block were never emitted. A
parallel projected `SELECT ... LIMIT n` could return fewer than `n` rows:
with LIMIT 4096, scanner A holds 10 rows and emits 2500 (2510 charged),
scanner B emits 1586 and exhausts the counter, and the query returns 4086
rows.

The counter now equals "LIMIT minus returned rows" between calls. Rows
still held in `_padding_block` when `get_block_after_projects()` returns
are refunded to the counter, and they are charged again when the next
call starts, so during a call every row the scanner holds is charged and
the read loop still stops as soon as the LIMIT is reached. An exhausted
counter therefore always means that at least LIMIT rows were returned,
which is what the EOS checks in `Scanner` and `ScannerContext`
rely on, while projected scans with selective filters keep coalescing
small blocks up to half a batch per call. The scan does not need to cap
its output at the LIMIT, since the operator cuts the extra rows.

### Release note

None

### Check List (For Author)

- Test: Unit Test / Regression test
    - Unit Test: added `ScannerProjectionTest.shared_limit_refunds_rows_held_in_padding`,
      `ScannerProjectionTest.shared_limit_keeps_padding_small_blocks` and
      `ScannerProjectionTest.shared_limit_reports_eos_when_scanner_rows_exhaust_it`;
      ran `ScannerProjectionTest.*`, `ScannerContextTest.*`,
      `ScannerLateArrivalRfTest.*`, `WorkloadGroupManagerTest.*`
    - Regression test: query_p0/limit and
      correctness_p0/test_shared_scan_limit_pending_tasks on a local cluster
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: `PInternalService::multiget_data_v2` submits its callback
to the remote scan scheduler. With the thread pool scheduler, which this
PR makes the default, the callback runs directly on a `ThreadPool` worker,
and `ThreadPool::dispatch_thread()` has no exception boundary. For a
request with an existing file mapping and row IDs,
`RowIdStorageReader::read_by_rowids()` constructs `SlotDescriptor(pslot)`
before its local `try` block, and an unknown `PTypeNode.type` (for example
one sent by a newer FE) throws `doris::Exception`. The exception escaped
the worker thread and terminated the BE. The task executor path caught it
in `PrioritizedSplitRunner::process()`, but the RPC still completed with a
success status and no row data.

The callback now calls `RowIdStorageReader::read_by_rowids_for_rpc()`,
which runs `read_by_rowids()` under `ASSIGN_STATUS_IF_CATCH_EXCEPTION`
and serializes the resulting status into the response before the closure
completes the RPC, mirroring the exception conversion the external-row
path already has in `submit_external_scan_tasks()`.
`ExecEnv::set_id_manager()` is added under `BE_TEST` so a unit test can install the
`IdManager` the reader looks up.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - Unit Test: added `RowIdStorageReaderTest.RpcHelperReportsThrownExceptionOnThreadPoolPath`,
      which runs the helper on a real `ThreadPoolSimplifiedScanScheduler`
      with a slot whose type node kind is unknown and asserts the error
      status on the response; ran `RowIdStorageReaderTest.*` and
      `SubmitExternalScanTasksTest.*`
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: close #xxx

Related PR: #xxx

Problem Summary:
`ScanLocalState::shared_scan_limit_ptr()` returned null whenever the live
counter was negative, and `Scanner::init()` caches that pointer once. The
counter is only negative for a moment: a scanner charges the rows it holds
in its padding block when `get_block_after_projects()` starts, reads on
while peers charge their rows, and refunds the held rows when the call
returns. A scanner initialized during that moment (a local state of a
parallel instance opening late) cached null, never checked or charged the
shared LIMIT, and could read a long selective tail after its peers had
already satisfied the query.

Decide pointer presence from the immutable SQL LIMIT of the operator
instead of the mutable counter, which is how `ScannerContext` already
decides it. `MockScanOperatorX::set_limit_for_test()` sets both the LIMIT
and the counter the way the operator constructor does.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - `ScannerProjectionTest.shared_limit_observed_by_scanner_initialized_under_negative_counter`
      initializes a scanner while the counter is negative and checks that it
      still charges the shared LIMIT and stops once it is exhausted.
    - `ScannerProjectionTest.no_shared_limit_without_sql_limit` keeps the
      no-LIMIT case on the null pointer.
- Behavior changed: No
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: Projected scanners could charge rows retained in their padding block against the shared scan LIMIT. If another scanner then exhausted the counter, the held rows might never reach the operator and a LIMIT query could return too few rows. Deduct only the rows actually produced by get_block_after_projects, and flush local padding when it can satisfy the remaining LIMIT without reading another source block.

### Release note

Fix projected scan LIMIT queries that could return fewer rows than requested.

### Check List (For Author)

- Test: Unit Test: 66 focused BE tests passed; formatting and build hygiene checks passed. Clang-tidy was attempted but remains blocked by pre-existing diagnostics and a missing stddef.h in the toolchain.
- Behavior changed: Yes (shared scan LIMIT counts produced rows rather than held padding)
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: The projected-scanner padding loop separately compared held rows with the shared LIMIT on each iteration to stop reading once enough rows were collected. Derive the batch target once from the shared remaining LIMIT before the loop instead. The shared counter is still decremented only for the block actually produced; concurrent peer updates may make this snapshot stale, but the scan operator retains the final LIMIT cut.

### Release note

None

### Check List (For Author)

- Test: Unit Test: 66 focused BE tests passed. Formatting, build hygiene, and diff checks passed. Clang-tidy was attempted; it still reports a pre-existing scanner constructor warning and the toolchain missing stddef.h.
- Behavior changed: Yes (the padding batch target is capped by a one-time shared remaining-LIMIT snapshot)
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: The padding loop was extracted into a member function used only once, increasing the API and review surface without sharing logic. Put the loop back in get_block_after_projects while retaining output-only shared LIMIT accounting and the entry-time batch target derived from the remaining LIMIT.

### Release note

None

### Check List (For Author)

- Test: Unit Test: 66 focused BE tests passed. Formatting and build hygiene checks passed. Clang-tidy was attempted; remaining diagnostics are pre-existing warnings and a toolchain missing stddef.h.
- Behavior changed: No (single-use helper inlined without changing scan results)
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: Scanner::get_block_after_projects() and Scanner::get_block() turned an exhausted shared scan LIMIT into EOS: a scanner scheduled after its peers had produced the whole LIMIT, or whose own rows exhausted it, reported EOS although its reader still had data. EOS means the underlying read is exhausted, while the LIMIT belongs to the scan operator, which cuts the extra rows and stops the scanners once it holds enough. Deriving EOS from the LIMIT bookkeeping means any mistake there ends a scan early and returns too few rows, instead of, at worst, letting a scanner run until the operator stops it.

The scanner now stops reading when the shared LIMIT is exhausted but never reports EOS for it: get_block_after_projects() returns an empty block on entry, the padding loop stops reading and returns the rows it already holds, and the tail only charges the produced rows. The two pre-existing counter checks in get_block() are removed so that the shared LIMIT is handled in one place. The query still finishes as before: ScannerContext::get_block_from_queue() completes the context once the counter is exhausted with nothing in flight, and ScanOperatorX::get_block_impl() stops the scanners after reached_limit().

### Release note

None

### Check List (For Author)

- Test: Unit Test: ScannerProjectionTest cases updated to expect no EOS from the shared LIMIT, plus two new cases (own rows exhaust the LIMIT, a peer exhausts it mid-read); ran the scan/workload-group/row-ID UT filter. Regression test: query_p0/limit and correctness_p0/test_shared_scan_limit_pending_tasks on a local cluster with the thread pool scheduler default.
- Behavior changed: Yes (scanners no longer report EOS for an exhausted shared scan LIMIT; the scan operator stops them)
- Does this need documentation: No
@mrhhsg
mrhhsg force-pushed the improve/default-thread-pool-scan-scheduler branch from 8442849 to e275f85 Compare October 15, 2026 08:14
@mrhhsg

mrhhsg commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

/review-light

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes: two new P2 findings are inline, and three previously reported P1 findings still apply at e275f85. Review converged after 2 rounds: all three reviewers returned NO_NEW_VALUABLE_FINDINGS on the unchanged final set; the final sweep covered all 14 supplied changed files and disposed of every candidate/risk.

The new findings concern repeated empty non-EOS scanner resubmission after shared LIMIT exhaustion, and RPC-test timeout teardown destroying captured objects before joining an active worker.

Existing blockers were independently confirmed and are carried without duplicate comments:

  • 4129581696: minimum-worker creation can fail without failing pool initialization; a failed first-worker submit retains the RPC callback even after its error path completes done. A later submission can execute released request/response/done captures. This generic ThreadPool fix was intentionally left outside the diff, but the mechanism remains reachable with the new default.
  • 4130378110: overlapping zero-worker start failures can leave an accepted task queued with no worker, stranding its completion. Positive min_threads does not exclude this state because initialization ignores thread-creation failure.
  • 4130378115: for projected LIMIT 2, two scanners holding one matching row each still see a remaining counter of 2 and can read long filtered tails. The per-call target clamp does not combine peer-held rows; the author's latest reply explicitly leaves this amplification unfixed.

Critical checkpoint conclusions:

  1. Goal/proof: both defaults select ThreadPool; emitted-only charging fixes the prior padding row-loss issue, and the RPC helper converts Doris exceptions before completion. Unit coverage was inspected; M1/M2 and the carried blockers limit the result.
  2. Scope/reuse: focused changes reuse the existing exception macro, shared counter, and scheduler interfaces.
  3. Concurrency/locks: workers and pipeline consumers share transfer_lock-protected pending/completed/in-flight state and an atomic remaining-row counter. Scans run outside that lock; no new lock inversion found. The new resubmission finding concerns progress.
  4. Lifecycle/static state: the new test timeout order is unsafe; known pool submission ownership/liveness failures remain. No new production reference cycle or cross-translation-unit initializer introduced.
  5. Configuration: existing immutable startup defaults change to false; explicit true still selects TaskExecutor separately for each table kind. No dynamic update is promised.
  6. Compatibility: no wire/storage format changed; the helper is internal C++. No new libc API requirement apparent from source; no release binary audit performed.
  7. Thrift/protobuf IDs: no gensrc changes.
  8. Parallel paths: both schedulers, projected/nonprojected scans, disabled padding, no SQL LIMIT, key TopN, late filters, and external row-ID children were examined. Nested same-pool saturation also exists under the prior TaskExecutor and is not a new finding.
  9. Conditions/comments: new conditions are explained; SQL-LIMIT presence correctly determines pointer presence even with a negative live counter. Empty non-EOS behavior still needs the scheduling fix.
  10. Tests: new default-selection, scanner admission, emitted/padded accounting and real-ThreadPool exception cases cover intended success paths. Integrated exhausted-counter resubmission, safe timeout teardown, startup-failure recovery and split-padding coverage remain inadequate. No tests were executed.
  11. Expected results: no golden output changed; unit assertions match intended successful behavior on inspection.
  12. Observability: existing scanner profiles/queue counters, pool metrics and RPC query-id timing logs remain available; no additional distinct gap found.
  13. Persistence/transactions: no edit-log, commit/version or failover changes.
  14. Writes/atomicity: no data-write path changes; read-side correctness, errors and lifetime were reviewed as above.
  15. FE-BE variables: none added or changed.
  16. Performance: new empty-task churn and existing parallel-padding read amplification remain; large-LIMIT coalescing is preserved and obsolete buffered-row zero-delta atomic work is gone.
  17. Other concerns/focus: supplied focus is only -light, with no extra functional question. Full changed-file coverage and missed-issue rechecks were retained; no unresolved candidate or uncovered source scope remains.

Existing findings 4129581700 (lost counted padding) and 4140089770 (uncaught row-ID Doris exception) are fixed; 4132871043 is obsolete, and 4226650611/4227165026 are addressed. Human EOS and BE_TEST-setter requests were checked. Static review only: no builds, tests, benchmarks or source modifications; author-reported runs were not treated as executed evidence. No measured spin rate or inevitable permanent hang is claimed.

Review converged after 2 round(s).

Existing P0/P1 findings confirmed for this head: #68610 (comment), #68610 (comment), #68610 (comment)

Comment thread be/src/exec/scan/scanner.cpp Resolved
Comment thread be/test/exec/rowid_fetcher_test.cpp Resolved
### What problem does this PR solve?

Issue Number: None

Related PR: apache#68610

Problem Summary: Since the scanner no longer reports EOS for an exhausted shared scan LIMIT, a scanner that is scheduled after its peers produced the whole LIMIT completes with an empty block and eos == false. ScannerContext::get_block_from_queue() treated such a task like any consumed non-EOS result and resubmitted it. With parallel scanning, another instance can exhaust the operator-wide counter while this context still has a scanner blocked in I/O, so the empty scanner was admitted, completed empty and resubmitted again for as long as that I/O lasted, burning scanner and pipeline CPU for a LIMIT that was already satisfied. Both schedulers resubmit from this one call: the ThreadPool path queues the task and a Context runnable whose admission ignores the LIMIT by design, and the TaskExecutor path returns the current task from _pull_next_scan_task() unconditionally.

get_block_from_queue() now parks a consumed non-EOS task in the pending queue without rescheduling it when the shared LIMIT is exhausted. The branch is only reached when the finish check did not fire, so a completed or in-flight task remains to wake the operator, and the context finishes once the counter is exhausted with nothing in flight, as before. The parked scanner never runs again; the scanner itself still only stops reading and never reports EOS for the LIMIT.

Also fix the teardown order in RowIdStorageReaderTest.RpcHelperReportsThrownExceptionOnThreadPoolPath: the response and the latch captured by the pool worker are declared before the scheduler, so a failed wait joins the worker in the stop guard before either captured object is destroyed.

### Release note

None

### Check List (For Author)

- Test: Unit Test: new ScannerContextTest.exhausted_shared_limit_parks_empty_task_until_in_flight_drains (counter at 0, one task in flight, an empty non-EOS completion is parked, the scheduler is never asked to resubmit it, and the context finishes when the in-flight task drains); ran the scan/workload-group/row-ID UT filter (142 tests). Regression test: query_p0/limit and correctness_p0/test_shared_scan_limit_pending_tasks on a local cluster with the thread pool scheduler default.
- Behavior changed: Yes (an admitted scanner is not resubmitted after the shared scan LIMIT is exhausted while other work is outstanding)
- Does this need documentation: No
@mrhhsg

mrhhsg commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

/review-light

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of PR #68610 at head 62afc6b03b7e55ba938eec1a5e13c2cad1cd8844 against base 4ffe6632b8cf75a0d647719c5f7a5f77308f0e5f. The review covered all 14 changed paths, the complete authoritative diff, required AGENTS.md guidance, upstream/downstream scanner and workload-group paths, and all existing review threads. No new inline finding survived verification or deduplication; the three existing P1 findings below remain applicable and are carried without reposting.

Existing blocking findings:

  • 4129581696: ThreadPool can retain an accepted remote RPC callback after a failed first-worker submission; multiget_data_v2 completes the RPC on the error path, and a later worker can execute the stale callback. ThreadPool initialization still ignores minimum-worker creation failures, and this PR changes the defaults to select that pool.
  • 4130378110: overlapping worker-start failures can leave accepted tasks queued with no running or pending worker. This is distinct from the retained callback case and remains in unchanged generic ThreadPool code.
  • 4130378115: the pending-holder projected LIMIT follow-up can make a scanner with no local padding read a long filtered tail while another scanner holds the last needed row. Results remain complete, but this is a real read-amplification issue intentionally left in the current head.

Critical checkpoint conclusions:

  1. Goal and proof: both existing scheduler flags now default to ThreadPool for newly constructed internal and external workload-group schedulers. Shared LIMIT charging is performed for emitted rows after projection, the pointer is selected from immutable SQL LIMIT state, exhausted scanners are parked while outstanding work drains, and row-ID RPC exceptions are serialized as response status. Added tests cover scheduler selection, LIMIT accounting/padding/EOS and late initialization, and an unknown row-ID type; this invocation is static only and ran no build or test.
  2. Scope and reuse: production changes remain focused on scheduler defaults, scanner LIMIT accounting/admission, and the RPC exception boundary, using existing scanner/context, ThreadPool, Status, and exception mechanisms.
  3. Concurrency and locks: scanner workers and pipeline consumers coordinate pending/completed/in-flight state under ScannerContext::_transfer_lock; the shared LIMIT is atomic; scan/projection work runs outside that lock. No new lock inversion was substantiated. The generic ThreadPool worker-start failures above remain.
  4. Lifecycle and memory: emitted rows are charged only after they are placed in the output block; task completion publishes before waking the dependency. RPC callbacks attach the row-ID tracker and serialize status before ClosureGuard completes. No new ownership cycle, static-initialization dependency, untracked large allocation, or nullable/COW defect was found.
  5. Configuration and compatibility: no new setting, wire field, storage format, persistence, transaction, or FE-BE variable was introduced. Explicit true values still select TaskExecutor; flags are sampled at scheduler construction.
  6. Parallel paths and conditions: local/remote schedulers, TaskExecutor/ThreadPool routes, projected/non-projected and nonmergeable scans, no-LIMIT/key-TopN, adaptive/low-memory admission, late scanner initialization, EOS, cancellation, and RPC submit/error paths were traced.
  7. Tests and observability: new BE unit-test source meaningfully exercises the changed invariants, but no runtime validation or generated result file was run or changed here. Existing context diagnostics, scanner counters/profiles, pool metrics/warnings, and RPC status/timing logs remain available.

User focus -light: the focus file names no additional technical area; the complete changed-file review found no focus-specific issue. Convergence is true after round 1: the normal scanner reviewer, normal RPC/default-scheduler reviewer, and separate risk-focused reviewer all returned no new valuable findings, and every candidate/risk has an evidence-based disposition. The review remains blocking only because the three confirmed existing P1 findings are carried in existing_blocking_comment_ids.

Review converged after 1 round(s).

Existing P0/P1 findings confirmed for this head: #68610 (comment), #68610 (comment), #68610 (comment)

@mrhhsg

mrhhsg commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29021 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 62afc6b03b7e55ba938eec1a5e13c2cad1cd8844, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17632	5143	5342	5143
q2	2118	315	262	262
q3	10242	1446	831	831
q4	4679	469	336	336
q5	7434	810	531	531
q6	183	187	151	151
q7	751	806	594	594
q8	9266	1371	1130	1130
q9	5689	4234	4216	4216
q10	6846	1298	1037	1037
q11	436	252	241	241
q12	646	404	290	290
q13	18064	3658	2998	2998
q14	285	272	248	248
q15	q16	730	721	671	671
q17	1221	776	611	611
q18	6972	6172	6097	6097
q19	1102	981	642	642
q20	383	339	225	225
q21	6024	2480	2536	2480
q22	377	326	287	287
Total cold run time: 101080 ms
Total hot run time: 29021 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6071	6040	6017	6017
q2	756	615	587	587
q3	5750	5781	5336	5336
q4	2067	2230	1560	1560
q5	5744	5554	5534	5534
q6	251	193	143	143
q7	2248	2115	1823	1823
q8	3017	2658	2673	2658
q9	8004	8056	7892	7892
q10	3775	3662	3210	3210
q11	608	420	403	403
q12	737	676	511	511
q13	3316	3584	3030	3030
q14	289	287	272	272
q15	q16	657	675	609	609
q17	7783	7031	6976	6976
q18	12954	12161	12843	12161
q19	933	817	795	795
q20	2144	2127	1926	1926
q21	5638	4637	4662	4637
q22	494	457	394	394
Total cold run time: 73236 ms
Total hot run time: 66474 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151095 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 62afc6b03b7e55ba938eec1a5e13c2cad1cd8844, data reload: false

query5	4302	628	446	446
query6	430	205	195	195
query7	4802	463	252	252
query8	326	172	148	148
query9	8781	3110	3110	3110
query10	485	299	245	245
query11	6050	3847	3563	3563
query12	152	93	88	88
query13	1263	462	333	333
query14	6108	3956	3701	3701
query14_1	3488	3445	3478	3445
query15	213	201	176	176
query16	952	446	409	409
query17	843	673	546	546
query18	2427	428	314	314
query19	203	167	139	139
query20	87	83	87	83
query21	218	131	113	113
query22	12953	12921	12825	12825
query23	13539	12918	12503	12503
query23_1	12460	12563	12506	12506
query24	7387	860	523	523
query24_1	469	473	479	473
query25	544	399	335	335
query26	1248	256	138	138
query27	2776	455	269	269
query28	4613	1964	1943	1943
query29	1554	608	439	439
query30	312	222	192	192
query31	1004	922	810	810
query32	145	91	84	84
query33	516	320	229	229
query34	907	857	493	493
query35	742	744	642	642
query36	795	831	719	719
query37	137	117	80	80
query38	1789	1716	1664	1664
query39	805	813	751	751
query39_1	728	725	751	725
query40	218	114	98	98
query41	71	68	64	64
query42	88	83	83	83
query43	350	369	308	308
query44	1367	692	698	692
query45	177	174	158	158
query46	855	984	562	562
query47	2911	2856	2828	2828
query48	305	314	219	219
query49	577	403	294	294
query50	695	287	216	216
query51	10630	10627	10560	10560
query52	90	83	106	83
query53	190	213	152	152
query54	250	196	183	183
query55	69	69	64	64
query56	248	230	223	223
query57	1665	1616	1531	1531
query58	275	264	255	255
query59	2127	2121	1948	1948
query60	282	245	226	226
query61	145	142	135	135
query62	394	333	287	287
query63	200	165	161	161
query64	2745	931	737	737
query65	3329	3281	3271	3271
query66	1788	422	295	295
query67	20031	20041	19849	19849
query68	2958	1039	623	623
query69	393	289	256	256
query70	867	765	771	765
query71	298	233	210	210
query72	2708	2579	2352	2352
query73	531	550	302	302
query74	4636	4499	4334	4334
query75	2328	2321	1916	1916
query76	2310	1047	625	625
query77	385	364	272	272
query78	9607	9627	9181	9181
query79	897	838	530	530
query80	482	414	337	337
query81	524	326	277	277
query82	206	136	107	107
query83	202	209	183	183
query84	291	128	104	104
query85	767	429	361	361
query86	262	236	226	226
query87	1974	1981	1783	1783
query88	3659	2710	2685	2685
query89	321	292	245	245
query90	2093	178	173	173
query91	164	161	134	134
query92	106	103	90	90
query93	1062	1023	587	587
query94	430	320	252	252
query95	577	326	357	326
query96	682	496	239	239
query97	2429	2424	2305	2305
query98	158	153	149	149
query99	758	756	655	655
Total cold run time: 233308 ms
Total hot run time: 151095 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.38 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 62afc6b03b7e55ba938eec1a5e13c2cad1cd8844, data reload: false

query1	0.00	0.00	0.01
query2	0.09	0.05	0.05
query3	0.26	0.09	0.08
query4	1.61	0.11	0.10
query5	0.23	0.24	0.23
query6	1.17	0.67	0.67
query7	0.04	0.00	0.00
query8	0.05	0.04	0.04
query9	0.40	0.36	0.36
query10	0.55	0.55	0.57
query11	0.19	0.15	0.14
query12	0.18	0.15	0.15
query13	0.50	0.49	0.48
query14	1.02	1.00	1.01
query15	0.61	0.64	0.62
query16	0.35	0.35	0.36
query17	1.15	1.13	1.14
query18	0.20	0.19	0.20
query19	2.06	2.01	2.02
query20	0.02	0.01	0.01
query21	15.49	0.21	0.13
query22	4.90	0.06	0.06
query23	16.23	0.31	0.13
query24	3.04	0.55	0.45
query25	0.10	0.06	0.05
query26	1.41	0.22	0.15
query27	0.05	0.05	0.05
query28	4.19	0.79	0.36
query29	12.49	4.09	3.22
query30	0.29	0.13	0.10
query31	2.78	0.54	0.32
query32	3.22	0.60	0.49
query33	3.11	3.18	3.19
query34	15.26	4.07	3.41
query35	3.36	3.35	3.33
query36	0.56	0.46	0.42
query37	0.09	0.06	0.07
query38	0.06	0.04	0.04
query39	0.04	0.03	0.03
query40	0.17	0.16	0.15
query41	0.09	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.04	0.03
Total cold run time: 97.69 s
Total hot run time: 24.38 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (7/7) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.00% (35677/46334)
Line Coverage 62.33% (404625/649162)
Region Coverage 58.75% (341815/581777)
Branch Coverage 59.61% (157587/264361)

@mrhhsg
mrhhsg merged commit e0bf487 into apache:master Oct 19, 2026
38 of 39 checks passed
@mrhhsg
mrhhsg deleted the improve/default-thread-pool-scan-scheduler branch October 19, 2026 07:54
yiguolei pushed a commit that referenced this pull request Oct 24, 2026
#68610 (#69508)

## Summary

- Use the existing thread-pool scheduler by default for internal and
external scans. The task executor remains available through the
corresponding explicit configuration options.
- Convert Doris exceptions raised during row-ID V2 fetch RPC processing
on a thread-pool worker into an error status in the RPC response.

## Backport scope

Cherry-picked from #68610 for branch-4.2. This branch does not have the
master-side shared scan LIMIT counter or the associated scanner/context
APIs, so its existing scanner behavior is retained. The backport
includes the scheduler defaults, the row-ID RPC exception boundary, and
focused tests without importing unrelated master-side scan changes.

## Tests

- ASAN BE and FE build: `BUILD_TYPE=ASAN ./build.sh --be --fe -j 32` —
passed.
- Focused BE unit tests covering row-ID fetch, external scan task
submission, workload-group schedulers, scanner context, and scanner
projection — 82 passed.
- `query_p0/limit` regression on an isolated local FE/BE cluster — 18
suites passed.
- `clang-format.sh`, `check-format.sh`, and `git diff --check` — passed.

## Release note

Internal and external scans now use the thread-pool scheduler by
default. Set `enable_task_executor_in_internal_table` or
`enable_task_executor_in_external_table` to `true` to opt back into the
task executor for the respective scan type.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants