This is an automated email from the ASF dual-hosted git repository.

Mryange pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new e1df149d617 [fix](local shuffle) Prevent local exchange under serial 
parent pipeline (#67490)
e1df149d617 is described below

commit e1df149d617d16066fec9c97d770c14b3ac045b2
Author: Mryange <[email protected]>
AuthorDate: Tue Sep 8 14:55:15 2026 +0800

    [fix](local shuffle) Prevent local exchange under serial parent pipeline 
(#67490)
    
    When FE local shuffle planning inserted a local exchange into a pipeline
    whose parent pipeline was serial, it could increase only the downstream
    pipeline task count while the paired sink and source operators remained
    one-to-one. This produced inconsistent pipeline concurrency and could
    fail during execution. Root cause: FE did not propagate the serial state
    of a parent pipeline across pipeline boundaries. This change tracks that
    state and skips local exchange insertion when the parent pipeline is
    serial, matching the existing BE planning constraint.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] 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 <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 .../glue/translator/PlanTranslatorContext.java     | 14 +++++++++
 .../org/apache/doris/planner/AddLocalExchange.java |  1 +
 .../java/org/apache/doris/planner/PlanNode.java    | 16 ++++++++--
 .../test_local_shuffle_rqg_bugs.groovy             | 35 ++++++++++++++++++++++
 4 files changed, 63 insertions(+), 3 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PlanTranslatorContext.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PlanTranslatorContext.java
index 0a09cd0670b..476579d2f12 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PlanTranslatorContext.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/PlanTranslatorContext.java
@@ -133,6 +133,12 @@ public class PlanTranslatorContext {
     // root and across pipeline boundaries (see shouldResetSerialFlagForChild).
     private final Map<PlanNodeId, Boolean> serialAncestorInPipelineMap = 
Maps.newHashMap();
 
+    // Per-node "does this pipeline have a serial parent pipeline" flag. 
Mirrors BE's
+    // Pipeline::num_tasks_of_parent() gate in _add_local_exchange: a pipeline 
whose parent
+    // has one task must not be split by another local exchange, because that 
would raise only
+    // the new source side to N tasks while the paired sink/source operators 
stay one-to-one.
+    private final Map<PlanNodeId, Boolean> serialParentPipelineMap = 
Maps.newHashMap();
+
     // Per-node "is there a downstream operator that depends on hash 
distribution for
     // correctness, with HASH/NOOP path connecting it to me" flag.  Mirrors 
BE's
     // _followed_by_shuffled_operator propagation in 
pipeline_fragment_context.cpp.
@@ -292,6 +298,14 @@ public class PlanTranslatorContext {
         return serialAncestorInPipelineMap.getOrDefault(node.getId(), false);
     }
 
+    public void setHasSerialParentPipeline(PlanNode node, boolean value) {
+        serialParentPipelineMap.put(node.getId(), value);
+    }
+
+    public boolean hasSerialParentPipeline(PlanNode node) {
+        return serialParentPipelineMap.getOrDefault(node.getId(), false);
+    }
+
     public void setHasShuffleForCorrectnessAncestor(PlanNode node, boolean 
value) {
         shuffledAncestorMap.put(node.getId(), value);
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/planner/AddLocalExchange.java 
b/fe/fe-core/src/main/java/org/apache/doris/planner/AddLocalExchange.java
index 1adea56c13e..b5cf3f49766 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/AddLocalExchange.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/AddLocalExchange.java
@@ -197,6 +197,7 @@ public class AddLocalExchange {
                 ? LocalExchangeTypeRequire.noRequire() : 
sink.getLocalExchangeTypeRequire();
         PlanNode root = fragment.getPlanRoot();
         context.setHasSerialAncestorInPipeline(root, false);
+        context.setHasSerialParentPipeline(root, false);
         Pair<PlanNode, LocalExchangeType> output = root
                 .enforceAndDeriveLocalExchange(context, null, require);
         PlanNode newRoot = output.first;
diff --git a/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java 
b/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java
index a635ca26730..b592c15f627 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java
@@ -50,6 +50,7 @@ import org.apache.doris.thrift.TPushAggOp;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Suppliers;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import org.apache.commons.collections4.CollectionUtils;
@@ -65,6 +66,7 @@ import java.util.Map.Entry;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 /**
@@ -1069,7 +1071,12 @@ public abstract class PlanNode extends 
TreeNode<PlanNode> {
         //   node's sink, e.g. Exchange is in AGG_Sink pipeline).
         // For non-splitting operators (shouldReset=false, e.g. streaming AGG):
         //   Inherit parent's serial flag + this node's own.
-        boolean inheritedSerial = shouldResetSerialFlagForChild(childIndex)
+        boolean startsNewPipeline = shouldResetSerialFlagForChild(childIndex);
+        Supplier<Boolean> currentNodeSerialOnBe = Suppliers.memoize(
+                () -> 
isSerialOperatorOnBe(translatorContext.getConnectContext()));
+        boolean currentPipelineSerial = 
translatorContext.hasSerialAncestorInPipeline(this)
+                || currentNodeSerialOnBe.get();
+        boolean inheritedSerial = startsNewPipeline
                 ? false : translatorContext.hasSerialAncestorInPipeline(this);
         // Use isSerialOperatorOnBe (= isSerialNode && 
fragment.useSerialSource) instead of the
         // raw isSerialNode().  BE's OperatorBase reads the Thrift 
`is_serial_operator` flag —
@@ -1078,8 +1085,10 @@ public abstract class PlanNode extends 
TreeNode<PlanNode> {
         // Using isSerialNode here would set the child's serial-ancestor flag 
wider than BE's
         // view and over-skip required LocalExchanges downstream.
         boolean childHasSerialAncestor = inheritedSerial
-                || isSerialOperatorOnBe(translatorContext.getConnectContext());
+                || currentNodeSerialOnBe.get();
         translatorContext.setHasSerialAncestorInPipeline(child, 
childHasSerialAncestor);
+        translatorContext.setHasSerialParentPipeline(child, startsNewPipeline
+                ? currentPipelineSerial : 
translatorContext.hasSerialParentPipeline(this));
 
         // 1b. Propagate shuffle-for-correctness-ancestor flag to child.
         // Mirrors BE's _followed_by_shuffled_operator: a downstream operator 
needs hash
@@ -1140,7 +1149,8 @@ public abstract class PlanNode extends TreeNode<PlanNode> 
{
         // Use isSerialOperatorOnBe (not isSerialNode) because BE's 
Pipeline::need_to_local_exchange
         // checks op->is_serial_operator() which reads the Thrift flag set 
from isSerialOperatorOnBe;
         // when fragment.useSerialSource is false, BE treats this node as 
non-serial.
-        if (translatorContext.hasSerialAncestorInPipeline(this)
+        if (translatorContext.hasSerialParentPipeline(this)
+                || translatorContext.hasSerialAncestorInPipeline(this)
                 || 
isSerialOperatorOnBe(translatorContext.getConnectContext())) {
             return childOutput;
         }
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 4401f0362d3..f53ab6204f5 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
@@ -1663,5 +1663,40 @@ suite("test_local_shuffle_rqg_bugs") {
         assertTrue(false, "Bug 26: ${t.message}")
     }
 
+    // Bug 27: do not insert a local exchange in a pipeline whose parent 
pipeline is serial.
+    // Otherwise the local exchange raises the lower AggSink pipeline to N 
tasks while its
+    // paired AggSource remains at one task, leaving task 1+ without a source 
dependency.
+    def bug27Query = { planner -> """
+        SELECT /*+SET_VAR(enable_local_shuffle_planner=${planner},
+                          enable_local_exchange_before_agg=false,
+                          enable_local_exchange_before_streaming_agg=true,
+                          enable_broadcast_join_force_passthrough=true,
+                          enable_share_hash_table_for_broadcast_join=false,
+                          parallel_pipeline_task_num=3,
+                          enable_sql_cache=false)*/
+               COUNT(*),
+               COUNT(DISTINCT CAST(f.pk AS STRING)),
+               MIN(CAST(f.pk AS STRING)),
+               MAX(CAST(f.pk AS STRING)),
+               COUNT(DISTINCT CAST(d.pk AS STRING)),
+               MIN(CAST(d.pk AS STRING)),
+               MAX(CAST(d.pk AS STRING)),
+               COUNT(DISTINCT CAST(f.col_int_undef_signed AS STRING)),
+               MIN(CAST(f.col_int_undef_signed AS STRING)),
+               MAX(CAST(f.col_int_undef_signed AS STRING))
+        FROM rqg_t1 f
+        INNER JOIN (
+            SELECT * FROM rqg_t2 d
+            WHERE d.col_int_undef_signed = 1 AND COALESCE(d.pk, -1) >= 10
+        ) d ON d.col_int_undef_signed = f.col_int_undef_signed
+            AND f.col_int_undef_signed = f.col_int_undef_signed2
+            AND f.pk = d.pk
+    """ }
+
+    def bug27BeResult = sql bug27Query(false)
+    for (int i = 0; i < 20; i++) {
+        assertEquals(bug27BeResult, sql(bug27Query(true)), "Bug 27 run ${i}")
+    }
+
     logger.info("=== All RQG bug reproduction tests completed ===")
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to