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

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


The following commit(s) were added to refs/heads/master by this push:
     new 1b552624f52 Add a broker hook to prepare each MSE stage before 
dispatch (#20104)
1b552624f52 is described below

commit 1b552624f52a96926c67d340d9c27a8de390a4ef
Author: Yash Mayya <[email protected]>
AuthorDate: Fri Oct 30 12:59:39 2026 -0700

    Add a broker hook to prepare each MSE stage before dispatch (#20104)
---
 .../MultiStageBrokerRequestHandler.java            | 15 +++++
 .../MultiStageBrokerRequestHandlerTest.java        | 71 ++++++++++++++++++++++
 2 files changed, 86 insertions(+)

diff --git 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
index c68506ca14e..1d1f9fd9809 100644
--- 
a/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
+++ 
b/pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandler.java
@@ -660,6 +660,16 @@ public class MultiStageBrokerRequestHandler extends 
BaseBrokerRequestHandler {
       RequestContext requestContext, HttpHeaders httpHeaders) {
   }
 
+  /// Extension hook for preparing a stage right before the broker sends it to 
its workers, to run the query or to
+  /// explain it on the servers. To run the query, the broker calls it for 
every stage, including stage 0 (the broker
+  /// reduce). To explain the query on the servers, it calls it only for the 
leaf stages that have segments.
+  ///
+  /// Subclasses can add entries to the custom properties of the stage
+  /// ([DispatchablePlanFragment#getCustomProperties()]), which its workers 
get in their stage metadata. They must not
+  /// replace the existing entries, such as the table name and the time 
boundary that servers use for leaf stages.
+  protected void prepareStageForDispatch(DispatchablePlanFragment stage, 
Map<String, String> queryOptions) {
+  }
+
   /// Estimates the total number of server query threads the given plan will 
consume; used to acquire permits from the
   /// multi-stage query throttler before dispatch. Subclasses can override to 
accurately
   /// estimate the real per-server work.
@@ -902,6 +912,10 @@ public class MultiStageBrokerRequestHandler extends 
BaseBrokerRequestHandler {
       return new BrokerResponseNative(QueryErrorCode.TOO_MANY_REQUESTS, 
errorMessage);
     }
 
+    for (DispatchablePlanFragment stage : 
dispatchableSubPlan.getQueryStageMap().values()) {
+      prepareStageForDispatch(stage, query.getOptions());
+    }
+
     // Short-circuit: if all leaf stages are empty (all segments pruned or 
table has no data),
     // run only the broker reduce stage locally. No server dispatch is 
attempted.
     boolean allLeafStagesEmpty = dispatchableSubPlan.isAllLeafStagesEmpty();
@@ -1151,6 +1165,7 @@ public class MultiStageBrokerRequestHandler extends 
BaseBrokerRequestHandler {
   private Collection<PlanNode> requestPhysicalPlan(DispatchablePlanFragment 
fragment,
       RequestContext requestContext, long queryTimeoutMs, Map<String, String> 
queryOptions) {
     List<PlanNode> stagePlans;
+    prepareStageForDispatch(fragment, queryOptions);
     try {
       stagePlans = _queryDispatcher.explain(requestContext, fragment, 
queryTimeoutMs, queryOptions);
     } catch (Exception e) {
diff --git 
a/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
 
b/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
index 149228f41d5..b89ae2fc01b 100644
--- 
a/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
+++ 
b/pinot-broker/src/test/java/org/apache/pinot/broker/requesthandler/MultiStageBrokerRequestHandlerTest.java
@@ -37,6 +37,7 @@ import org.apache.pinot.common.metrics.BrokerMetrics;
 import org.apache.pinot.common.response.BrokerResponse;
 import org.apache.pinot.common.response.broker.BrokerResponseNativeV2;
 import org.apache.pinot.common.response.broker.ResultTable;
+import org.apache.pinot.core.routing.MockRoutingManagerFactory;
 import org.apache.pinot.core.routing.MultiClusterRoutingContext;
 import org.apache.pinot.core.routing.RoutingManager;
 import org.apache.pinot.core.transport.ServerInstance;
@@ -58,6 +59,7 @@ import org.apache.pinot.spi.accounting.ThreadAccountantUtils;
 import org.apache.pinot.spi.auth.broker.RequesterIdentity;
 import org.apache.pinot.spi.env.PinotConfiguration;
 import 
org.apache.pinot.spi.eventlistener.query.BrokerQueryEventListenerFactory;
+import org.apache.pinot.spi.exception.QueryErrorCode;
 import org.apache.pinot.spi.query.QueryThreadContext;
 import org.apache.pinot.spi.trace.QueryFingerprint;
 import org.apache.pinot.spi.trace.RequestContext;
@@ -72,13 +74,16 @@ import org.apache.pinot.spi.utils.NetUtils;
 import org.apache.pinot.sql.parsers.CalciteSqlParser;
 import org.apache.pinot.sql.parsers.SqlNodeAndOptions;
 import org.apache.pinot.util.TestUtils;
+import org.mockito.ArgumentCaptor;
 import org.mockito.MockedConstruction;
 import org.mockito.Mockito;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockConstruction;
@@ -153,6 +158,72 @@ public class MultiStageBrokerRequestHandlerTest extends 
QueryEnvironmentTestBase
         "onQueryCompletion hook must be called with the BrokerResponse from 
handleRequest for MSE");
   }
 
+  /// The broker calls 
[MultiStageBrokerRequestHandler#prepareStageForDispatch] for each stage before 
it sends the stage
+  /// to its workers, both to run the query and to explain it on the servers.
+  @Test
+  public void testPrepareStageForDispatchHook()
+      throws Exception {
+    int port1 = NetUtils.findOpenPort();
+    int port2 = NetUtils.findOpenPort();
+    MockRoutingManagerFactory factory = new MockRoutingManagerFactory(port1, 
port2);
+    TABLE_SCHEMAS.forEach((tableName, schema) -> factory.registerTable(schema, 
tableName));
+    SERVER1_SEGMENTS.forEach(
+        (tableName, segments) -> segments.forEach(segment -> 
factory.registerSegment(port1, tableName, segment)));
+    SERVER2_SEGMENTS.forEach(
+        (tableName, segments) -> segments.forEach(segment -> 
factory.registerSegment(port2, tableName, segment)));
+    RoutingManager routingManager = factory.buildRoutingManager(null);
+    WorkerManager workerManager =
+        new WorkerManager("Broker_localhost", "localhost", 
NetUtils.findOpenPort(), routingManager);
+    PinotConfiguration config = new PinotConfiguration();
+    config.setProperty(MultiStageQueryRunner.KEY_OF_QUERY_RUNNER_HOSTNAME, 
"localhost");
+    config.setProperty(MultiStageQueryRunner.KEY_OF_QUERY_RUNNER_PORT, 
Integer.toString(NetUtils.findOpenPort()));
+    BrokerQueryEventListenerFactory.init(config);
+    QueryQuotaManager queryQuotaManager = mock(QueryQuotaManager.class);
+    when(queryQuotaManager.acquire(anyString())).thenReturn(true);
+    when(queryQuotaManager.acquireDatabase(anyString())).thenReturn(true);
+    when(queryQuotaManager.acquireApplication(anyString())).thenReturn(true);
+    AtomicReference<DispatchableSubPlan> throttledPlan = new 
AtomicReference<>();
+    try (MockedConstruction<QueryDispatcher> dispatchers = 
mockConstruction(QueryDispatcher.class)) {
+      MultiStageBrokerRequestHandler handler = new 
MultiStageBrokerRequestHandler(config, "testBrokerId",
+          new BrokerRequestIdGenerator(), routingManager, new 
AllowAllAccessControlFactory(), queryQuotaManager,
+          factory.buildTableCache(), mock(MultiStageQueryThrottler.class), 
mock(FailureDetector.class),
+          ThreadAccountantUtils.getNoOpAccountant(), null, workerManager, 
workerManager) {
+        @Override
+        protected void prepareStageForDispatch(DispatchablePlanFragment stage, 
Map<String, String> queryOptions) {
+          stage.getCustomProperties().put("testKey", "stage" + 
stage.getPlanFragment().getFragmentId());
+        }
+
+        @Override
+        protected int getEstimatedNumQueryThreads(DispatchableSubPlan 
dispatchableSubPlan) {
+          throttledPlan.set(dispatchableSubPlan);
+          return super.getEstimatedNumQueryThreads(dispatchableSubPlan);
+        }
+      };
+      try {
+        String query = "SELECT col1, COUNT(*) FROM a GROUP BY col1";
+
+        // The mocked throttler stops the query before it runs
+        BrokerResponse response = handler.handleRequest(query);
+        assertEquals(response.getExceptions().get(0).getErrorCode(), 
QueryErrorCode.EXECUTION_TIMEOUT.getId());
+        Map<Integer, DispatchablePlanFragment> stages = 
throttledPlan.get().getQueryStageMap();
+        assertEquals(stages.size(), 3);
+        stages.forEach(
+            (stageId, stage) -> 
assertEquals(stage.getCustomProperties().get("testKey"), "stage" + stageId));
+
+        handler.handleRequest("SET " + QueryOptionKey.EXPLAIN_ASKING_SERVERS + 
" = true; EXPLAIN PLAN FOR " + query);
+        ArgumentCaptor<DispatchablePlanFragment> explainedStages =
+            ArgumentCaptor.forClass(DispatchablePlanFragment.class);
+        verify(dispatchers.constructed().get(0), atLeastOnce())
+            .explain(any(), explainedStages.capture(), anyLong(), any());
+        for (DispatchablePlanFragment stage : explainedStages.getAllValues()) {
+          assertEquals(stage.getCustomProperties().get("testKey"), "stage" + 
stage.getPlanFragment().getFragmentId());
+        }
+      } finally {
+        handler.shutDown();
+      }
+    }
+  }
+
   @Test
   public void 
testApplyBrokerDefaultQueryOptionsInjectsStreamingGroupByFlushThreshold()
       throws Exception {


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

Reply via email to