xiangfu0 commented on code in PR #19944:
URL: https://github.com/apache/pinot/pull/19944#discussion_r4343867923


##########
pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/EquivalentStagesReplacer.java:
##########
@@ -55,33 +77,58 @@ public interface OnSubstitution {
   }
 
   private static class Replacer extends 
PlanNodeVisitor.DepthFirstVisitor<Void, GroupedStages> {
+    private final PlanNode _root;
     private final OnSubstitution _listener;
+    private final boolean _guarded;
+    private final List<SpoolSubstitution> _substitutions = new ArrayList<>();
+    /// A stage can be visited more than once (for example the leader of a 
group), so a receive is decided only once.
+    private final Set<MailboxReceiveNode> _visitedReceives = 
Collections.newSetFromMap(new IdentityHashMap<>());
 
-    public Replacer(OnSubstitution listener) {
+    public Replacer(PlanNode root, OnSubstitution listener, boolean guarded) {
+      _root = root;
       _listener = listener;
+      _guarded = guarded;
     }
 
     @Override
     public Void visitMailboxReceive(MailboxReceiveNode node, GroupedStages 
equivalenceGroups) {
+      if (!_visitedReceives.add(node)) {
+        return null;
+      }
       MailboxSendNode sender = node.getSender();
       MailboxSendNode leader = equivalenceGroups.getGroup(sender).first();
-      if (canSubstitute(sender, leader)) {
-        // we don't want to visit the children of the node given it is going 
to be pruned
-        node.setSender(leader);
-        leader.addReceiver(node);
-        _listener.onSubstitution(node.getStageId(), sender.getStageId(), 
leader.getStageId());
-      } else {
+      if (sender == leader) {
         visitMailboxSend(leader, equivalenceGroups);

Review Comment:
   MINOR (perf): under `auto`, the guard re-runs `findRisk` over the whole plan 
after every accepted substitution, so this is roughly O(substitutions x 
plan-size), and `findRisk` itself builds an O(deferrals^2) matrix where 
deferrals can grow with the cartesian product of receives per join/union. For 
plans with many equivalent stages that could add noticeable planning latency. 
It's gated behind spools being present and only on `auto`, so probably fine in 
practice — but worth confirming on a worst-case plan (many shared stages 
feeding several joins) before this is the default.



##########
pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/EquivalentStagesReplacer.java:
##########
@@ -55,33 +77,58 @@ public interface OnSubstitution {
   }
 
   private static class Replacer extends 
PlanNodeVisitor.DepthFirstVisitor<Void, GroupedStages> {
+    private final PlanNode _root;
     private final OnSubstitution _listener;
+    private final boolean _guarded;
+    private final List<SpoolSubstitution> _substitutions = new ArrayList<>();
+    /// A stage can be visited more than once (for example the leader of a 
group), so a receive is decided only once.
+    private final Set<MailboxReceiveNode> _visitedReceives = 
Collections.newSetFromMap(new IdentityHashMap<>());
 
-    public Replacer(OnSubstitution listener) {
+    public Replacer(PlanNode root, OnSubstitution listener, boolean guarded) {
+      _root = root;
       _listener = listener;
+      _guarded = guarded;
     }
 
     @Override
     public Void visitMailboxReceive(MailboxReceiveNode node, GroupedStages 
equivalenceGroups) {
+      if (!_visitedReceives.add(node)) {
+        return null;
+      }
       MailboxSendNode sender = node.getSender();
       MailboxSendNode leader = equivalenceGroups.getGroup(sender).first();
-      if (canSubstitute(sender, leader)) {
-        // we don't want to visit the children of the node given it is going 
to be pruned
-        node.setSender(leader);
-        leader.addReceiver(node);
-        _listener.onSubstitution(node.getStageId(), sender.getStageId(), 
leader.getStageId());
-      } else {
+      if (sender == leader) {
         visitMailboxSend(leader, equivalenceGroups);
+        return null;
+      }
+      if (leader.sharesReceiverStages(sender)) {
+        // The leader is already sending to this stage. Given we don't have 
the ability to send to multiple
+        // receivers in the same stage, we cannot optimize this case right now.
+        _substitutions.add(new SpoolSubstitution(node.getStageId(), 
sender.getStageId(), leader.getStageId(),
+            SpoolSubstitution.Outcome.REJECTED_SAME_RECEIVER_STAGE, null));
+        // The original sender stays in the plan, so the stages below it can 
still be shared.
+        visitMailboxSend(sender, equivalenceGroups);
+        return null;
       }
+      node.setSender(leader);
+      leader.addReceiver(node);
+      if (_guarded) {
+        SpoolDeadlockGuard.DeadlockRisk risk = 
SpoolDeadlockGuard.findRisk(_root);

Review Comment:
   MINOR (perf): under `auto`, the guard re-runs `findRisk` over the whole plan 
after every accepted substitution, so this is roughly O(substitutions x 
plan-size), and `findRisk` itself builds an O(deferrals^2) matrix where the 
deferral count grows with the cartesian product of receives per join/union. For 
plans with many equivalent stages that could add noticeable planning latency. 
It's gated behind spools being present and only on `auto`, so probably fine in 
practice — but worth confirming on a worst-case plan (many shared stages 
feeding several joins) before this becomes the default.



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