github-actions[bot] commented on code in PR #68185:
URL: https://github.com/apache/doris/pull/68185#discussion_r4045751498


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/properties/ChildrenPropertiesRegulator.java:
##########
@@ -58,6 +58,8 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+import org.apache.logging.log4j.LogManager;

Review Comment:
   [P1] Remove the unused imports that fail CheckStyle
   
   Neither `LogManager` nor `Logger` is referenced in this file. The exact-head 
CheckStyle job already rejects these two added lines (and reports no other 
Checkstyle errors), so this PR cannot pass the required gate until both imports 
are removed or used.



##########
fe/fe-core/src/main/java/org/apache/doris/planner/ExchangeNode.java:
##########
@@ -187,6 +187,13 @@ public boolean isSerialNode() {
 
     @Override
     public boolean isSerialOperatorOnBe(ConnectContext context) {
+        if (context != null && 
context.getSessionVariable().isEnableLocalShufflePlanner()) {

Review Comment:
   [P1] Preserve serial handling for funneled non-bucket exchanges
   
   Removing the fragment-level serial-scan condition here makes a RANDOM 
exchange in a pooled scan fragment non-serial. Mixed partitioned/unpartitioned 
set operations create that shape: the partitioned scan child is absorbed, while 
the unpartitioned child remains behind a RANDOM exchange. `DistributePlanner` 
still sends every non-bucket input only to `getFirstInstancePerWorker()`, and 
the new zero-sender completion only recognizes bucket shuffle. The other local 
tasks therefore construct receivers with the full sender count but get no 
channel or EOS, so the query can wait forever. Please keep these funneled 
exchanges serial, or spread and terminate their receivers as well.



##########
fe/fe-core/src/main/java/org/apache/doris/planner/AddLocalExchange.java:
##########
@@ -81,11 +90,107 @@ public void 
addLocalExchange(FragmentIdMapping<DistributedPlan> distributedPlans
             if (maxPerBeInstances <= 1) {
                 continue;
             }
+            context.setCurrentFragmentBucketUpgradeEligible(
+                    isBucketUpgradeEligible(pipePlan, maxPerBeInstances, 
context));
             PlanFragment fragment = pipePlan.getFragmentJob().getFragment();
             addLocalExchangeForFragment(fragment, context);
         }
     }
 
+    /**
+     * Bucket → local-hash parallelism upgrade eligibility .
+     *
+     * A pooled bucket-join fragment runs its bucket joins at bucket-count 
parallelism:
+     * each LocalShuffleBucketJoinAssignedJob owns a disjoint set of join 
buckets and only
+     * instances with buckets do join work (e.g. 8 buckets/BE but 16 
instances/BE → 8 idle).
+     * When nothing above the join needs bucket alignment, HashJoinNode can 
re-distribute
+     * both sides with LOCAL_EXECUTION_HASH_SHUFFLE to use all instances — see
+     * {@link HashJoinNode#enforceAndDeriveLocalExchange}.
+     *
+     * This method computes the per-fragment numeric condition from the actual 
instance
+     * assignment: maxPerBeInstances > maxBucketsWithDataPerWorker × ratio.  
The ratio comes
+     * from session variable {@code local_shuffle_bucket_upgrade_ratio}; 
values <= 1 disable
+     * the upgrade entirely (a required parallelism gain of at most 1x means 
no gain).
+     */
+    private boolean isBucketUpgradeEligible(PipelineDistributedPlan pipePlan,
+            long maxPerBeInstances, PlanTranslatorContext context) {
+        ConnectContext connectContext = context.getConnectContext();
+        if (connectContext == null || connectContext.getSessionVariable() == 
null) {
+            return false;
+        }
+        double ratio = 
connectContext.getSessionVariable().getLocalShuffleBucketUpgradeRatio();
+        List<AssignedJob> instanceJobs = pipePlan.getInstanceJobs();
+        if (instanceJobs.isEmpty()
+                || 
!instanceJobs.stream().allMatch(LocalShuffleBucketJoinAssignedJob.class::isInstance))
 {
+            // Only pooled bucket-join fragments have the bucket-count 
parallelism cap.
+            return false;
+        }
+        Map<Long, Set<Integer>> bucketsPerWorker = new HashMap<>();
+        Map<Long, Integer> instancesPerWorker = new HashMap<>();
+        Map<Long, Integer> coresPerWorker = new HashMap<>();
+        for (AssignedJob job : instanceJobs) {
+            long workerId = job.getAssignedWorker().id();
+            bucketsPerWorker.computeIfAbsent(workerId, k -> new HashSet<>())
+                    .addAll(((LocalShuffleBucketJoinAssignedJob) 
job).getAssignedJoinBucketIndexes());
+            instancesPerWorker.merge(workerId, 1, Integer::sum);
+            coresPerWorker.computeIfAbsent(workerId, k -> 
resolveWorkerCores(job.getAssignedWorker()));
+        }
+        // Conservative: every worker that owns buckets must clear the gain 
bar. The gain is
+        // computed on EFFECTIVE parallelism (capped by the BE's executor 
threads): when the
+        // bucket count already saturates the cores, adding instances cannot 
speed the join
+        // up and the extra local exchange is a pure cost.
+        boolean anyBuckets = false;
+        for (Map.Entry<Long, Set<Integer>> entry : 
bucketsPerWorker.entrySet()) {
+            int buckets = entry.getValue().size();
+            if (buckets == 0) {
+                continue;
+            }
+            anyBuckets = true;
+            int instances = instancesPerWorker.getOrDefault(entry.getKey(), 0);
+            int cores = coresPerWorker.getOrDefault(entry.getKey(), 
Integer.MAX_VALUE);
+            if (!shouldUpgradeBucketParallelism(ratio,
+                    Math.min(instances, cores), Math.min(buckets, cores))) {
+                return false;
+            }
+        }
+        return anyBuckets;
+    }
+
+    /**
+     * Effective execution threads of the worker's backend 
(pipelineExecutorSize, falling
+     * back to cpuCores). Values <= 1 mean the heartbeat has not reported yet 
— treat the
+     * capacity as unknown/uncapped rather than blocking the upgrade.
+     */
+    private static int resolveWorkerCores(
+            
org.apache.doris.nereids.trees.plans.distribute.worker.DistributedPlanWorker 
worker) {
+        if (worker instanceof 
org.apache.doris.nereids.trees.plans.distribute.worker.BackendWorker) {
+            org.apache.doris.system.Backend backend =
+                    
((org.apache.doris.nereids.trees.plans.distribute.worker.BackendWorker) 
worker).getBackend();
+            int size = backend.getPipelineExecutorSize();
+            if (size <= 1) {

Review Comment:
   [P2] Honor a reported single pipeline executor
   
   `pipeline_executor_size = 1` is a valid configured capacity: the BE reports 
every positive value verbatim, but this branch treats one as ‘not reported’ and 
replaces it with the machine CPU count. On a many-core BE intentionally 
configured with one pipeline executor, the ratio can therefore enable two local 
hash exchanges even though effective concurrency remains one, making this 
default-on optimization pure overhead. Please distinguish the unreported state, 
or conservatively preserve any positive reported executor size.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to