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 637b6b3babd Add broker pruning support to partitioned leaf path and
logical tables in MSE (#18924)
637b6b3babd is described below
commit 637b6b3babd59b1c9ccf46e40b656ca53e0f7039
Author: Yash Mayya <[email protected]>
AuthorDate: Wed Jul 8 13:01:07 2026 -0700
Add broker pruning support to partitioned leaf path and logical tables in
MSE (#18924)
---
...PartitionLLCRealtimeClusterIntegrationTest.java | 31 ++
.../BaseLogicalTableIntegrationTest.java | 29 ++
.../apache/pinot/query/routing/WorkerManager.java | 242 ++++++++--
.../pinot/query/routing/WorkerManagerTest.java | 527 +++++++++++++++++++++
4 files changed, 785 insertions(+), 44 deletions(-)
diff --git
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/SegmentPartitionLLCRealtimeClusterIntegrationTest.java
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/SegmentPartitionLLCRealtimeClusterIntegrationTest.java
index 201fd136e09..75e1ad0e166 100644
---
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/SegmentPartitionLLCRealtimeClusterIntegrationTest.java
+++
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/SegmentPartitionLLCRealtimeClusterIntegrationTest.java
@@ -207,6 +207,37 @@ public class
SegmentPartitionLLCRealtimeClusterIntegrationTest extends BaseClust
}
}
+ @Test(dependsOnMethods = "testPartitionRouting")
+ public void testMultiStageBrokerPruningOnPartitionedTable()
+ throws Exception {
+ setUseMultiStageQueryEngine(true);
+ try {
+ // 'CA' hashes to partition 0, so only that partition's segments should
be routed when broker pruning is enabled.
+ String filteredQuery = "SELECT COUNT(*) FROM mytable WHERE DestState =
'CA'";
+
+ // Baseline: the multi-stage engine without broker pruning scans all
segments across both partitions.
+ JsonNode unpruned = postQuery(filteredQuery);
+ assertTrue(unpruned.get("exceptions").isEmpty(), "Unexpected exceptions
without broker pruning");
+ int unprunedSegmentsQueried =
unpruned.get(MetadataKey.NUM_SEGMENTS_QUERIED.getName()).asInt();
+
+ // With broker pruning enabled, the non-matching partition's segments
are pruned at the broker before dispatch.
+ JsonNode pruned = postQuery("SET useBrokerPruning=true; " +
filteredQuery);
+ assertTrue(pruned.get("exceptions").isEmpty(), "Unexpected exceptions
with broker pruning");
+
+ // Broker pruning is a routing optimization only; the result must be
identical to the unpruned run.
+
assertEquals(pruned.get("resultTable").get("rows").get(0).get(0).asLong(),
+ unpruned.get("resultTable").get("rows").get(0).get(0).asLong(),
+ "Broker pruning changed the query result");
+ // Broker pruning actually engaged: the non-matching partition's
segments were dropped, so fewer were queried.
+ assertTrue(pruned.get("numSegmentsPrunedByBroker").asInt() > 0,
+ "Expected the broker to prune the non-matching partition's
segments");
+
assertTrue(pruned.get(MetadataKey.NUM_SEGMENTS_QUERIED.getName()).asInt() <
unprunedSegmentsQueried,
+ "Expected fewer segments queried with broker pruning");
+ } finally {
+ setUseMultiStageQueryEngine(false);
+ }
+ }
+
@Test(dependsOnMethods = "testPartitionRouting")
public void testPartitionIdVirtualColumn()
throws Exception {
diff --git
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/logicaltable/BaseLogicalTableIntegrationTest.java
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/logicaltable/BaseLogicalTableIntegrationTest.java
index cefa8f2c7f9..db52283c71d 100644
---
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/logicaltable/BaseLogicalTableIntegrationTest.java
+++
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/logicaltable/BaseLogicalTableIntegrationTest.java
@@ -453,6 +453,35 @@ public abstract class BaseLogicalTableIntegrationTest
extends BaseClusterIntegra
super.testGeneratedQueries(true, useMultiStageQueryEngine);
}
+ /**
+ * End-to-end smoke test that enabling broker segment pruning on the MSE
logical-planner path executes cleanly for a
+ * logical table and does not change results. It exercises the logical-table
routing path where the leaf-stage filter
+ * is forwarded (via {@code
WorkerManager.buildLogicalTableRoutingBrokerRequest}) into each physical
table's routing
+ * request so the {@code LogicalTableRouteProvider} runs segment pruners
over a filter-bearing request rather than a
+ * bare {@code SELECT *}. A malformed forwarded request would surface here
as an exception or a changed result.
+ *
+ * <p>Note: these physical tables carry no segment-pruner config, so this
asserts correctness, not that a segment was
+ * actually pruned; the exact pruning behavior is unit-tested in {@code
WorkerManagerTest}.
+ */
+ @Test(dataProvider = "useBothQueryEngines")
+ public void testBrokerPruningPreservesLogicalTableResults(boolean
useMultiStageQueryEngine)
+ throws Exception {
+ setUseMultiStageQueryEngine(useMultiStageQueryEngine);
+ String logicalTableName = getLogicalTableName();
+ String filteredQuery = "SELECT COUNT(*) FROM " + logicalTableName + "
WHERE DaysSinceEpoch > 16312";
+
+ JsonNode withoutPruning = postQuery(filteredQuery);
+ assertTrue(withoutPruning.get("exceptions").isEmpty(), "Unexpected
exceptions without broker pruning");
+
+ JsonNode withPruning = postQuery("SET useBrokerPruning=true; " +
filteredQuery);
+ assertTrue(withPruning.get("exceptions").isEmpty(), "Unexpected exceptions
with broker pruning");
+
+ // Broker pruning is a routing optimization only; the result must be
identical to the unpruned run.
+
assertEquals(withPruning.get("resultTable").get("rows").get(0).get(0).asLong(),
+ withoutPruning.get("resultTable").get("rows").get(0).get(0).asLong(),
+ "Broker pruning changed the result of a logical table query");
+ }
+
@Test
public void testDisableGroovyQueryTableConfigOverride()
throws Exception {
diff --git
a/pinot-query-planner/src/main/java/org/apache/pinot/query/routing/WorkerManager.java
b/pinot-query-planner/src/main/java/org/apache/pinot/query/routing/WorkerManager.java
index 973078771c3..a0edc857847 100644
---
a/pinot-query-planner/src/main/java/org/apache/pinot/query/routing/WorkerManager.java
+++
b/pinot-query-planner/src/main/java/org/apache/pinot/query/routing/WorkerManager.java
@@ -18,6 +18,7 @@
*/
package org.apache.pinot.query.routing;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import java.util.ArrayList;
@@ -492,14 +493,20 @@ public class WorkerManager {
String partitionKey =
tableOptions.get(PinotHintOptions.TableHintOptions.PARTITION_KEY);
if (partitionKey != null) {
- assignWorkersToPartitionedLeafFragment(metadata, context,
partitionKey, tableOptions);
+ // Broker pruning: build a filter-bearing routing query (null when
disabled/unsupported) so the partitioned
+ // assignment can drop partitions with no matching segments. Reuses
the same gate as the non-partitioned path.
+ // Skip pre-partitioned leaves up front: pruning is disabled for them
(see computePartitionsToKeep), so don't
+ // spend planning time building the routing query, e.g. for
colocated-join leaves.
+ PinotQuery routingPinotQuery = metadata.isPrePartitioned() ? null
+ : extractRoutingQuery(fragment.getFragmentRoot(),
metadata.getScannedTables().get(0), context);
+ assignWorkersToPartitionedLeafFragment(metadata, context,
partitionKey, tableOptions, routingPinotQuery);
updateContextForLeafStage(metadata, context);
return;
}
}
if (metadata.getLogicalTableRouteInfo() != null) {
- assignWorkersToNonPartitionedLeafFragmentForLogicalTable(metadata,
context);
+ assignWorkersToNonPartitionedLeafFragmentForLogicalTable(fragment,
metadata, context);
} else {
assignWorkersToNonPartitionedLeafFragment(fragment, metadata, context);
}
@@ -780,8 +787,8 @@ public class WorkerManager {
CalciteSqlCompiler.compileToBrokerRequest("SELECT * FROM \"" +
tableNameWithType + "\""), samplerName);
}
- private void
assignWorkersToNonPartitionedLeafFragmentForLogicalTable(DispatchablePlanMetadata
metadata,
- DispatchablePlanContext context) {
+ private void
assignWorkersToNonPartitionedLeafFragmentForLogicalTable(PlanFragment fragment,
+ DispatchablePlanMetadata metadata, DispatchablePlanContext context) {
LogicalTableRouteInfo logicalTableRouteInfo =
metadata.getLogicalTableRouteInfo();
Preconditions.checkNotNull(logicalTableRouteInfo);
LogicalTableRouteProvider tableRouteProvider = new
LogicalTableRouteProvider();
@@ -789,32 +796,57 @@ public class WorkerManager {
if (logicalTableRouteInfo.getTimeBoundaryInfo() != null) {
metadata.setTimeBoundaryInfo(logicalTableRouteInfo.getTimeBoundaryInfo());
}
- BrokerRequest offlineBrokerRequest = null;
- BrokerRequest realtimeBrokerRequest = null;
Map<String, String> queryOptions =
context.getPlannerContext().getOptions();
- if (logicalTableRouteInfo.hasOffline()) {
- offlineBrokerRequest = CalciteSqlCompiler.compileToBrokerRequest(
- "SELECT * FROM \"" + logicalTableRouteInfo.getOfflineTableName() +
"\"");
- if (MapUtils.isNotEmpty(queryOptions) &&
offlineBrokerRequest.isSetPinotQuery()) {
- offlineBrokerRequest.getPinotQuery().setQueryOptions(new
HashMap<>(queryOptions));
- }
- }
+ // Broker pruning: build a filter-bearing routing query (null when
disabled/unsupported). When non-null, each
+ // physical table's route below carries the filter so segment pruners can
eliminate segments; when null, fall back
+ // to an unfiltered SELECT * per table type. The physical route resolution
ignores the data source table name (it
+ // uses the physical table names), so we set the typed logical name here
purely to build a valid broker request.
+ String rawTableName = TableNameBuilder.extractRawTableName(
+ logicalTableRouteInfo.hasOffline() ?
logicalTableRouteInfo.getOfflineTableName()
+ : logicalTableRouteInfo.getRealtimeTableName());
+ PinotQuery routingPinotQuery =
extractRoutingQuery(fragment.getFragmentRoot(), rawTableName, context);
- if (logicalTableRouteInfo.hasRealtime()) {
- realtimeBrokerRequest = CalciteSqlCompiler.compileToBrokerRequest(
- "SELECT * FROM \"" + logicalTableRouteInfo.getRealtimeTableName() +
"\"");
- if (MapUtils.isNotEmpty(queryOptions) &&
realtimeBrokerRequest.isSetPinotQuery()) {
- realtimeBrokerRequest.getPinotQuery().setQueryOptions(new
HashMap<>(queryOptions));
- }
- }
+ BrokerRequest offlineBrokerRequest = logicalTableRouteInfo.hasOffline() ?
buildLogicalTableRoutingBrokerRequest(
+ logicalTableRouteInfo.getOfflineTableName(), routingPinotQuery,
queryOptions) : null;
+ BrokerRequest realtimeBrokerRequest = logicalTableRouteInfo.hasRealtime()
? buildLogicalTableRoutingBrokerRequest(
+ logicalTableRouteInfo.getRealtimeTableName(), routingPinotQuery,
queryOptions) : null;
tableRouteProvider.calculateRoutes(logicalTableRouteInfo, _routingManager,
offlineBrokerRequest,
realtimeBrokerRequest, context.getRequestId());
+ if (routingPinotQuery != null) {
+
context.addNumSegmentsPrunedByBroker(logicalTableRouteInfo.getNumPrunedSegmentsTotal());
+ }
assignTableSegmentsToWorkers(logicalTableRouteInfo, metadata);
}
+ /**
+ * Builds the routing {@link BrokerRequest} for one physical table type of a
logical table. When {@code
+ * routingPinotQuery} is non-null it carries the leaf-stage filter so
segment pruners can eliminate segments;
+ * otherwise a bare {@code SELECT *} is used (no pruning). The data source
is set to {@code logicalTableNameWithType}
+ * (the typed logical table name; physical table names are resolved later by
the route provider).
+ *
+ * <p>The given {@code routingPinotQuery} is not modified: it is deep-copied
before the table name is rewritten, so
+ * the same instance can be reused to build both the offline and realtime
requests.
+ */
+ @VisibleForTesting
+ static BrokerRequest buildLogicalTableRoutingBrokerRequest(String
logicalTableNameWithType,
+ @Nullable PinotQuery routingPinotQuery, Map<String, String>
queryOptions) {
+ BrokerRequest brokerRequest;
+ if (routingPinotQuery != null) {
+ PinotQuery pinotQuery = routingPinotQuery.deepCopy();
+ pinotQuery.getDataSource().setTableName(logicalTableNameWithType);
+ brokerRequest = CalciteSqlCompiler.convertToBrokerRequest(pinotQuery);
+ } else {
+ brokerRequest = CalciteSqlCompiler.compileToBrokerRequest("SELECT * FROM
\"" + logicalTableNameWithType + "\"");
+ }
+ if (MapUtils.isNotEmpty(queryOptions) && brokerRequest.isSetPinotQuery()) {
+ brokerRequest.getPinotQuery().setQueryOptions(new
HashMap<>(queryOptions));
+ }
+ return brokerRequest;
+ }
+
private static void assignTableSegmentsToWorkers(LogicalTableRouteInfo
logicalTableRouteInfo,
DispatchablePlanMetadata metadata) {
Map<ServerInstance, Map<String, List<String>>>
serverInstanceToLogicalSegmentsMap =
@@ -884,7 +916,8 @@ public class WorkerManager {
// Partitioned leaf stage assignment
// --------------------------------------------------------------------------
private void assignWorkersToPartitionedLeafFragment(DispatchablePlanMetadata
metadata,
- DispatchablePlanContext context, String partitionKey, Map<String,
String> tableOptions) {
+ DispatchablePlanContext context, String partitionKey, Map<String,
String> tableOptions,
+ @Nullable PinotQuery routingPinotQuery) {
// when partition key exist, we assign workers for leaf-stage in
partitioned fashion.
String numPartitionsStr =
tableOptions.get(PinotHintOptions.TableHintOptions.PARTITION_SIZE);
@@ -907,41 +940,152 @@ public class WorkerManager {
int numPartitions = partitionInfoMap.length;
assert numPartitions % numWorkers == 0;
int numPartitionsPerWorker = numPartitions / numWorkers;
- Map<Integer, QueryServerInstance> workedIdToServerInstanceMap = new
HashMap<>();
+
+ // Broker pruning: the partitions to keep (null means keep all).
Partitions absent from the set are skipped below.
+ Set<Integer> partitionsToKeep =
+ computePartitionsToKeep(routingPinotQuery, metadata,
context.getRequestId(), partitionInfoMap);
+ if (partitionsToKeep != null) {
+ long numSegmentsPrunedByBroker = countPrunedSegments(partitionInfoMap,
partitionsToKeep);
+ if (numSegmentsPrunedByBroker > 0) {
+ context.addNumSegmentsPrunedByBroker(numSegmentsPrunedByBroker);
+ }
+ }
+
+ Map<Integer, QueryServerInstance> workerIdToServerInstanceMap = new
HashMap<>();
Map<Integer, Map<String, List<String>>> workerIdToSegmentsMap = new
HashMap<>();
if (numPartitionsPerWorker == 1) {
- assignOnePartitionPerWorker(tableName, context.getRequestId(),
partitionInfoMap,
- _routingManager.getEnabledServerInstanceMap(),
workedIdToServerInstanceMap, workerIdToSegmentsMap);
+ assignOnePartitionPerWorker(tableName, context.getRequestId(),
partitionInfoMap, partitionsToKeep,
+ _routingManager.getEnabledServerInstanceMap(),
workerIdToServerInstanceMap, workerIdToSegmentsMap);
} else {
assignMultiplePartitionsPerWorker(tableName, context.getRequestId(),
numPartitionsPerWorker, partitionInfoMap,
- _routingManager.getEnabledServerInstanceMap(),
workedIdToServerInstanceMap, workerIdToSegmentsMap);
+ partitionsToKeep, _routingManager.getEnabledServerInstanceMap(),
workerIdToServerInstanceMap,
+ workerIdToSegmentsMap);
}
- metadata.setWorkerIdToServerInstanceMap(workedIdToServerInstanceMap);
+ metadata.setWorkerIdToServerInstanceMap(workerIdToServerInstanceMap);
metadata.setWorkerIdToSegmentsMap(workerIdToSegmentsMap);
metadata.setTimeBoundaryInfo(partitionTableInfo._timeBoundaryInfo);
metadata.setPartitionFunction(partitionFunction);
}
- /// Pick one worker per partition for partitioned leaf stage.
+ /**
+ * Broker pruning for the partitioned leaf path. Returns the set of
partition ids that still have at least one segment
+ * matching the query filter, or {@code null} to keep all partitions.
+ *
+ * <p>Returns {@code null} (no pruning) when any of the following hold:
+ * <ul>
+ * <li>broker pruning is disabled or the leaf shape is unsupported (the
routing query is {@code null}), or there is
+ * no filter to prune with;</li>
+ * <li>the leaf feeds a pre-partitioned (1-to-1 direct) exchange --
dropping/compacting workers would misalign
+ * sender/receiver worker ids in {@code MailboxAssignmentVisitor}. A
non-pre-partitioned leaf is shuffled via
+ * {@code connectWorkers}, which re-hashes across any worker count, so
pruning is safe there;</li>
+ * <li>routing fails (pruning is best-effort);</li>
+ * <li>every partition would be pruned -- an empty worker map would break
exchanges in a multi-leaf plan (the
+ * all-leaves-empty short-circuit does not fire for a partially-empty
plan), and the server-side filter still
+ * yields the correct empty result unpruned.</li>
+ * </ul>
+ *
+ * <p>Partition survival is decided by routing the filter-bearing query
through the {@link RoutingManager} (the same
+ * mechanism the non-partitioned path uses), so the segment-level pruners
judge survival using each segment's own
+ * partition metadata. This is correct for every partition function and
configuration, unlike recomputing the
+ * partition id from the table-level function name (which lacks the
per-segment function config). A partition is
+ * dropped only when every one of its segments was pruned; a segment that
merely became unavailable keeps its
+ * partition alive so matching data is never silently dropped.
+ *
+ * <p>Note that pruning here is partition-level, not segment-level: a
surviving partition dispatches all of its
+ * segments, including ones the pruners eliminated (the server-side pruners
drop those again cheaply). This keeps
+ * surviving partitions' assignments identical to the unpruned path -- the
only behavioral delta is dropped
+ * workers -- at the cost of a lower pruning ceiling than the
non-partitioned path for partitions with mixed-match
+ * segments. Segment-level pruning within surviving partitions is a possible
follow-up.
+ */
+ @Nullable
+ private Set<Integer> computePartitionsToKeep(@Nullable PinotQuery
routingPinotQuery,
+ DispatchablePlanMetadata metadata, long requestId, PartitionInfo[]
partitionInfoMap) {
+ if (routingPinotQuery == null || routingPinotQuery.getFilterExpression()
== null || metadata.isPrePartitioned()) {
+ return null;
+ }
+ Map<String, RoutingTable> routingTableMap;
+ try {
+ routingTableMap = getRoutingTable(routingPinotQuery, requestId);
+ } catch (RuntimeException e) {
+ // Pruning is best-effort: never fail a query that would otherwise route
successfully unpruned.
+ LOGGER.warn("Broker pruning skipped for partitioned table {} due to
routing failure: {}",
+ routingPinotQuery.getDataSource().getTableName(), e.getMessage());
+ return null;
+ }
+ if (routingTableMap.isEmpty()) {
+ return null;
+ }
+ Set<String> matchedSegments = new HashSet<>();
+ for (RoutingTable routingTable : routingTableMap.values()) {
+ for (SegmentsToQuery segmentsToQuery :
routingTable.getServerInstanceToSegmentsMap().values()) {
+ matchedSegments.addAll(segmentsToQuery.getSegments());
+ }
+ // Keep a partition alive if any of its segments is merely unavailable
(rather than pruned) so we never drop data.
+ matchedSegments.addAll(routingTable.getUnavailableSegments());
+ }
+ Set<Integer> partitionsToKeep = new HashSet<>();
+ for (int i = 0; i < partitionInfoMap.length; i++) {
+ PartitionInfo partitionInfo = partitionInfoMap[i];
+ if (partitionInfo != null &&
(containsAny(partitionInfo._offlineSegments, matchedSegments) || containsAny(
+ partitionInfo._realtimeSegments, matchedSegments))) {
+ partitionsToKeep.add(i);
+ }
+ }
+ // If everything would be pruned, keep all partitions to avoid an empty
worker map (see the javadoc above).
+ return partitionsToKeep.isEmpty() ? null : partitionsToKeep;
+ }
+
+ private static boolean containsAny(@Nullable List<String> segments,
Set<String> matchedSegments) {
+ if (segments != null) {
+ for (String segment : segments) {
+ if (matchedSegments.contains(segment)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /// Counts the segments in partitions dropped by broker pruning (those
absent from {@code partitionsToKeep}).
+ private static long countPrunedSegments(PartitionInfo[] partitionInfoMap,
Set<Integer> partitionsToKeep) {
+ long numPrunedSegments = 0;
+ for (int i = 0; i < partitionInfoMap.length; i++) {
+ PartitionInfo partitionInfo = partitionInfoMap[i];
+ if (partitionInfo != null && !partitionsToKeep.contains(i)) {
+ numPrunedSegments +=
CollectionUtils.size(partitionInfo._offlineSegments)
+ + CollectionUtils.size(partitionInfo._realtimeSegments);
+ }
+ }
+ return numPrunedSegments;
+ }
+
+ /// Pick one worker per partition for partitioned leaf stage. When {@code
partitionsToKeep} is non-null (broker
+ /// pruning is active), partitions absent from the set are pruned by the
query filter and skipped.
private void assignOnePartitionPerWorker(String tableName, long requestId,
PartitionInfo[] partitionInfoMap,
- Map<String, ServerInstance> enabledServerInstanceMap,
- Map<Integer, QueryServerInstance> workedIdToServerInstanceMap,
+ @Nullable Set<Integer> partitionsToKeep, Map<String, ServerInstance>
enabledServerInstanceMap,
+ Map<Integer, QueryServerInstance> workerIdToServerInstanceMap,
Map<Integer, Map<String, List<String>>> workerIdToSegmentsMap) {
int numPartitions = partitionInfoMap.length;
int workerId = 0;
for (int i = 0; i < numPartitions; i++) {
+ // Skip partitions pruned by the broker filter. Empty partitions are
never in partitionsToKeep, so under pruning
+ // they are skipped here too; the precondition below only fires when
pruning is inactive (partitionsToKeep null).
+ if (partitionsToKeep != null && !partitionsToKeep.contains(i)) {
+ continue;
+ }
PartitionInfo partitionInfo = partitionInfoMap[i];
// TODO: Currently we don't support the case when a partition doesn't
contain any segment. The reason is that
// the leaf stage won't be able to directly return empty response.
Preconditions.checkState(partitionInfo != null, "Failed to find any
segment for table: %s, partition: %s",
tableName, i);
- // NOTE: Pick worker based on the request id so that the same worker is
picked across different table scan when
- // the segments for the same partition is colocated
+ // NOTE: Pick worker based on the request id plus the partition id (not
a running counter) so that the same worker
+ // is picked across different table scans when the segments for
the same partition are colocated, and so
+ // that skipping pruned partitions does not shift the server
assignment of the surviving ones.
ServerInstance serverInstance =
- pickEnabledServer(partitionInfo._fullyReplicatedServers,
enabledServerInstanceMap, requestId++);
+ pickEnabledServer(partitionInfo._fullyReplicatedServers,
enabledServerInstanceMap, requestId + i);
Preconditions.checkState(serverInstance != null,
"Failed to find enabled fully replicated server for table: %s,
partition: %s", tableName, i);
- workedIdToServerInstanceMap.put(workerId, new
QueryServerInstance(serverInstance));
+ workerIdToServerInstanceMap.put(workerId, new
QueryServerInstance(serverInstance));
workerIdToSegmentsMap.put(workerId,
getSegmentsMap(partitionInfo._offlineSegments,
partitionInfo._realtimeSegments));
workerId++;
@@ -954,8 +1098,9 @@ public class WorkerManager {
/// E.g. when there are 16 partitions for table A and 4 partitions for table
B, we may assign 16 partitions for table
/// A to 4 workers, where partition 0, 4, 8, 12 goes to worker 0, partition
1, 5, 9, 13 goes to worker 1, etc.
private void assignMultiplePartitionsPerWorker(String tableName, long
requestId, int numPartitionsPerWorker,
- PartitionInfo[] partitionInfoMap, Map<String, ServerInstance>
enabledServerInstanceMap,
- Map<Integer, QueryServerInstance> workedIdToServerInstanceMap,
+ PartitionInfo[] partitionInfoMap, @Nullable Set<Integer>
partitionsToKeep,
+ Map<String, ServerInstance> enabledServerInstanceMap,
+ Map<Integer, QueryServerInstance> workerIdToServerInstanceMap,
Map<Integer, Map<String, List<String>>> workerIdToSegmentsMap) {
int numPartitions = partitionInfoMap.length;
assert numPartitions % numPartitionsPerWorker == 0;
@@ -966,6 +1111,10 @@ public class WorkerManager {
List<String> offlineSegments = null;
List<String> realtimeSegments = null;
for (int j = i; j < numPartitions; j += numWorkers) {
+ if (partitionsToKeep != null && !partitionsToKeep.contains(j)) {
+ // Partition pruned by the broker filter.
+ continue;
+ }
PartitionInfo partitionInfo = partitionInfoMap[j];
if (partitionInfo == null) {
continue;
@@ -990,18 +1139,23 @@ public class WorkerManager {
}
}
}
- // TODO: Currently we don't support the case when all partitions for a
worker don't contain any segment. The
- // reason is that the leaf stage won't be able to directly return
empty response.
- Preconditions.checkState(fullyReplicatedServers != null,
- "Failed to find any segment for table: %s, worker: %s, partitions
per worker: %s", tableName, i,
- numPartitionsPerWorker);
- // NOTE: Pick worker based on the request id so that the same worker is
picked across different table scan when
- // the segments for the same partition is colocated
- ServerInstance serverInstance =
pickEnabledServer(fullyReplicatedServers, enabledServerInstanceMap,
requestId++);
+ // Without broker pruning we don't support a worker whose partitions all
lack segments, because the leaf stage
+ // can't directly return an empty response. With pruning active a
fully-pruned worker is legitimate and skipped.
+ if (fullyReplicatedServers == null) {
+ Preconditions.checkState(partitionsToKeep != null,
+ "Failed to find any segment for table: %s, worker: %s, partitions
per worker: %s", tableName, i,
+ numPartitionsPerWorker);
+ continue;
+ }
+ // NOTE: Pick worker based on the request id plus the worker index (not
a running counter) so that the same worker
+ // is picked across different table scans when the segments for
the same partition are colocated, and so
+ // that skipping fully-pruned workers does not shift the server
assignment of the surviving ones.
+ ServerInstance serverInstance =
+ pickEnabledServer(fullyReplicatedServers, enabledServerInstanceMap,
requestId + i);
Preconditions.checkState(serverInstance != null,
"Failed to find enabled fully replicated server for table: %s,
worker: %s, partitions per worker: %s",
tableName, i, numPartitionsPerWorker);
- workedIdToServerInstanceMap.put(workerId, new
QueryServerInstance(serverInstance));
+ workerIdToServerInstanceMap.put(workerId, new
QueryServerInstance(serverInstance));
workerIdToSegmentsMap.put(workerId, getSegmentsMap(offlineSegments,
realtimeSegments));
workerId++;
}
diff --git
a/pinot-query-planner/src/test/java/org/apache/pinot/query/routing/WorkerManagerTest.java
b/pinot-query-planner/src/test/java/org/apache/pinot/query/routing/WorkerManagerTest.java
index e12f60c4693..0c5ac12cfeb 100644
---
a/pinot-query-planner/src/test/java/org/apache/pinot/query/routing/WorkerManagerTest.java
+++
b/pinot-query-planner/src/test/java/org/apache/pinot/query/routing/WorkerManagerTest.java
@@ -31,6 +31,7 @@ import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.apache.pinot.common.config.provider.TableCache;
import org.apache.pinot.common.request.BrokerRequest;
import org.apache.pinot.common.request.Expression;
+import org.apache.pinot.common.request.PinotQuery;
import org.apache.pinot.core.routing.RoutingManager;
import org.apache.pinot.core.routing.RoutingTable;
import org.apache.pinot.core.routing.SegmentsToQuery;
@@ -45,6 +46,7 @@ import org.apache.pinot.spi.config.table.TableConfig;
import org.apache.pinot.spi.data.FieldSpec;
import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.sql.parsers.CalciteSqlCompiler;
import org.testng.annotations.Test;
import static org.mockito.ArgumentMatchers.anyString;
@@ -331,6 +333,439 @@ public class WorkerManagerTest {
assertEquals(queryOptions.get("useLeafServerForIntermediateStage"),
"true");
}
+ //
---------------------------------------------------------------------------
+ // Broker pruning: partitioned leaf path
+ //
---------------------------------------------------------------------------
+
+ private static final String PARTITIONED_TABLE = "testTable";
+ private static final String PARTITIONED_TABLE_OFFLINE = "testTable_OFFLINE";
+
+ @Test
+ public void testBrokerPruningPartitionedLeafPrunesNonMatchingPartitions() {
+ // 4 partitions, one segment + one server each. The routing manager
reports only partition 2's segment survives.
+ QueryEnvironment queryEnvironment =
+ newPartitionedQueryEnvironment(new int[]{0, 1, 2, 3}, 4,
List.of("seg2"), /*reportedPrunedByRouting=*/999);
+ try (QueryEnvironment.CompiledQuery compiledQuery =
queryEnvironment.compile(
+ "SET useBrokerPruning=true; SELECT col2 FROM testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='4') */ "
+ + "WHERE col1 = 'foo'")) {
+ DispatchableSubPlan dispatchableSubPlan =
compiledQuery.planQuery(0).getQueryPlan();
+ // seg0, seg1, seg3 pruned (3). The count is computed from dropped
partitions, not the routing table's own
+ // numPrunedSegments (999), proving the partitioned path computes it
independently.
+ assertEquals(dispatchableSubPlan.getNumSegmentsPrunedByBroker(), 3);
+ DispatchablePlanFragment leaf = leafFragment(dispatchableSubPlan);
+ assertNotNull(leaf);
+ assertEquals(leaf.getWorkerIdToSegmentsMap().size(), 1);
+ assertEquals(assignedSegments(leaf), List.of("seg2"));
+ }
+ }
+
+ @Test
+ public void
testBrokerPruningPartitionedLeafKeepsMultipleMatchingPartitions() {
+ QueryEnvironment queryEnvironment =
+ newPartitionedQueryEnvironment(new int[]{0, 1, 2, 3}, 4,
List.of("seg1", "seg3"), 0);
+ try (QueryEnvironment.CompiledQuery compiledQuery =
queryEnvironment.compile(
+ "SET useBrokerPruning=true; SELECT col2 FROM testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='4') */ "
+ + "WHERE col1 = 'foo'")) {
+ DispatchableSubPlan dispatchableSubPlan =
compiledQuery.planQuery(0).getQueryPlan();
+ // seg0, seg2 pruned.
+ assertEquals(dispatchableSubPlan.getNumSegmentsPrunedByBroker(), 2);
+ DispatchablePlanFragment leaf = leafFragment(dispatchableSubPlan);
+ assertNotNull(leaf);
+ assertEquals(leaf.getWorkerIdToSegmentsMap().size(), 2);
+ assertEquals(new HashSet<>(assignedSegments(leaf)), Set.of("seg1",
"seg3"));
+ }
+ }
+
+ @Test
+ public void testBrokerPruningPartitionedLeafAllPrunedFallsBackToUnpruned() {
+ // The routing manager reports that no segment survives. Rather than
produce an empty worker map (which would break
+ // exchanges in a multi-leaf plan), the partitioned path falls back to an
unpruned assignment: the server-side
+ // filter still yields the correct empty result.
+ QueryEnvironment queryEnvironment = newPartitionedQueryEnvironment(new
int[]{0, 1, 2, 3}, 4, List.of(), 4);
+ try (QueryEnvironment.CompiledQuery compiledQuery =
queryEnvironment.compile(
+ "SET useBrokerPruning=true; SELECT col2 FROM testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='4') */ "
+ + "WHERE col1 = 'foo'")) {
+ DispatchableSubPlan dispatchableSubPlan =
compiledQuery.planQuery(0).getQueryPlan();
+ assertEquals(dispatchableSubPlan.getNumSegmentsPrunedByBroker(), 0);
+ DispatchablePlanFragment leaf = leafFragment(dispatchableSubPlan);
+ assertNotNull(leaf);
+ assertEquals(leaf.getWorkerIdToSegmentsMap().size(), 4);
+ assertEquals(new HashSet<>(assignedSegments(leaf)), Set.of("seg0",
"seg1", "seg2", "seg3"));
+ }
+ }
+
+ @Test
+ public void testBrokerPruningPartitionedLeafDisabledKeepsAllPartitions() {
+ // Without SET useBrokerPruning=true the logical planner default (false)
applies, so no pruning occurs.
+ QueryEnvironment queryEnvironment =
+ newPartitionedQueryEnvironment(new int[]{0, 1, 2, 3}, 4,
List.of("seg2"), 3);
+ try (QueryEnvironment.CompiledQuery compiledQuery =
queryEnvironment.compile(
+ "SELECT col2 FROM testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='4') */ "
+ + "WHERE col1 = 'foo'")) {
+ DispatchableSubPlan dispatchableSubPlan =
compiledQuery.planQuery(0).getQueryPlan();
+ assertEquals(dispatchableSubPlan.getNumSegmentsPrunedByBroker(), 0);
+ DispatchablePlanFragment leaf = leafFragment(dispatchableSubPlan);
+ assertNotNull(leaf);
+ assertEquals(leaf.getWorkerIdToSegmentsMap().size(), 4);
+ }
+ }
+
+ @Test
+ public void testBrokerPruningPartitionedLeafMultiplePartitionsPerWorker() {
+ // 4 partitions across 2 workers (partition_size=2 => 2 partitions per
worker). Worker 0 owns partitions {0, 2}
+ // (colocated on server 0), worker 1 owns {1, 3} (colocated on server 1).
Only worker 0's partitions survive.
+ QueryEnvironment queryEnvironment =
+ newPartitionedQueryEnvironment(new int[]{0, 1, 0, 1}, 2,
List.of("seg0", "seg2"), 0);
+ try (QueryEnvironment.CompiledQuery compiledQuery =
queryEnvironment.compile(
+ "SET useBrokerPruning=true; SELECT col2 FROM testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='2') */ "
+ + "WHERE col1 = 'foo'")) {
+ DispatchableSubPlan dispatchableSubPlan =
compiledQuery.planQuery(0).getQueryPlan();
+ // Worker 1's partitions (seg1, seg3) are fully pruned.
+ assertEquals(dispatchableSubPlan.getNumSegmentsPrunedByBroker(), 2);
+ DispatchablePlanFragment leaf = leafFragment(dispatchableSubPlan);
+ assertNotNull(leaf);
+ assertEquals(leaf.getWorkerIdToSegmentsMap().size(), 1);
+ assertEquals(new HashSet<>(assignedSegments(leaf)), Set.of("seg0",
"seg2"));
+ }
+ }
+
+ //
---------------------------------------------------------------------------
+ // Broker pruning: logical-table leaf path (filter forwarding into
per-physical-table routing requests)
+ //
---------------------------------------------------------------------------
+
+ @Test
+ public void testBuildLogicalTableRoutingRequestForwardsFilter() {
+ // With a routing query present, the physical table's routing request must
carry the leaf filter (so segment
+ // pruners can run), the typed logical table name, and the query options.
+ PinotQuery routingPinotQuery =
+ CalciteSqlCompiler.compileToBrokerRequest("SELECT col2 FROM someTable
WHERE col1 = 'foo'").getPinotQuery();
+ BrokerRequest brokerRequest =
WorkerManager.buildLogicalTableRoutingBrokerRequest("logicalTable_OFFLINE",
+ routingPinotQuery, Map.of("useLeafServerForIntermediateStage",
"true"));
+
+ assertEquals(brokerRequest.getPinotQuery().getDataSource().getTableName(),
"logicalTable_OFFLINE");
+ Expression filterExpression =
brokerRequest.getPinotQuery().getFilterExpression();
+ assertNotNull(filterExpression);
+ assertEquals(filterExpression.getFunctionCall().getOperator(), "EQUALS");
+
assertEquals(filterExpression.getFunctionCall().getOperands().get(0).getIdentifier().getName(),
"col1");
+
assertEquals(brokerRequest.getPinotQuery().getQueryOptions().get("useLeafServerForIntermediateStage"),
"true");
+ // The source routing query must not be mutated (the method deep-copies
before rewriting the table name).
+ assertEquals(routingPinotQuery.getDataSource().getTableName(),
"someTable");
+ }
+
+ @Test
+ public void testBuildLogicalTableRoutingRequestWithoutFilterUsesSelectStar()
{
+ // With no routing query (pruning disabled/unsupported), a bare SELECT *
request is used so no pruning occurs.
+ BrokerRequest brokerRequest =
+
WorkerManager.buildLogicalTableRoutingBrokerRequest("logicalTable_REALTIME",
null, Map.of());
+ assertEquals(brokerRequest.getPinotQuery().getDataSource().getTableName(),
"logicalTable_REALTIME");
+ assertNull(brokerRequest.getPinotQuery().getFilterExpression());
+ }
+
+ @Test
+ public void testBrokerPruningPartitionedLeafSkippedForColocatedJoin() {
+ // A pre-partitioned leaf (here both sides of a colocated self-join) feeds
a 1-to-1 direct exchange wired by worker
+ // id. Compacting a side's workers for pruned partitions could pair
mismatched partitions across the exchange, so
+ // pruning is skipped for any pre-partitioned leaf and all partitions stay
assigned on every scan.
+ QueryEnvironment queryEnvironment =
+ newPartitionedQueryEnvironment(new int[]{0, 1, 2, 3}, 4,
List.of("seg2"), 3);
+ try (QueryEnvironment.CompiledQuery compiledQuery =
queryEnvironment.compile(
+ "SET useBrokerPruning=true; SELECT t1.col2 FROM testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='4') */ t1 "
+ + "JOIN testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='4') */ t2 "
+ + "ON t1.col1 = t2.col1 WHERE t1.col1 = 'foo'")) {
+ DispatchableSubPlan dispatchableSubPlan =
compiledQuery.planQuery(0).getQueryPlan();
+ assertEquals(dispatchableSubPlan.getNumSegmentsPrunedByBroker(), 0);
+ List<DispatchablePlanFragment> leafFragments =
leafFragments(dispatchableSubPlan);
+ assertFalse(leafFragments.isEmpty());
+ for (DispatchablePlanFragment leaf : leafFragments) {
+ assertEquals(leaf.getWorkerIdToSegmentsMap().size(), 4, "Expected all
partitions assigned for a pruned-gated "
+ + "colocated join leaf");
+ }
+ }
+ }
+
+ @Test
+ public void
testBrokerPruningPartitionedLeafKeepsPartitionWithUnavailableSegment() {
+ // Partition 2's segment survives; partition 1's segment is merely
unavailable (not pruned). A partition must be
+ // kept when its only segment is unavailable, so we never drop data that a
transient outage hid -- only partitions
+ // 0 and 3 (whose segments were actually pruned) are dropped.
+ QueryEnvironment queryEnvironment = newPartitionedQueryEnvironment(new
int[]{0, 1, 2, 3}, 4, 1, List.of("seg2"),
+ List.of("seg1"), 0, false);
+ try (QueryEnvironment.CompiledQuery compiledQuery =
queryEnvironment.compile(
+ "SET useBrokerPruning=true; SELECT col2 FROM testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='4') */ "
+ + "WHERE col1 = 'foo'")) {
+ DispatchableSubPlan dispatchableSubPlan =
compiledQuery.planQuery(0).getQueryPlan();
+ // Only seg0 and seg3 pruned; seg1 is unavailable (kept), seg2 survives.
+ assertEquals(dispatchableSubPlan.getNumSegmentsPrunedByBroker(), 2);
+ DispatchablePlanFragment leaf = leafFragment(dispatchableSubPlan);
+ assertNotNull(leaf);
+ assertEquals(leaf.getWorkerIdToSegmentsMap().size(), 2);
+ assertEquals(new HashSet<>(assignedSegments(leaf)), Set.of("seg1",
"seg2"));
+ }
+ }
+
+ @Test
+ public void testBrokerPruningPartitionedLeafFallsBackWhenRoutingFails() {
+ // If the routing call used to compute partition survival fails, pruning
is best-effort and must fall back to the
+ // unpruned assignment rather than failing the query.
+ QueryEnvironment queryEnvironment =
+ newPartitionedQueryEnvironment(new int[]{0, 1, 2, 3}, 4, 1, List.of(),
List.of(), 0, /*throwOnRouting=*/true);
+ try (QueryEnvironment.CompiledQuery compiledQuery =
queryEnvironment.compile(
+ "SET useBrokerPruning=true; SELECT col2 FROM testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='4') */ "
+ + "WHERE col1 = 'foo'")) {
+ DispatchableSubPlan dispatchableSubPlan =
compiledQuery.planQuery(0).getQueryPlan();
+ assertEquals(dispatchableSubPlan.getNumSegmentsPrunedByBroker(), 0);
+ DispatchablePlanFragment leaf = leafFragment(dispatchableSubPlan);
+ assertNotNull(leaf);
+ assertEquals(leaf.getWorkerIdToSegmentsMap().size(), 4);
+ }
+ }
+
+ @Test
+ public void testBrokerPruningPartitionedLeafKeepsServerPlacementStable() {
+ // Every partition is fully replicated on the same two servers, so
pickEnabledServer's choice depends on the
+ // per-partition seed. Pruning partition 0 must NOT shift the surviving
partitions' server assignments -- the seed
+ // is requestId + partitionId, not a running counter that would shift when
earlier partitions are skipped. Assert
+ // each surviving segment lands on the same server whether or not
partition 0 was pruned.
+ QueryEnvironment unprunedEnv = newPartitionedQueryEnvironment(new int[]{0,
0, 0, 0}, 2, 2,
+ List.of("seg0", "seg1", "seg2", "seg3"), List.of(), 0, false);
+ Map<String, String> unprunedPlacement;
+ try (QueryEnvironment.CompiledQuery compiledQuery = unprunedEnv.compile(
+ "SET useBrokerPruning=true; SELECT col2 FROM testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='4') */ "
+ + "WHERE col1 = 'foo'")) {
+ unprunedPlacement =
segmentToServer(leafFragment(compiledQuery.planQuery(0).getQueryPlan()));
+ }
+ assertEquals(unprunedPlacement.size(), 4);
+
+ // Prune partition 0 (only seg1/seg2/seg3 survive).
+ QueryEnvironment prunedEnv = newPartitionedQueryEnvironment(new int[]{0,
0, 0, 0}, 2, 2,
+ List.of("seg1", "seg2", "seg3"), List.of(), 0, false);
+ Map<String, String> prunedPlacement;
+ try (QueryEnvironment.CompiledQuery compiledQuery = prunedEnv.compile(
+ "SET useBrokerPruning=true; SELECT col2 FROM testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='4') */ "
+ + "WHERE col1 = 'foo'")) {
+ prunedPlacement =
segmentToServer(leafFragment(compiledQuery.planQuery(0).getQueryPlan()));
+ }
+
+ assertEquals(prunedPlacement.size(), 3);
+ for (String segment : List.of("seg1", "seg2", "seg3")) {
+ assertEquals(prunedPlacement.get(segment),
unprunedPlacement.get(segment),
+ "Pruning partition 0 shifted the server assignment of surviving " +
segment);
+ }
+ }
+
+ @Test
+ public void testBrokerPruningPartitionedLeafHybridTable() {
+ // Hybrid table: partition p holds offline segment segO{p} and realtime
segment segR{p}, both colocated on
+ // server p. The routing manager reports segO2 surviving on the offline
side and segR1 on the realtime side, so
+ // partitions {1, 2} are kept (a partition survives if a segment of EITHER
type matches) and {0, 3} are pruned.
+ QueryEnvironment queryEnvironment =
newHybridPartitionedQueryEnvironment(List.of("segO2"), List.of("segR1"));
+ try (QueryEnvironment.CompiledQuery compiledQuery =
queryEnvironment.compile(
+ "SET useBrokerPruning=true; SELECT col2 FROM testTable "
+ + "/*+ tableOptions(partition_function='hashcode',
partition_key='col1', partition_size='4') */ "
+ + "WHERE col1 = 'foo'")) {
+ DispatchableSubPlan dispatchableSubPlan =
compiledQuery.planQuery(0).getQueryPlan();
+ // Pruned = both segments of partitions 0 and 3.
+ assertEquals(dispatchableSubPlan.getNumSegmentsPrunedByBroker(), 4);
+ DispatchablePlanFragment leaf = leafFragment(dispatchableSubPlan);
+ assertNotNull(leaf);
+ assertEquals(leaf.getWorkerIdToSegmentsMap().size(), 2);
+ // Pruning is partition-level: surviving partitions dispatch ALL their
segments, so the non-matching segO1 and
+ // segR2 are still included alongside the matching segR1 and segO2.
+ assertEquals(new HashSet<>(assignedSegments(leaf)), Set.of("segO1",
"segR1", "segO2", "segR2"));
+ }
+ }
+
+ /**
+ * Builds a QueryEnvironment for a hybrid partitioned table "testTable"
(function Hashcode on col1, 4 partitions).
+ * Partition {@code p} holds offline segment {@code "segO{p}"} and realtime
segment {@code "segR{p}"}, both fully
+ * replicated on server {@code p}. The given surviving segment lists are
what the {@link RoutingManager} returns for
+ * the filtered routing query of each table type.
+ */
+ private static QueryEnvironment
newHybridPartitionedQueryEnvironment(List<String> survivingOfflineSegments,
+ List<String> survivingRealtimeSegments) {
+ int numPartitions = 4;
+ ServerInstance[] servers = new ServerInstance[numPartitions];
+ Map<String, ServerInstance> enabledServers = new HashMap<>();
+ for (int i = 0; i < numPartitions; i++) {
+ servers[i] = getServerInstance("localhost", i + 1);
+ enabledServers.put(servers[i].getInstanceId(), servers[i]);
+ }
+ TablePartitionReplicatedServersInfo.PartitionInfo[] offlinePartitions =
+ new TablePartitionReplicatedServersInfo.PartitionInfo[numPartitions];
+ TablePartitionReplicatedServersInfo.PartitionInfo[] realtimePartitions =
+ new TablePartitionReplicatedServersInfo.PartitionInfo[numPartitions];
+ for (int p = 0; p < numPartitions; p++) {
+ Set<String> partitionServers = Set.of(servers[p].getInstanceId());
+ offlinePartitions[p] = new
TablePartitionReplicatedServersInfo.PartitionInfo(partitionServers,
+ List.of("segO" + p));
+ realtimePartitions[p] = new
TablePartitionReplicatedServersInfo.PartitionInfo(partitionServers,
+ List.of("segR" + p));
+ }
+ String realtimeTableName = PARTITIONED_TABLE + "_REALTIME";
+ TablePartitionReplicatedServersInfo offlineInfo = new
TablePartitionReplicatedServersInfo(
+ PARTITIONED_TABLE_OFFLINE, "col1", "Hashcode", numPartitions,
offlinePartitions, List.of());
+ TablePartitionReplicatedServersInfo realtimeInfo = new
TablePartitionReplicatedServersInfo(
+ realtimeTableName, "col1", "Hashcode", numPartitions,
realtimePartitions, List.of());
+
+ PartitionedRoutingManager routingManager = new
PartitionedRoutingManager(enabledServers,
+ Map.of(PARTITIONED_TABLE_OFFLINE, offlineInfo, realtimeTableName,
realtimeInfo),
+ Map.of(PARTITIONED_TABLE_OFFLINE, hybridRoutingTable(servers,
survivingOfflineSegments),
+ realtimeTableName, hybridRoutingTable(servers,
survivingRealtimeSegments)),
+ false, new TimeBoundaryInfo("col3", "100"));
+
+ Map<String, String> tableNameMap = new HashMap<>();
+ tableNameMap.put(PARTITIONED_TABLE_OFFLINE, PARTITIONED_TABLE_OFFLINE);
+ tableNameMap.put(realtimeTableName, realtimeTableName);
+ tableNameMap.put(PARTITIONED_TABLE, PARTITIONED_TABLE);
+ TableCache tableCache = mock(TableCache.class);
+ when(tableCache.getTableNameMap()).thenReturn(tableNameMap);
+ when(tableCache.getActualTableName(anyString())).thenAnswer(inv ->
tableNameMap.get(inv.getArgument(0)));
+
when(tableCache.getSchema(anyString())).thenReturn(getSchemaBuilder(PARTITIONED_TABLE).build());
+
when(tableCache.getTableConfig(PARTITIONED_TABLE_OFFLINE)).thenReturn(mock(TableConfig.class));
+
when(tableCache.getTableConfig(realtimeTableName)).thenReturn(mock(TableConfig.class));
+
+ WorkerManager workerManager = new WorkerManager("Broker_localhost",
"localhost", 5, routingManager);
+ return new QueryEnvironment(CommonConstants.DEFAULT_DATABASE, tableCache,
workerManager);
+ }
+
+ /** Buckets the given surviving segments onto their owning server (partition
p's segment lives on server p). */
+ private static RoutingTable hybridRoutingTable(ServerInstance[] servers,
List<String> survivingSegments) {
+ Map<ServerInstance, List<String>> serverToSegmentList = new HashMap<>();
+ for (String segment : survivingSegments) {
+ int partition = Integer.parseInt(segment.substring("segO".length()));
+ serverToSegmentList.computeIfAbsent(servers[partition], k -> new
ArrayList<>()).add(segment);
+ }
+ Map<ServerInstance, SegmentsToQuery> serverToSegments = new HashMap<>();
+ serverToSegmentList.forEach((server, segments) ->
serverToSegments.put(server,
+ new SegmentsToQuery(segments, List.of())));
+ return new RoutingTable(serverToSegments, List.of(), 0);
+ }
+
+ private static QueryEnvironment newPartitionedQueryEnvironment(int[]
serverIdxPerPartition, int numServers,
+ List<String> survivingSegments, int reportedPrunedByRouting) {
+ return newPartitionedQueryEnvironment(serverIdxPerPartition, numServers,
1, survivingSegments, List.of(),
+ reportedPrunedByRouting, false);
+ }
+
+ /**
+ * Builds a QueryEnvironment for an offline partitioned table "testTable"
(function Hashcode on col1). Partition
+ * {@code p} holds one segment {@code "seg{p}"} fully replicated on the
{@code replicasPerPartition} servers starting
+ * at {@code serverIdxPerPartition[p]}. {@code survivingSegments} is what
the {@link RoutingManager} returns from
+ * getRoutingTable for the filtered routing query -- i.e. the segments that
survive broker pruning; the corresponding
+ * partitions are kept. {@code unavailableSegments} are reported by the
routing table as unavailable (they must keep
+ * their partition alive). When {@code throwOnRouting} is true,
getRoutingTable throws to exercise the fail-open path.
+ */
+ private static QueryEnvironment newPartitionedQueryEnvironment(int[]
serverIdxPerPartition, int numServers,
+ int replicasPerPartition, List<String> survivingSegments, List<String>
unavailableSegments,
+ int reportedPrunedByRouting, boolean throwOnRouting) {
+ int numPartitions = serverIdxPerPartition.length;
+ ServerInstance[] servers = new ServerInstance[numServers];
+ Map<String, ServerInstance> enabledServers = new HashMap<>();
+ for (int i = 0; i < numServers; i++) {
+ servers[i] = getServerInstance("localhost", i + 1);
+ enabledServers.put(servers[i].getInstanceId(), servers[i]);
+ }
+ TablePartitionReplicatedServersInfo.PartitionInfo[] partitionInfoMap =
+ new TablePartitionReplicatedServersInfo.PartitionInfo[numPartitions];
+ for (int p = 0; p < numPartitions; p++) {
+ Set<String> fullyReplicatedServers = new HashSet<>();
+ for (int r = 0; r < replicasPerPartition; r++) {
+ fullyReplicatedServers.add(servers[serverIdxPerPartition[p] +
r].getInstanceId());
+ }
+ partitionInfoMap[p] =
+ new
TablePartitionReplicatedServersInfo.PartitionInfo(fullyReplicatedServers,
List.of("seg" + p));
+ }
+ TablePartitionReplicatedServersInfo tablePartitionInfo = new
TablePartitionReplicatedServersInfo(
+ PARTITIONED_TABLE_OFFLINE, "col1", "Hashcode", numPartitions,
partitionInfoMap, List.of());
+
+ // Model the pruned routing table: surviving segments bucketed onto their
owning server.
+ Map<ServerInstance, List<String>> serverToSegmentList = new HashMap<>();
+ for (String segment : survivingSegments) {
+ int partition = Integer.parseInt(segment.substring("seg".length()));
+
serverToSegmentList.computeIfAbsent(servers[serverIdxPerPartition[partition]],
k -> new ArrayList<>())
+ .add(segment);
+ }
+ Map<ServerInstance, SegmentsToQuery> serverToSegments = new HashMap<>();
+ serverToSegmentList.forEach((server, segments) ->
serverToSegments.put(server,
+ new SegmentsToQuery(segments, List.of())));
+ RoutingTable prunedRoutingTable =
+ new RoutingTable(serverToSegments, new
ArrayList<>(unavailableSegments), reportedPrunedByRouting);
+
+ PartitionedRoutingManager routingManager = new
PartitionedRoutingManager(enabledServers,
+ Map.of(PARTITIONED_TABLE_OFFLINE, tablePartitionInfo),
+ Map.of(PARTITIONED_TABLE_OFFLINE, prunedRoutingTable), throwOnRouting);
+
+ Map<String, String> tableNameMap = new HashMap<>();
+ tableNameMap.put(PARTITIONED_TABLE_OFFLINE, PARTITIONED_TABLE_OFFLINE);
+ tableNameMap.put(PARTITIONED_TABLE, PARTITIONED_TABLE);
+ TableCache tableCache = mock(TableCache.class);
+ when(tableCache.getTableNameMap()).thenReturn(tableNameMap);
+ when(tableCache.getActualTableName(anyString())).thenAnswer(inv ->
tableNameMap.get(inv.getArgument(0)));
+
when(tableCache.getSchema(anyString())).thenReturn(getSchemaBuilder(PARTITIONED_TABLE).build());
+
when(tableCache.getTableConfig(PARTITIONED_TABLE_OFFLINE)).thenReturn(mock(TableConfig.class));
+
+ WorkerManager workerManager = new WorkerManager("Broker_localhost",
"localhost", 5, routingManager);
+ return new QueryEnvironment(CommonConstants.DEFAULT_DATABASE, tableCache,
workerManager);
+ }
+
+ /** Returns the leaf table-scan fragment: the only fragment with segment
assignments. */
+ @Nullable
+ private static DispatchablePlanFragment leafFragment(DispatchableSubPlan
dispatchableSubPlan) {
+ List<DispatchablePlanFragment> leafFragments =
leafFragments(dispatchableSubPlan);
+ return leafFragments.isEmpty() ? null : leafFragments.get(0);
+ }
+
+ /** Returns all fragments with segment assignments (the table-scan leaves).
*/
+ private static List<DispatchablePlanFragment>
leafFragments(DispatchableSubPlan dispatchableSubPlan) {
+ List<DispatchablePlanFragment> leafFragments = new ArrayList<>();
+ for (DispatchablePlanFragment fragment :
dispatchableSubPlan.getQueryStageMap().values()) {
+ if (!fragment.getWorkerIdToSegmentsMap().isEmpty()) {
+ leafFragments.add(fragment);
+ }
+ }
+ return leafFragments;
+ }
+
+ /** Maps each assigned segment to the instance id of the server its worker
was placed on. */
+ private static Map<String, String> segmentToServer(DispatchablePlanFragment
leafFragment) {
+ Map<Integer, String> workerIdToServer = new HashMap<>();
+ for (Map.Entry<QueryServerInstance, List<Integer>> entry
+ : leafFragment.getServerInstanceToWorkerIdMap().entrySet()) {
+ for (Integer workerId : entry.getValue()) {
+ workerIdToServer.put(workerId, entry.getKey().getInstanceId());
+ }
+ }
+ Map<String, String> segmentToServer = new HashMap<>();
+ for (Map.Entry<Integer, Map<String, List<String>>> entry :
leafFragment.getWorkerIdToSegmentsMap().entrySet()) {
+ String server = workerIdToServer.get(entry.getKey());
+ for (List<String> segments : entry.getValue().values()) {
+ for (String segment : segments) {
+ segmentToServer.put(segment, server);
+ }
+ }
+ }
+ return segmentToServer;
+ }
+
+ private static List<String> assignedSegments(DispatchablePlanFragment
leafFragment) {
+ List<String> segments = new ArrayList<>();
+ for (Map<String, List<String>> segmentsByType :
leafFragment.getWorkerIdToSegmentsMap().values()) {
+ segmentsByType.values().forEach(segments::addAll);
+ }
+ return segments;
+ }
+
/**
* Tests that literal-only stages (e.g. UNION ALL of constant values) are
assigned to the same
* servers as the table-scanning stages, not to all enabled servers across
all tenants.
@@ -645,4 +1080,96 @@ public class WorkerManagerTest {
return false;
}
}
+
+ /**
+ * A RoutingManager for the partitioned leaf path. It exposes a {@link
TablePartitionReplicatedServersInfo} per typed
+ * table (driving {@code calculatePartitionTableInfo}) and returns a
pre-configured {@link RoutingTable} of surviving
+ * segments from getRoutingTable (simulating what the real segment pruners
would return for the query filter). This
+ * lets the test drive which partitions survive without depending on the
pruner internals (which are tested
+ * separately).
+ */
+ private static class PartitionedRoutingManager implements RoutingManager {
+ private final Map<String, ServerInstance> _enabledServers;
+ private final Map<String, TablePartitionReplicatedServersInfo>
_partitionInfoByTable;
+ private final Map<String, RoutingTable> _routingTableByTable;
+ private final boolean _throwOnRouting;
+ @Nullable
+ private final TimeBoundaryInfo _timeBoundaryInfo;
+
+ PartitionedRoutingManager(Map<String, ServerInstance> enabledServers,
+ Map<String, TablePartitionReplicatedServersInfo> partitionInfoByTable,
+ Map<String, RoutingTable> routingTableByTable, boolean throwOnRouting)
{
+ this(enabledServers, partitionInfoByTable, routingTableByTable,
throwOnRouting, null);
+ }
+
+ PartitionedRoutingManager(Map<String, ServerInstance> enabledServers,
+ Map<String, TablePartitionReplicatedServersInfo> partitionInfoByTable,
+ Map<String, RoutingTable> routingTableByTable, boolean throwOnRouting,
+ @Nullable TimeBoundaryInfo timeBoundaryInfo) {
+ _enabledServers = enabledServers;
+ _partitionInfoByTable = partitionInfoByTable;
+ _routingTableByTable = routingTableByTable;
+ _throwOnRouting = throwOnRouting;
+ _timeBoundaryInfo = timeBoundaryInfo;
+ }
+
+ @Override
+ public Map<String, ServerInstance> getEnabledServerInstanceMap() {
+ return _enabledServers;
+ }
+
+ @Nullable
+ @Override
+ public RoutingTable getRoutingTable(BrokerRequest brokerRequest, long
requestId) {
+ if (_throwOnRouting) {
+ throw new RuntimeException("Simulated routing failure");
+ }
+ return
_routingTableByTable.get(brokerRequest.getQuerySource().getTableName());
+ }
+
+ @Nullable
+ @Override
+ public RoutingTable getRoutingTable(BrokerRequest brokerRequest, String
tableNameWithType, long requestId) {
+ return getRoutingTable(brokerRequest, requestId);
+ }
+
+ @Nullable
+ @Override
+ public List<String> getSegments(BrokerRequest brokerRequest) {
+ return List.of();
+ }
+
+ @Override
+ public boolean routingExists(String tableNameWithType) {
+ return _partitionInfoByTable.containsKey(tableNameWithType);
+ }
+
+ @Nullable
+ @Override
+ public TimeBoundaryInfo getTimeBoundaryInfo(String offlineTableName) {
+ return _timeBoundaryInfo;
+ }
+
+ @Nullable
+ @Override
+ public TablePartitionInfo getTablePartitionInfo(String tableNameWithType) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public TablePartitionReplicatedServersInfo
getTablePartitionReplicatedServersInfo(String tableNameWithType) {
+ return _partitionInfoByTable.get(tableNameWithType);
+ }
+
+ @Override
+ public Set<String> getServingInstances(String tableNameWithType) {
+ return new HashSet<>(_enabledServers.keySet());
+ }
+
+ @Override
+ public boolean isTableDisabled(String tableNameWithType) {
+ return false;
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]