yashmayya commented on code in PR #19874:
URL: https://github.com/apache/pinot/pull/19874#discussion_r4286627032


##########
pinot-query-planner/src/main/java/org/apache/pinot/query/routing/WorkerManager.java:
##########
@@ -455,6 +440,68 @@ private void assignWorkersToNonRootFragment(PlanFragment 
fragment, DispatchableP
     }
   }
 
+  /// Returns whether the leaf pass assigns the workers of the given fragment. 
A lookup join fragment scans the
+  /// dimension table, so it is a leaf fragment. But it takes its workers from 
its input, so when the input is assigned
+  /// in the intermediate pass (e.g. an aggregation below the join), the 
lookup join fragment is assigned in that pass
+  /// too.
+  private static boolean isAssignedInLeafPass(PlanFragment fragment,
+      Map<Integer, DispatchablePlanMetadata> metadataMap) {
+    if (!isLeafPlan(metadataMap.get(fragment.getFragmentId()))) {
+      return false;
+    }
+    List<PlanFragment> children = fragment.getChildren();
+    return !isLookupJoin(children) || isAssignedInLeafPass(children.get(0), 
metadataMap);
+  }
+
+  /// Assigns the workers of a lookup join fragment from the workers of its 
input, and fakes an empty segments map so
+  /// that the workers are identified as leaf stage workers. The join probes 
the dimension table in memory, so it must
+  /// run on servers, which hold the dimension table. A leaf input runs on 
servers, so the join takes the same workers.
+  /// An intermediate input can run on instances that cannot scan segments (see
+  /// [#getCandidateServersForReplicatedLeaf]), so then the instances come 
from that hook.
+  ///
+  /// @param isInputLeaf whether the input of the join is assigned in the leaf 
pass
+  private void assignWorkersToLookupJoinFragment(PlanFragment fragment, 
DispatchablePlanContext context,
+      boolean isInputLeaf) {
+    Map<Integer, DispatchablePlanMetadata> metadataMap = 
context.getDispatchablePlanMetadataMap();
+    DispatchablePlanMetadata metadata = 
metadataMap.get(fragment.getFragmentId());
+    DispatchablePlanMetadata childMetadata = 
metadataMap.get(fragment.getChildren().get(0).getFragmentId());
+    Map<Integer, QueryServerInstance> workerIdToServerInstanceMap = 
assignWorkersForLocalExchange(childMetadata);
+    boolean inheritPartitioning = true;
+    if (!isInputLeaf) {
+      int numInputWorkers = workerIdToServerInstanceMap.size();
+      List<QueryServerInstance> inputWorkers = new 
ArrayList<>(numInputWorkers);
+      for (int workerId = 0; workerId < numInputWorkers; workerId++) {
+        inputWorkers.add(workerIdToServerInstanceMap.get(workerId));
+      }
+      List<QueryServerInstance> workers = 
getCandidateServersForReplicatedLeaf(context, inputWorkers);
+      if (!workers.equals(inputWorkers)) {

Review Comment:
   Good point. Added a note to the hook doc in 18ab7ac58e: return the input 
workers unchanged, in the same order, to keep the exchange local and the input 
partitioning.



##########
pinot-query-planner/src/main/java/org/apache/pinot/query/routing/WorkerManager.java:
##########
@@ -455,6 +440,68 @@ private void assignWorkersToNonRootFragment(PlanFragment 
fragment, DispatchableP
     }
   }
 
+  /// Returns whether the leaf pass assigns the workers of the given fragment. 
A lookup join fragment scans the
+  /// dimension table, so it is a leaf fragment. But it takes its workers from 
its input, so when the input is assigned
+  /// in the intermediate pass (e.g. an aggregation below the join), the 
lookup join fragment is assigned in that pass
+  /// too.
+  private static boolean isAssignedInLeafPass(PlanFragment fragment,
+      Map<Integer, DispatchablePlanMetadata> metadataMap) {
+    if (!isLeafPlan(metadataMap.get(fragment.getFragmentId()))) {
+      return false;
+    }
+    List<PlanFragment> children = fragment.getChildren();
+    return !isLookupJoin(children) || isAssignedInLeafPass(children.get(0), 
metadataMap);
+  }
+
+  /// Assigns the workers of a lookup join fragment from the workers of its 
input, and fakes an empty segments map so
+  /// that the workers are identified as leaf stage workers. The join probes 
the dimension table in memory, so it must
+  /// run on servers, which hold the dimension table. A leaf input runs on 
servers, so the join takes the same workers.
+  /// An intermediate input can run on instances that cannot scan segments (see
+  /// [#getCandidateServersForReplicatedLeaf]), so then the instances come 
from that hook.
+  ///
+  /// @param isInputLeaf whether the input of the join is assigned in the leaf 
pass
+  private void assignWorkersToLookupJoinFragment(PlanFragment fragment, 
DispatchablePlanContext context,
+      boolean isInputLeaf) {
+    Map<Integer, DispatchablePlanMetadata> metadataMap = 
context.getDispatchablePlanMetadataMap();
+    DispatchablePlanMetadata metadata = 
metadataMap.get(fragment.getFragmentId());
+    DispatchablePlanMetadata childMetadata = 
metadataMap.get(fragment.getChildren().get(0).getFragmentId());
+    Map<Integer, QueryServerInstance> workerIdToServerInstanceMap = 
assignWorkersForLocalExchange(childMetadata);
+    boolean inheritPartitioning = true;
+    if (!isInputLeaf) {
+      int numInputWorkers = workerIdToServerInstanceMap.size();
+      List<QueryServerInstance> inputWorkers = new 
ArrayList<>(numInputWorkers);
+      for (int workerId = 0; workerId < numInputWorkers; workerId++) {
+        inputWorkers.add(workerIdToServerInstanceMap.get(workerId));
+      }
+      List<QueryServerInstance> workers = 
getCandidateServersForReplicatedLeaf(context, inputWorkers);
+      if (!workers.equals(inputWorkers)) {
+        // The workers differ from the input's, so the exchange redistributes 
the rows and the worker ids no longer
+        // stand for the input's partition classes.
+        workerIdToServerInstanceMap = 
Maps.newHashMapWithExpectedSize(workers.size());
+        for (int workerId = 0; workerId < workers.size(); workerId++) {
+          workerIdToServerInstanceMap.put(workerId, workers.get(workerId));
+        }
+        inheritPartitioning = false;
+      }
+    }
+    metadata.setWorkerIdToServerInstanceMap(workerIdToServerInstanceMap);
+    if (inheritPartitioning) {
+      metadata.setPartitionFunction(childMetadata.getPartitionFunction());
+      // The worker map comes from the child, so the worker ids stand for the 
same partition classes as the child's.
+      metadata.setPartitionClassIds(childMetadata.getPartitionClassIds());
+    } else {
+      metadata.setPartitionFunction(DEFAULT_SHUFFLE_PARTITION_FUNCTION);
+    }
+    // Fake a segments map so that the worker can be correctly identified as 
leaf stage
+    Map<String, List<String>> segmentsMap = Map.of(TableType.OFFLINE.name(), 
List.of());

Review Comment:
   Added to the comment in 18ab7ac58e: the `OFFLINE` key with no segments is 
only a placeholder, because the join reads the dimension table from memory.



##########
pinot-query-planner/src/test/java/org/apache/pinot/query/routing/WorkerManagerTest.java:
##########
@@ -2298,6 +2300,118 @@ ColocatedTableSpec unavailableSegments(List<String> 
unavailableSegments) {
     }
   }
 
+  private static final String LOOKUP_JOINS_OVER_AGGREGATE_QUERY =
+      "SELECT /*+ joinOptions(join_strategy='lookup') */ t.col1, t.cnt, 
dim1.col2, dim2.col2 "
+          + "FROM (SELECT col1, COUNT(*) AS cnt FROM fact GROUP BY col1) t "
+          + "JOIN dim1 ON t.col1 = dim1.col1 JOIN dim2 ON t.col1 = dim2.col1";
+
+  /// A lookup join over an intermediate input (the aggregation of a subquery) 
is assigned after its input. By default
+  /// it takes the workers of its input, like a lookup join over a leaf input. 
The second lookup join takes the workers
+  /// of the first one, so it must be assigned after the first one too.
+  @Test
+  public void testLookupJoinOverIntermediateInputTakesInputWorkers() {
+    DispatchableSubPlan plan = 
newLookupJoinQueryEnvironment(null).planQuery(LOOKUP_JOINS_OVER_AGGREGATE_QUERY);
+    DispatchablePlanFragment dim1Join = lookupJoinFragment(plan, "dim1");
+    DispatchablePlanFragment dim2Join = lookupJoinFragment(plan, "dim2");
+    Map<Integer, String> aggregateWorkers = 
workerIdToServer(inputFragment(plan, dim1Join));
+    assertEquals(aggregateWorkers.size(), 2);
+    assertEquals(workerIdToServer(dim1Join), aggregateWorkers);
+    assertEquals(workerIdToServer(dim2Join), aggregateWorkers);
+  }
+
+  /// When the intermediate stages run on an instance that cannot scan 
segments, like a broker, a lookup join over an
+  /// intermediate input runs on the servers from 
[WorkerManager#getCandidateServersForReplicatedLeaf]. Only servers
+  /// hold the dimension tables.
+  @Test
+  public void testLookupJoinOverIntermediateInputUsesReplicatedLeafServers() {
+    QueryServerInstance broker = new QueryServerInstance("Broker_localhost_9", 
"localhost", 9, 9);
+    DispatchableSubPlan plan = 
newLookupJoinQueryEnvironment(broker).planQuery(LOOKUP_JOINS_OVER_AGGREGATE_QUERY);
+    DispatchablePlanFragment dim1Join = lookupJoinFragment(plan, "dim1");
+    DispatchablePlanFragment dim2Join = lookupJoinFragment(plan, "dim2");
+    assertEquals(workerIdToServer(inputFragment(plan, dim1Join)), Map.of(0, 
broker.getInstanceId()));
+    Set<String> factServers = new 
HashSet<>(workerIdToServer(leafFragmentForTable(plan, "fact")).values());
+    assertEquals(factServers.size(), 2);
+    assertEquals(new HashSet<>(workerIdToServer(dim1Join).values()), 
factServers);
+    assertEquals(workerIdToServer(dim2Join), workerIdToServer(dim1Join));

Review Comment:
   I would rather leave this out of this PR. The partition function and class 
ids live only in `DispatchablePlanMetadata` during planning, and 
`DispatchableSubPlan` does not expose them, so these asserts need new test 
plumbing. Partial coverage exists: in the inherit case the exchange is direct, 
so `checkPartitionClassAgreement` in `MailboxAssignmentVisitor` fails planning 
if the copied class ids disagree with the input. The redistribution branch sets 
the same values as `assignWorkersToIntermediateFragment` does for a stage that 
does not inherit partitioning.



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