Repository navigation
[improvement](be) Use thread pool scan scheduler by default - #68610
Conversation
|
Thank you for your contribution to Apache Doris. Please clearly describe your PR:
|
|
/review-light |
There was a problem hiding this comment.
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.
### 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
### 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
|
/review-light |
There was a problem hiding this comment.
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
truestill selects TaskExecutor. Local and external scanners shareScannerScheduler::_scanner_scan(), while the remote scheduler also acceptsmultiget_data_v2work. 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
_lockprotects queue, token, worker counts, and take markers, but worker creation occurs outside it; M1 is the resulting all-starts-fail interleaving. Context_transfer_lockprotects 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
-lightand 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.
### 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
### 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
### 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
|
/review-light |
There was a problem hiding this comment.
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)
…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
|
/review-light |
There was a problem hiding this comment.
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_lockprotects pending/completed/in-flight Context state,_lockprotects 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
falseand 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)
…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
…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
### 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
|
/review-light |
There was a problem hiding this comment.
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; explicittruestill 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.
…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
### 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
|
/review-light |
There was a problem hiding this comment.
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.
|
run buildall |
TPC-H: Total hot run time: 28096 ms |
TPC-DS: Total hot run time: 150321 ms |
ClickBench: Total hot run time: 23.69 s |
BE UT Coverage ReportIncrement line coverage Increment coverage report
|
BE Regression && UT Coverage ReportIncrement line coverage Increment coverage report
|
### 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
8442849 to
e275f85
Compare
|
/review-light |
There was a problem hiding this comment.
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:
- 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.
- Scope/reuse: focused changes reuse the existing exception macro, shared counter, and scheduler interfaces.
- 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.
- 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.
- Configuration: existing immutable startup defaults change to false; explicit true still selects TaskExecutor separately for each table kind. No dynamic update is promised.
- Compatibility: no wire/storage format changed; the helper is internal C++. No new libc API requirement apparent from source; no release binary audit performed.
- Thrift/protobuf IDs: no gensrc changes.
- 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.
- 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.
- 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.
- Expected results: no golden output changed; unit assertions match intended successful behavior on inspection.
- Observability: existing scanner profiles/queue counters, pool metrics and RPC query-id timing logs remain available; no additional distinct gap found.
- Persistence/transactions: no edit-log, commit/version or failover changes.
- Writes/atomicity: no data-write path changes; read-side correctness, errors and lifetime were reviewed as above.
- FE-BE variables: none added or changed.
- 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.
- 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)
### 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
|
/review-light |
There was a problem hiding this comment.
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:
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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)
|
run buildall |
TPC-H: Total hot run time: 29021 ms |
TPC-DS: Total hot run time: 151095 ms |
ClickBench: Total hot run time: 24.38 s |
BE Regression && UT Coverage ReportIncrement line coverage Increment coverage report
|
#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.
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_tableandenable_task_executor_in_external_table. Both configs currently default totrueon master, so every workload group createsTaskExecutorSimplifiedScanScheduler(the time-sharing task executor) forlocal (
ls_*) and remote (rs_*) scans. The time-sharing executor is stillmaturing (for example the self-deadlock fixed in #58273; the internal-table
default was already reverted once in #58144), so the default should be the
ThreadPoolSimplifiedScanSchedulerfor both internal and external tables.This change sets both configs' default values to
false. The task executorcan still be enabled explicitly through
be.conf.Making the thread pool path the default exposed four gaps.
Pending scanners after an exhausted shared LIMIT. For a
SELECT ... LIMIT nover 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 checksthe 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 thecounter is exhausted with nothing in flight. The two pre-existing checks in
Scanner::get_block()that seteoffrom the shared counter are removed forthe same reason, so the shared LIMIT is handled in one place.
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_blockand emit a later block first. Once peer scanners exhaust thecounter, the context reaches EOS without running that scanner again, and the
charged rows it holds are lost; for example, a parallel projected
LIMIT 4096could return 4086 rows. The counter is now decremented only bythe 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.
Exceptions in the row-ID fetch RPC callback.
multiget_data_v2submitsits callback to the remote scan scheduler; on a
ThreadPoolworker there isno exception boundary, and
RowIdStorageReader::read_by_rowids()constructsSlotDescriptor(pslot)before its localtry, so an unknownPTypeNode.typewould throwdoris::Exceptionand terminate the BE. Thecallback now calls
RowIdStorageReader::read_by_rowids_for_rpc(), whichconverts a thrown exception into an error status on the response before the
closure completes the RPC.
ExecEnv::set_id_manager()is added underBE_TESTfor the unit test.The shared LIMIT pointer of a scanner initialized while the counter is
negative.
ScanLocalState::shared_scan_limit_ptr()returned null wheneverthe live counter was negative, and
Scanner::init()caches that pointeronce. 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
ScannerContextalready decides it.Release note
The BE configs
enable_task_executor_in_internal_tableandenable_task_executor_in_external_tablenow default tofalse; internaland external table scans use the thread pool scan scheduler by default.
Check List (For Author)
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_limitand
RowIdStorageReaderTest.RpcHelperReportsThrownExceptionOnThreadPoolPath(runs the RPC helper on a real
ThreadPoolSimplifiedScanScheduler;the test process aborts without the fix);
ran
ScannerProjectionTest.*,ScannerContextTest.*,ScannerLateArrivalRfTest.*,WorkloadGroupManagerTest.*,RowIdStorageReaderTest.*,SubmitExternalScanTasksTest.*default: query_p0/limit (19 suites) and
correctness_p0/test_shared_scan_limit_pending_tasks
table scans changes from the task executor to the thread pool; it can be
switched back via the two BE configs)