This is an automated email from the ASF dual-hosted git repository.
huajianlan pushed a commit to branch fe_local_shuffle
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/fe_local_shuffle by this push:
new b0475298d70 [fix](fe) Skip PASSTHROUGH LE wrapping for Exchange under
serial ancestor
b0475298d70 is described below
commit b0475298d7033f9c74e09c4bc3cf2d8f055313d8
Author: 924060929 <[email protected]>
AuthorDate: Thu Apr 9 18:32:35 2026 +0800
[fix](fe) Skip PASSTHROUGH LE wrapping for Exchange under serial ancestor
### What problem does this PR solve?
Issue Number: close #xxx
Problem Summary:
When a serial NLJ (RIGHT_OUTER/FULL_OUTER) has an UNPARTITIONED Exchange
child, ExchangeNode.enforceAndDeriveLocalExchange() wraps the Exchange
with a PASSTHROUGH LocalExchange. On BE, the NLJ_PROBE (serial) sets
pipeline num_tasks=1, then the LE handler set_num_tasks(_num_instances)
overrides it to N, triggering DCHECK crash: serial operator in pipeline
with num_tasks > 1.
Fix: Check hasSerialAncestorInPipeline before wrapping. When a serial
ancestor already constrains the pipeline to 1 task, inserting a
PASSTHROUGH LE would create a pipeline boundary where the BE LE handler
resets num_tasks, conflicting with the serial constraint. Return NOOP
instead to keep the Exchange directly in the serial pipeline.
### Release note
None
### Check List (For Author)
- Test: Regression test (Bug 18) + manual test with various pptn values
- Behavior changed: No
- Does this need documentation: No
Co-authored-by: Copilot <[email protected]>
---
.../org/apache/doris/planner/ExchangeNode.java | 9 ++++
.../test_local_shuffle_rqg_bugs.groovy | 53 ++++++++++++++++++++++
2 files changed, 62 insertions(+)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/planner/ExchangeNode.java
b/fe/fe-core/src/main/java/org/apache/doris/planner/ExchangeNode.java
index 60d42500d5e..4f03834ab9b 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/ExchangeNode.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/ExchangeNode.java
@@ -189,6 +189,15 @@ public class ExchangeNode extends PlanNode {
&& fragment != null
&& fragment.useSerialSource(ConnectContext.get());
if (willBeSerialOnBe) {
+ // If there is already a serial ancestor in the same pipeline
(e.g., serial NLJ
+ // for RIGHT_OUTER/FULL_OUTER join), don't insert any local
exchange. The serial
+ // ancestor already constrains the pipeline to 1 task. Inserting a
PASSTHROUGH LE
+ // would create a pipeline boundary where the BE
LOCAL_EXCHANGE_NODE handler calls
+ // set_num_tasks(_num_instances), overriding the serial constraint
and causing a
+ // DCHECK crash (serial operator in pipeline with num_tasks > 1).
+ if (translatorContext.hasSerialAncestorInPipeline(this)) {
+ return Pair.of(this, LocalExchangeType.NOOP);
+ }
// Serial exchange → 1 task. Must fan out to N tasks for
downstream operators.
// For HASH/BUCKET exchanges: return NOOP and let parent insert
the appropriate
// redistribution (HASH_SHUFFLE or BUCKET_HASH_SHUFFLE).
PASSTHROUGH round-robin
diff --git
a/regression-test/suites/nereids_p0/local_shuffle/test_local_shuffle_rqg_bugs.groovy
b/regression-test/suites/nereids_p0/local_shuffle/test_local_shuffle_rqg_bugs.groovy
index f838e5f61de..f2d2dd71072 100644
---
a/regression-test/suites/nereids_p0/local_shuffle/test_local_shuffle_rqg_bugs.groovy
+++
b/regression-test/suites/nereids_p0/local_shuffle/test_local_shuffle_rqg_bugs.groovy
@@ -988,5 +988,58 @@ suite("test_local_shuffle_rqg_bugs") {
assertTrue(false, "Bug 16+17: Serial AnalyticEval crash/hang:
${t.message}")
}
+ // Bug 18: DCHECK crash in Pipeline::set_num_tasks when PASSTHROUGH LE is
inserted
+ // between serial NLJ and its child Exchange.
+ // Root cause: ExchangeNode.enforceAndDeriveLocalExchange wraps
UNPARTITIONED serial
+ // Exchange with PASSTHROUGH LE. On BE, NLJ_PROBE (serial) sets pipeline
num_tasks=1,
+ // then the LE handler's set_num_tasks(_num_instances) overrides it to N,
triggering
+ // DCHECK (serial operator in pipeline with num_tasks > 1).
+ // Fix: skip PASSTHROUGH wrapping when hasSerialAncestorInPipeline is
true.
+ // Query: LEFT JOIN with always-true self-ref condition (table.pk =
table.pk) creates
+ // RIGHT_OUTER NLJ (serial). With pptn>1 and ignore_data_distribution,
the fragment
+ // gets N instances but NLJ forces 1 task.
+ try {
+ logger.info("Bug 18: Testing serial NLJ with PASSTHROUGH LE crash")
+ // Use existing rqg_t1 table (10 rows, 10 buckets)
+ def bug18_baseline = sql """
+ SELECT /*+SET_VAR(enable_local_shuffle_planner=false,
+ enable_sql_cache=false)*/
+ table1.col_varchar_10__undef_signed AS field1
+ FROM rqg_t1 AS table1
+ LEFT JOIN rqg_t1 AS table2
+ ON table2.pk = table2.pk
+ WHERE table1.pk BETWEEN 2 AND 11
+ GROUP BY field1
+ ORDER BY 1
+ """
+
+ // Test with various pptn values — crash requires pptn > 1
+ for (int ppt : [4, 7]) {
+ def bug18_result = sql """
+ SELECT /*+SET_VAR(use_serial_exchange=false,
+ parallel_pipeline_task_num=${ppt},
+ enable_local_shuffle_planner=true,
+ ignore_storage_data_distribution=true,
+ enable_sql_cache=false,
+
enable_share_hash_table_for_broadcast_join=false,
+ enable_broadcast_join_force_passthrough=true,
+ enable_parallel_scan=false)*/
+ table1.col_varchar_10__undef_signed AS field1
+ FROM rqg_t1 AS table1
+ LEFT JOIN rqg_t1 AS table2
+ ON table2.pk = table2.pk
+ WHERE table1.pk BETWEEN 2 AND 11
+ GROUP BY field1
+ ORDER BY 1
+ """
+ assertEquals(bug18_baseline, bug18_result,
+ "Bug 18 pptn=${ppt}: result mismatch with serial NLJ + local
exchange")
+ }
+ logger.info("Bug 18: PASSED (no crash, correct results)")
+ } catch (Throwable t) {
+ logger.error("Bug 18 FAILED: ${t.message}")
+ assertTrue(false, "Bug 18: Serial NLJ PASSTHROUGH LE crash:
${t.message}")
+ }
+
logger.info("=== All RQG bug reproduction tests completed ===")
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]