Skip to content

[fix](be) Finish pending scanners after shared limit#65814

Open
Mryange wants to merge 2 commits into
apache:masterfrom
Mryange:fix-shared-scan-limit
Open

[fix](be) Finish pending scanners after shared limit#65814
Mryange wants to merge 2 commits into
apache:masterfrom
Mryange:fix-shared-scan-limit

Conversation

@Mryange

@Mryange Mryange commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Queries with a shared scan limit could hang when the limit was exhausted while ScannerContext still had pending scan tasks. _pull_next_scan_task() returned nullptr without scheduling those tasks, so the scanner lifecycle could stop making progress and the pipeline waited until query timeout.

This change removes the shared-limit early return from ScannerContext::_pull_next_scan_task(). Pending scanners are submitted normally, and the exhausted shared limit is handled in Scanner::get_block(), which returns EOS before reading more data. ScannerContext::get_block_from_queue() then consumes the EOS tasks and marks the context finished after the in-flight scanners have completed.

Release note

None

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

### What problem does this PR solve?

Issue Number: None

Related PR: apache#62222

Problem Summary: Queries with a shared scan LIMIT could leave pending scan tasks unscheduled after the shared budget reached zero. With no scanner in flight, the scanner context never reached EOS and the pipeline waited until query timeout. Submit pending scanners so they observe the exhausted limit, return EOS, and complete the scanner context lifecycle.

### Release note

Fix query timeout when a LIMIT is exhausted with pending parallel scan tasks.

### Check List (For Author)

- Test: Regression test
    - Regression test: test_shared_scan_limit_pending_tasks
    - Manual test: case 500078775 passed 10/10 with local shuffle disabled
- Behavior changed: Yes, affected LIMIT queries now finish instead of timing out
- 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?

@Mryange

Mryange commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

/review

@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 — changes requested for two blocking issues.

The liveness problem is real, but the production deletion solves it by draining every pending scanner after shared LIMIT exhaustion. Since both schedulers call fallible prepare()/open() before Scanner::get_block() observes exhaustion, a satisfied query can do O(remaining ranges) setup/eager I/O and can fail on an otherwise-unused reader. The finite-exhausted path needs to finish/wake the context during scheduling when no completed/in-flight work remains.

The regression is not deterministic: parallelism and a five-second timeout make the lost wakeup plausible but do not force either a late context's initial exact-zero state or the separate zero-to-negative window. A buggy base schedule can pass with identical empty output.

Checkpoint conclusions:

  • Goal/scope/concurrency: the goal is valid; generic ScannerContext behavior affects OLAP and external/file readers and both schedulers. Lock order and ownership remain consistent, but the head's lifecycle workaround is too broad.
  • Lifecycle/error/performance/observability: no separate leak or static-initialization issue was found; existing status propagation creates the unused-reader failure, while current dependency debug state and scanner profiles expose the extra work.
  • Correctness/compatibility/config/persistence: final LIMIT truncation and the expected empty SQL results are correct. There is no production config, protocol/storage-format, FE-BE variable, transaction, persistence, or rolling-upgrade surface change.
  • Tests: setup/result conventions are sound, but deterministic lifecycle/open-count proof is missing. No build or test was run in this review-only environment. Current style/license checks pass; macOS BE UT did not reach runtime validation because its runner used JDK 25 instead of the required JDK 17.
  • User focus: no additional review focus was provided.

The two blocking findings are inline.

// exhausted; they would only open and immediately EOF.
if (_shared_scan_limit->load(std::memory_order_acquire) == 0) {
return nullptr;
}

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.

[P1] Avoid opening pending scanners after the LIMIT is satisfied

After this deletion, a context that did not produce the limiting row repeatedly schedules its remaining tasks: every consumed EOS calls schedule_scan_task() before the local finish check, and _scanner_scan() runs prepare()/open() before Scanner::get_block() sees the shared counter <= 0. With many tablet/file ranges this does O(remaining scanners) initialization/eager-read work; worse, an otherwise-unneeded submit/prepare/open error is propagated through _process_status, so a LIMIT query that already has enough rows can now fail.

The old guard can strand a late context whose first schedule already sees zero because its dependency starts blocked; a separate zero-to-negative window exists after EOS. Please keep a no-I/O finite-exhausted path (limit >= 0 && shared_remaining <= 0), but have the scheduling path itself finish/wake the context when it will submit no task and no completed/in-flight work remains (or synthesize a cheap EOS). If work remains in flight, let its callbacks return without starting new pending readers. Do not rely only on a later consumer check, which can be unreachable.


sql "SET enable_sql_cache = false"
sql "SET experimental_enable_local_shuffle = false"
sql "SET query_timeout = 5"

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.

[P2] Force the lost-wakeup ordering in this regression

query_timeout = 5 only detects the bug if scheduling happens to reproduce it. The base can hang when a late ScannerContext first schedules after a peer has set the finite shared counter to zero: its dependency starts blocked and the old guard submits no worker to wake it. There is also a separate active-context zero-to-negative window. Changing pipeline parallelism makes these schedules plausible but forces neither; allowed base schedules still produce these same empty qt_ results.

Please add a deterministic BE lifecycle test or regression debug-point/barrier that creates pending work with a finite remaining value of zero before initial scheduling and asserts EOS/dependency readiness; ideally also assert that the pending scanner is never opened. The zero-to-negative window can be covered separately. Once the state is forced, use a less load-sensitive outer timeout so slow CI is not indistinguishable from the lifecycle failure.

@Mryange

Mryange commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

/review 重新review一下,我觉得目前把shared limit 交给 get block 是比较简单的,如果你认为这样不要,请给出你认为比较好的设计访问(例如synthesize a cheap EOS这种),并且要仔细审视一下pipeline 和 scan的生命周期。

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

Re-review complete for 9b5ff496ed517a202b7262f1f0b70c351544db33. The existing P1 lifecycle issue and P2 test issue remain blocking. After duplicate suppression, this round found no additional distinct inline issue.

On the requested design focus: keeping the shared-LIMIT check in Scanner::get_block() is useful as a final safety net and it does restore liveness in this head, but I do not recommend making it the only lifecycle boundary. ScannerScheduler::_scanner_scan() calls prepare() and open() before get_block(), so an exhausted pending OLAP/file/JDBC/metadata scanner can still allocate/open remote readers, do eager work, and propagate an otherwise irrelevant failure after the query already has enough rows.

A safer design is:

  1. Define one finite-exhaustion predicate: limit >= 0 && shared_remaining <= 0. The finite check matters because -1 is the no-LIMIT sentinel; <= 0 matters because concurrent block subtraction can overshoot below zero.
  2. Under ScannerContext::_transfer_lock, stop selecting _pending_tasks and stop resubmitting the just-consumed non-EOS task once that predicate is true. Do not drop _completed_tasks: their rows have already decremented the shared counter and may still be needed downstream. Likewise, wait for existing _in_flight_tasks_num callbacks instead of closing a running scanner.
  3. Add the same context-owned exhaustion check at worker entry, before prepare/open, so a task queued before another context exhausted the budget returns through the normal empty-EOS callback without opening its reader. Do not infer this state only from Scanner::_shared_scan_limit: late scanner initialization currently maps any negative shared value to nullptr, so an overshot finite budget is otherwise indistinguishable from the -1 no-LIMIT sentinel. A task already inside a read may finish and publish its block.
  4. When _completed_tasks.empty() and _in_flight_tasks_num == 0, mark the context finished and call set_always_ready() even if pending scanners remain. This directly completes a late context and prevents both the initial exact-zero lost wakeup and the active zero-to-negative window.
  5. Preserve terminal accounting for a scanner that was already opened and produced a final block. Do not run close/profile work for a running scanner while holding _transfer_lock.

This context-level terminal path is cheaper and simpler than synthesizing one EOS task per pending scanner. A synthetic EOS is still useful for an already-queued worker because it reuses the normal push_back_scan_task() wakeup contract, but manufacturing O(scanner count) consumer turns is unnecessary.

The deterministic tests should force, rather than hope for, two schedules: (a) a finite context whose first scheduling attempt sees remaining exactly zero, asserting EOS/dependency readiness and zero prepare/open/read calls; and (b) two workers barriered around one-row publication so the counter deterministically overshoots negative while another scanner remains pending, asserting already-produced blocks are delivered, the pending reader is never opened, and all dependencies become permanently ready. The current three timeout-based SQL runs do not establish those states.

Checkpoint conclusions:

  • Goal/scope/concurrency: the goal and one-line scope are clear, and the head eventually makes progress. Per-context queue transitions remain protected by _transfer_lock, and both scheduler backends share the same callback/wakeup path. The remaining blocker is the too-late lifecycle boundary, not a new lock-order or deadlock issue.
  • Lifecycle/error/performance: draining every pending scanner after exhaustion preserves progress but performs O(remaining scanners) fallible initialization. Completed/in-flight work must be preserved; new pending I/O should stop. No separate ownership cycle, use-after-free, or static-lifecycle issue was found.
  • Parallel paths/compatibility/config/persistence: the generic behavior applies to OLAP and external scanners and both simplified schedulers. Key-TopN and no-LIMIT paths must remain excluded. There is no protocol, storage-format, FE-BE variable, configuration, transaction, persistence, or rolling-upgrade surface change.
  • Tests/observability: regression naming, ordering, setup, and expected empty output are consistent, but the lifecycle schedule and no-open property are not deterministic or observed. Existing dependency/scanner profiles are sufficient for diagnosis; a counting BE test is the stronger proof.
  • Validation: this is a review-only checkout with .worktree_initialized and thirdparty/installed absent, so no local build or test was run. Current formatter/license/title checks pass. The macOS BE UT job failed before build/test because its image had JDK 25 while the job requires JDK 17, so it provides no runtime evidence for this patch.

### What problem does this PR solve?

Issue Number: None

Related PR: apache#62222

Problem Summary: Shared scan-limit completion only recognized an exact remaining value of zero. Concurrent scanners can overshoot the finite budget below zero, while -1 still represents a query without LIMIT. Add a single finite-exhaustion predicate for ScannerContext completion and increase the regression timeout to avoid load-sensitive failures.

### Release note

Fix ScannerContext completion when a finite shared scan limit becomes negative.

### Check List (For Author)

- Test: No new test run for this follow-up
    - Previous regression and manual reproduction passed before the timeout-only adjustment
- Behavior changed: Yes, negative exhausted shared limits now complete the scanner context
- Does this need documentation: No
@Mryange

Mryange commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

/reivew 这个是一个fix的pr,提前return这个操作我们可能需要再后面的pr来考虑,回归的话现在设置成60s timeout了,基本上不会有出错的可能,可以先合入看看

@Mryange

Mryange commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17611	4018	4036	4018
q2	2059	331	207	207
q3	10240	1468	812	812
q4	4679	465	333	333
q5	7613	845	575	575
q6	197	176	138	138
q7	776	822	617	617
q8	9356	1695	1590	1590
q9	5634	4415	4395	4395
q10	6750	1775	1465	1465
q11	518	366	334	334
q12	738	592	461	461
q13	18058	3466	2780	2780
q14	272	267	251	251
q15	q16	801	774	714	714
q17	978	1070	934	934
q18	6944	5777	5615	5615
q19	1316	1318	1066	1066
q20	790	667	571	571
q21	5982	2661	2405	2405
q22	446	352	302	302
Total cold run time: 101758 ms
Total hot run time: 29583 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4388	4329	4291	4291
q2	297	323	219	219
q3	4600	4955	4453	4453
q4	2176	2179	1375	1375
q5	4414	4261	4245	4245
q6	230	175	127	127
q7	1730	2057	1756	1756
q8	2583	2239	2256	2239
q9	8011	8051	7850	7850
q10	4715	4683	4227	4227
q11	591	419	388	388
q12	746	784	554	554
q13	3425	3660	2925	2925
q14	295	319	275	275
q15	q16	750	738	657	657
q17	1384	1330	1338	1330
q18	8068	7327	7471	7327
q19	1192	1156	1086	1086
q20	2209	2205	1948	1948
q21	5298	4608	4422	4422
q22	517	445	415	415
Total cold run time: 57619 ms
Total hot run time: 52109 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 178690 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 0d1809173d0ffeedc74d33616b3fd887d945c4fe, data reload: false

query5	4324	620	504	504
query6	470	230	211	211
query7	4854	597	352	352
query8	339	193	169	169
query9	8778	4068	4120	4068
query10	468	355	302	302
query11	5890	2338	2146	2146
query12	156	102	101	101
query13	1267	540	428	428
query14	6210	5232	4967	4967
query14_1	4253	4222	4217	4217
query15	219	207	181	181
query16	1049	472	511	472
query17	1104	721	605	605
query18	2446	494	361	361
query19	216	196	155	155
query20	114	105	108	105
query21	244	159	143	143
query22	13502	13575	13313	13313
query23	17437	16463	16171	16171
query23_1	16294	16204	16237	16204
query24	7417	1777	1313	1313
query24_1	1291	1273	1301	1273
query25	572	480	403	403
query26	1362	360	221	221
query27	2569	601	389	389
query28	4429	1993	1986	1986
query29	1102	607	507	507
query30	341	282	229	229
query31	1115	1095	984	984
query32	106	65	61	61
query33	535	323	280	280
query34	1173	1121	650	650
query35	761	789	695	695
query36	1203	1189	1045	1045
query37	167	107	90	90
query38	1893	1758	1658	1658
query39	890	890	843	843
query39_1	857	851	830	830
query40	247	165	139	139
query41	65	63	63	63
query42	91	92	90	90
query43	322	328	280	280
query44	1401	783	781	781
query45	195	188	188	188
query46	1100	1164	737	737
query47	2189	2144	2037	2037
query48	393	402	283	283
query49	578	417	306	306
query50	1104	428	361	361
query51	10980	11235	10894	10894
query52	88	88	75	75
query53	261	295	216	216
query54	288	240	220	220
query55	72	70	65	65
query56	288	279	286	279
query57	1349	1255	1226	1226
query58	288	259	259	259
query59	1577	1649	1448	1448
query60	308	283	259	259
query61	158	154	151	151
query62	535	504	440	440
query63	256	211	206	206
query64	2843	1083	864	864
query65	4703	4625	4607	4607
query66	1825	547	378	378
query67	29312	29246	29114	29114
query68	3113	1556	1006	1006
query69	406	316	263	263
query70	1036	935	952	935
query71	359	351	326	326
query72	3018	2699	2337	2337
query73	820	788	440	440
query74	5019	4951	4706	4706
query75	2502	2499	2111	2111
query76	2325	1195	787	787
query77	352	386	288	288
query78	12029	11795	11532	11532
query79	1384	1156	761	761
query80	983	564	467	467
query81	526	331	284	284
query82	591	162	119	119
query83	407	335	302	302
query84	290	155	131	131
query85	999	636	540	540
query86	406	308	283	283
query87	1834	1819	1765	1765
query88	3729	2820	2788	2788
query89	430	380	329	329
query90	1856	203	200	200
query91	205	194	166	166
query92	62	61	57	57
query93	1586	1545	1080	1080
query94	606	361	322	322
query95	775	585	486	486
query96	1080	798	340	340
query97	2636	2642	2489	2489
query98	214	216	212	212
query99	1077	1116	973	973
Total cold run time: 263161 ms
Total hot run time: 178690 ms

@hello-stephen

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

query1	0.01	0.01	0.01
query2	0.10	0.05	0.04
query3	0.25	0.14	0.14
query4	1.61	0.14	0.14
query5	0.24	0.22	0.21
query6	1.25	1.07	1.08
query7	0.04	0.01	0.00
query8	0.05	0.04	0.04
query9	0.38	0.31	0.31
query10	0.56	0.56	0.55
query11	0.20	0.13	0.13
query12	0.19	0.14	0.14
query13	0.46	0.46	0.48
query14	1.02	1.01	1.00
query15	0.62	0.58	0.59
query16	0.32	0.31	0.30
query17	1.12	1.10	1.11
query18	0.23	0.22	0.21
query19	2.02	1.92	2.01
query20	0.02	0.01	0.01
query21	15.46	0.22	0.14
query22	4.89	0.06	0.05
query23	16.12	0.30	0.12
query24	3.05	0.44	0.31
query25	0.12	0.05	0.04
query26	0.72	0.20	0.16
query27	0.04	0.03	0.04
query28	3.54	0.97	0.53
query29	12.56	4.09	3.30
query30	0.28	0.16	0.16
query31	2.80	0.60	0.31
query32	3.23	0.59	0.49
query33	3.15	3.22	3.23
query34	15.47	4.20	3.53
query35	3.53	3.51	3.53
query36	0.56	0.46	0.42
query37	0.09	0.07	0.06
query38	0.06	0.04	0.04
query39	0.04	0.02	0.03
query40	0.19	0.16	0.15
query41	0.08	0.03	0.03
query42	0.04	0.03	0.03
query43	0.05	0.04	0.04
Total cold run time: 96.76 s
Total hot run time: 25.05 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 57.51% (23995/41724)
Line Coverage 41.23% (236060/572539)
Region Coverage 37.05% (186569/503493)
Branch Coverage 38.27% (83850/219107)

@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 75.00% (30524/40699)
Line Coverage 59.13% (336441/569010)
Region Coverage 55.79% (281970/505382)
Branch Coverage 57.16% (125324/219249)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants