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

924060929 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 b747c34914a [fix](local shuffle) address all receiver instances for a 
non-serial exchange (#65348)
b747c34914a is described below

commit b747c34914afd25a853c76e97fe8a531df6f5b93
Author: 924060929 <[email protected]>
AuthorDate: Wed Jul 8 14:37:41 2026 +0800

    [fix](local shuffle) address all receiver instances for a non-serial 
exchange (#65348)
    
    Related PR: #64793
    
    Problem Summary:
    
    PR #64793 decoupled an exchange's serial flag from the fragment's serial
    scan under the FE local-shuffle planner, so a local-shuffle fragment can
    now host a **non-serial** RANDOM/HASH exchange whose BE receiver runs on
    every instance and waits for the full sender set.
    
    However `DistributePlanner.filterInstancesWhichCanReceiveDataFromRemote`
    still funnels the sender's destinations to the first instance per worker
    whenever the receiver uses local shuffle (keyed on
    `LocalShuffleAssignedJob`), and #64793 only added destination spreading
    for `BUCKET_SHUFFLE`, not for RANDOM/HASH. So the sender addresses only
    instance 0 while the non-first instances each build a receiver expecting
    the full sender set — they wait forever for an EOS that never arrives,
    and a query such as an MV partition-union rewrite (a RANDOM exchange
    feeding a UNION) hangs until query timeout.
    
    This PR gates the funnel on `ExchangeNode.isSerialOperatorOnBe` instead
    of `LocalShuffleAssignedJob`: only a serial exchange runs a single
    receiver per worker and may funnel to it; a non-serial exchange must
    address every receiver instance. `BUCKET_SHUFFLE` is unchanged (it
    re-spreads by bucket in `getDestinationsByBuckets`). This mirrors the
    `!isSerialOperatorOnBe` gate already used there.
    
    Verified end-to-end on a cloud single-BE cluster: the co-located `UNION
    <- BUCKETED AGGREGATE <- OlapScan` plan hangs at query timeout before
    the fix and returns in ~1s after it. Added
    `DistributePlannerReceiverDestinationTest` covering the serial /
    non-serial / shared-broadcast-hash-table cases.
    
    ### Release note
    
    Fix a query hang (until timeout) where a non-serial RANDOM/HASH exchange
    feeding a local-shuffle receiver fragment addressed only the first
    instance per worker, leaving the other instances waiting forever for
    sender EOS.
---
 .../trees/plans/distribute/DistributePlanner.java  |  17 ++-
 .../DistributePlannerReceiverDestinationTest.java  | 120 +++++++++++++++++++++
 2 files changed, 132 insertions(+), 5 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/DistributePlanner.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/DistributePlanner.java
index fd1ff52f5f9..5568d332f23 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/DistributePlanner.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/distribute/DistributePlanner.java
@@ -30,7 +30,6 @@ import 
org.apache.doris.nereids.trees.plans.distribute.worker.job.AssignedJob;
 import 
org.apache.doris.nereids.trees.plans.distribute.worker.job.AssignedJobBuilder;
 import 
org.apache.doris.nereids.trees.plans.distribute.worker.job.BucketScanSource;
 import 
org.apache.doris.nereids.trees.plans.distribute.worker.job.DefaultScanSource;
-import 
org.apache.doris.nereids.trees.plans.distribute.worker.job.LocalShuffleAssignedJob;
 import 
org.apache.doris.nereids.trees.plans.distribute.worker.job.LocalShuffleBucketJoinAssignedJob;
 import 
org.apache.doris.nereids.trees.plans.distribute.worker.job.StaticAssignedJob;
 import 
org.apache.doris.nereids.trees.plans.distribute.worker.job.UnassignedJob;
@@ -50,6 +49,7 @@ import org.apache.doris.qe.StmtExecutor;
 import org.apache.doris.thrift.TPartitionType;
 import org.apache.doris.thrift.TUniqueId;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.LinkedHashMultimap;
@@ -260,13 +260,20 @@ public class DistributePlanner {
         return connectContext != null && 
connectContext.getSessionVariable().isEnableLocalShufflePlanner();
     }
 
-    private List<AssignedJob> filterInstancesWhichCanReceiveDataFromRemote(
+    @VisibleForTesting
+    List<AssignedJob> filterInstancesWhichCanReceiveDataFromRemote(
             PipelineDistributedPlan receiverPlan,
             boolean enableShareHashTableForBroadcastJoin,
             ExchangeNode linkNode) {
-        boolean useLocalShuffle = receiverPlan.getInstanceJobs().stream()
-                .anyMatch(LocalShuffleAssignedJob.class::isInstance);
-        if (useLocalShuffle) {
+        // Funnel to the first instance per worker only when the exchange runs 
a single receiver
+        // per worker on BE, i.e. it is serial. A non-serial exchange builds a 
live receiver on
+        // every instance, each waiting for the full sender set, so the sender 
must address all of
+        // them; funneling would leave the non-first instances blocked forever 
on an EOS that never
+        // arrives. This must not key on LocalShuffleAssignedJob: once the FE 
local-shuffle planner
+        // decoupled an exchange's serial flag from the fragment's serial 
scan, a local-shuffle
+        // fragment can host a non-serial RANDOM/HASH exchange, and only the 
BUCKET_SHUFFLE path
+        // re-spreads its destinations (see getDestinationsByBuckets).
+        if 
(linkNode.isSerialOperatorOnBe(statementContext.getConnectContext())) {
             return getFirstInstancePerWorker(receiverPlan.getInstanceJobs());
         } else if (enableShareHashTableForBroadcastJoin && 
linkNode.isRightChildOfBroadcastHashJoin()) {
             return getFirstInstancePerWorker(receiverPlan.getInstanceJobs());
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/distribute/DistributePlannerReceiverDestinationTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/distribute/DistributePlannerReceiverDestinationTest.java
new file mode 100644
index 00000000000..220062dfb95
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/distribute/DistributePlannerReceiverDestinationTest.java
@@ -0,0 +1,120 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.plans.distribute;
+
+import org.apache.doris.nereids.StatementContext;
+import 
org.apache.doris.nereids.trees.plans.distribute.worker.DistributedPlanWorker;
+import org.apache.doris.nereids.trees.plans.distribute.worker.job.AssignedJob;
+import 
org.apache.doris.nereids.trees.plans.distribute.worker.job.LocalShuffleAssignedJob;
+import org.apache.doris.planner.ExchangeNode;
+import org.apache.doris.planner.PlanFragment;
+import org.apache.doris.qe.ConnectContext;
+
+import com.google.common.collect.Lists;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.List;
+
+/**
+ * Verifies that DistributePlanner picks the receiver instances a remote 
sender addresses
+ * based on whether the exchange is serial on BE -- not on whether the 
receiver fragment
+ * happens to use local shuffle.
+ *
+ * A non-serial exchange runs a live receiver on every instance, so the sender 
must address
+ * them all. Funneling such an exchange to the first instance per worker 
leaves the other
+ * instances waiting forever for an EOS that no sender sends (query hangs 
until timeout).
+ */
+public class DistributePlannerReceiverDestinationTest {
+
+    private DistributePlanner newPlanner() {
+        List<PlanFragment> noFragments = Lists.newArrayList();
+        return new DistributePlanner(Mockito.mock(StatementContext.class), 
noFragments, false, false);
+    }
+
+    private AssignedJob instanceOn(DistributedPlanWorker worker) {
+        LocalShuffleAssignedJob instance = 
Mockito.mock(LocalShuffleAssignedJob.class);
+        Mockito.when(instance.getAssignedWorker()).thenReturn(worker);
+        return instance;
+    }
+
+    private ExchangeNode exchange(boolean serialOnBe, boolean 
rightChildOfBroadcastJoin) {
+        ExchangeNode node = Mockito.mock(ExchangeNode.class);
+        
Mockito.when(node.isSerialOperatorOnBe(Mockito.nullable(ConnectContext.class))).thenReturn(serialOnBe);
+        
Mockito.when(node.isRightChildOfBroadcastHashJoin()).thenReturn(rightChildOfBroadcastJoin);
+        return node;
+    }
+
+    private PipelineDistributedPlan receiverWith(List<AssignedJob> instances) {
+        PipelineDistributedPlan plan = 
Mockito.mock(PipelineDistributedPlan.class);
+        Mockito.when(plan.getInstanceJobs()).thenReturn(instances);
+        return plan;
+    }
+
+    @Test
+    public void nonSerialExchangeAddressesEveryReceiverInstance() {
+        // 4 receiver instances across 2 workers (e.g. a bucket-to-hash 
upgraded fragment)
+        DistributedPlanWorker w0 = Mockito.mock(DistributedPlanWorker.class);
+        DistributedPlanWorker w1 = Mockito.mock(DistributedPlanWorker.class);
+        AssignedJob i0 = instanceOn(w0);
+        AssignedJob i1 = instanceOn(w0);
+        AssignedJob i2 = instanceOn(w1);
+        AssignedJob i3 = instanceOn(w1);
+        List<AssignedJob> all = Lists.newArrayList(i0, i1, i2, i3);
+
+        List<AssignedJob> destinations = 
newPlanner().filterInstancesWhichCanReceiveDataFromRemote(
+                receiverWith(all), false, exchange(false, false));
+
+        // Non-serial exchange: every instance owns a live receiver, so all 
must be addressed.
+        Assertions.assertEquals(all, destinations);
+    }
+
+    @Test
+    public void serialExchangeFunnelsToFirstInstancePerWorker() {
+        DistributedPlanWorker w0 = Mockito.mock(DistributedPlanWorker.class);
+        DistributedPlanWorker w1 = Mockito.mock(DistributedPlanWorker.class);
+        AssignedJob i0 = instanceOn(w0);
+        AssignedJob i1 = instanceOn(w0);
+        AssignedJob i2 = instanceOn(w1);
+        AssignedJob i3 = instanceOn(w1);
+        List<AssignedJob> all = Lists.newArrayList(i0, i1, i2, i3);
+
+        List<AssignedJob> destinations = 
newPlanner().filterInstancesWhichCanReceiveDataFromRemote(
+                receiverWith(all), false, exchange(true, false));
+
+        // Serial exchange: BE runs a single receiver per worker, funnel to 
the first instance.
+        Assertions.assertEquals(Lists.newArrayList(i0, i2), destinations);
+    }
+
+    @Test
+    public void broadcastShareHashTableFunnelsEvenWhenNonSerial() {
+        DistributedPlanWorker w0 = Mockito.mock(DistributedPlanWorker.class);
+        DistributedPlanWorker w1 = Mockito.mock(DistributedPlanWorker.class);
+        AssignedJob i0 = instanceOn(w0);
+        AssignedJob i1 = instanceOn(w0);
+        AssignedJob i2 = instanceOn(w1);
+        List<AssignedJob> all = Lists.newArrayList(i0, i1, i2);
+
+        List<AssignedJob> destinations = 
newPlanner().filterInstancesWhichCanReceiveDataFromRemote(
+                receiverWith(all), true, exchange(false, true));
+
+        // Shared-hash-table broadcast build still funnels one build per 
worker.
+        Assertions.assertEquals(Lists.newArrayList(i0, i2), destinations);
+    }
+}


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

Reply via email to