This is an automated email from the ASF dual-hosted git repository.
raghavyadav01 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 66eb74d69d1 Add requiresFetch flag to a segment to for prefetch
planning (#20068)
66eb74d69d1 is described below
commit 66eb74d69d185ae99772b5a41cb6acef2aa583b4
Author: Jhow <[email protected]>
AuthorDate: Sat Oct 31 09:22:41 2026 +0800
Add requiresFetch flag to a segment to for prefetch planning (#20068)
* add requiresFetch flag to a segment to skip prefetch planning
* do run planner for each segment, let the planner decide to skip
* rework
* nit
* verify fetch lifecycle in test, add test for streaming
* Address review nits in InstancePlanMakerPrefetchTest
---
.../core/plan/maker/InstancePlanMakerImplV2.java | 74 +++---
.../pinot/core/query/prefetch/FetchPlanner.java | 10 +-
.../query/pruner/BloomFilterSegmentPruner.java | 5 +-
.../plan/maker/InstancePlanMakerPrefetchTest.java | 283 +++++++++++++++++++++
.../query/pruner/BloomFilterSegmentPrunerTest.java | 50 ++++
.../immutable/ImmutableSegmentImpl.java | 5 +
.../org/apache/pinot/segment/spi/IndexSegment.java | 8 +
.../pinot/segment/spi/store/SegmentDirectory.java | 8 +
8 files changed, 402 insertions(+), 41 deletions(-)
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/plan/maker/InstancePlanMakerImplV2.java
b/pinot-core/src/main/java/org/apache/pinot/core/plan/maker/InstancePlanMakerImplV2.java
index 464bbd77897..5f5911e51c4 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/plan/maker/InstancePlanMakerImplV2.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/plan/maker/InstancePlanMakerImplV2.java
@@ -30,6 +30,7 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
+import java.util.function.Function;
import javax.annotation.Nullable;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
@@ -188,6 +189,29 @@ public class InstancePlanMakerImplV2 implements PlanMaker {
_groupByTrimThreshold = groupByTrimThreshold;
}
+ /// Adds one plan node per segment to `planNodes`. With prefetch enabled, a
segment for which the [FetchPlanner]
+ /// plans a [FetchContext] gets a plan node that acquires and releases its
buffers around execution, and is added to
+ /// `fetchSegmentContexts` with its context added to `fetchContexts` at the
same position. A segment for which the
+ /// planner returns null (it needs no fetch) gets the plain plan node, as
without prefetch. The fetch lists are only
+ /// written with prefetch enabled, so they can be immutable empty lists
otherwise.
+ private void addSegmentPlanNodes(List<SegmentContext> segmentContexts,
QueryContext queryContext,
+ Function<SegmentContext, PlanNode> segmentPlanNodeMaker, List<PlanNode>
planNodes,
+ List<SegmentContext> fetchSegmentContexts, List<FetchContext>
fetchContexts) {
+ boolean enablePrefetch = queryContext.isEnablePrefetch();
+ for (SegmentContext segmentContext : segmentContexts) {
+ FetchContext fetchContext =
+ enablePrefetch ?
_fetchPlanner.planFetchForProcessing(segmentContext.getIndexSegment(),
queryContext) : null;
+ if (fetchContext != null) {
+ fetchSegmentContexts.add(segmentContext);
+ fetchContexts.add(fetchContext);
+ planNodes.add(new
AcquireReleaseColumnsSegmentPlanNode(segmentPlanNodeMaker.apply(segmentContext),
+ segmentContext, fetchContext));
+ } else {
+ planNodes.add(segmentPlanNodeMaker.apply(segmentContext));
+ }
+ }
+ }
+
@Override
public Plan makeInstancePlan(List<SegmentContext> segmentContexts,
QueryContext queryContext,
ExecutorService executorService) {
@@ -197,27 +221,15 @@ public class InstancePlanMakerImplV2 implements PlanMaker
{
int numSegments = segmentContexts.size();
List<PlanNode> planNodes = new ArrayList<>(numSegments);
- List<FetchContext> fetchContexts;
- if (queryContext.isEnablePrefetch()) {
- fetchContexts = new ArrayList<>(numSegments);
- for (SegmentContext segmentContext : segmentContexts) {
- FetchContext fetchContext =
-
_fetchPlanner.planFetchForProcessing(segmentContext.getIndexSegment(),
queryContext);
- fetchContexts.add(fetchContext);
- planNodes.add(
- new
AcquireReleaseColumnsSegmentPlanNode(makeSegmentPlanNode(segmentContext,
queryContext), segmentContext,
- fetchContext));
- }
- } else {
- fetchContexts = List.of();
- for (SegmentContext segmentContext : segmentContexts) {
- planNodes.add(makeSegmentPlanNode(segmentContext, queryContext));
- }
- }
+ boolean enablePrefetch = queryContext.isEnablePrefetch();
+ List<SegmentContext> fetchSegmentContexts = enablePrefetch ? new
ArrayList<>(numSegments) : List.of();
+ List<FetchContext> fetchContexts = enablePrefetch ? new
ArrayList<>(numSegments) : List.of();
+ addSegmentPlanNodes(segmentContexts, queryContext, segmentContext ->
makeSegmentPlanNode(segmentContext,
+ queryContext), planNodes, fetchSegmentContexts, fetchContexts);
CombinePlanNode combinePlanNode = createCombinePlanNode(planNodes,
queryContext, executorService, null);
return new GlobalPlanImplV0(
- new InstanceResponsePlanNode(combinePlanNode, segmentContexts,
fetchContexts, queryContext));
+ new InstanceResponsePlanNode(combinePlanNode, fetchSegmentContexts,
fetchContexts, queryContext));
}
/// Resolves [AndRestrictionPushdownMode] for a query.
@@ -403,27 +415,15 @@ public class InstancePlanMakerImplV2 implements PlanMaker
{
int numSegments = segmentContexts.size();
List<PlanNode> planNodes = new ArrayList<>(numSegments);
- List<FetchContext> fetchContexts;
- if (queryContext.isEnablePrefetch()) {
- fetchContexts = new ArrayList<>(numSegments);
- for (SegmentContext segmentContext : segmentContexts) {
- FetchContext fetchContext =
-
_fetchPlanner.planFetchForProcessing(segmentContext.getIndexSegment(),
queryContext);
- fetchContexts.add(fetchContext);
- planNodes.add(
- new
AcquireReleaseColumnsSegmentPlanNode(makeStreamingSegmentPlanNode(segmentContext,
queryContext),
- segmentContext, fetchContext));
- }
- } else {
- fetchContexts = List.of();
- for (SegmentContext segmentContext : segmentContexts) {
- planNodes.add(makeStreamingSegmentPlanNode(segmentContext,
queryContext));
- }
- }
+ boolean enablePrefetch = queryContext.isEnablePrefetch();
+ List<SegmentContext> fetchSegmentContexts = enablePrefetch ? new
ArrayList<>(numSegments) : List.of();
+ List<FetchContext> fetchContexts = enablePrefetch ? new
ArrayList<>(numSegments) : List.of();
+ addSegmentPlanNodes(segmentContexts, queryContext, segmentContext ->
makeStreamingSegmentPlanNode(segmentContext,
+ queryContext), planNodes, fetchSegmentContexts, fetchContexts);
CombinePlanNode combinePlanNode = createCombinePlanNode(planNodes,
queryContext, executorService, streamer);
- return new GlobalPlanImplV0(
- new StreamingInstanceResponsePlanNode(combinePlanNode,
segmentContexts, fetchContexts, queryContext, streamer));
+ return new GlobalPlanImplV0(new
StreamingInstanceResponsePlanNode(combinePlanNode, fetchSegmentContexts,
+ fetchContexts, queryContext, streamer));
}
@Override
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/prefetch/FetchPlanner.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/prefetch/FetchPlanner.java
index e320f01812b..cbeb4f409da 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/prefetch/FetchPlanner.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/prefetch/FetchPlanner.java
@@ -18,6 +18,7 @@
*/
package org.apache.pinot.core.query.prefetch;
+import javax.annotation.Nullable;
import org.apache.pinot.core.query.request.context.QueryContext;
import org.apache.pinot.segment.spi.FetchContext;
import org.apache.pinot.segment.spi.IndexSegment;
@@ -27,6 +28,9 @@ import org.apache.pinot.segment.spi.IndexSegment;
/// read from some slow storage backend, to reduce the query latency. Although
the plan methods take in an indexSegment
/// object, it should mainly access the segment metadata like what types of
index it has, as the index data may not be
/// available, but leaving this to the implementations to check and handle per
their own need.
+///
+/// A planner can return null for a segment that needs no fetch, e.g. one
whose data is all local, so its fetch calls
+/// would be no-ops. The caller then skips the fetch lifecycle (prefetch,
acquire, release) for that segment entirely.
/// TODO: this interface and the dependent classes like QueryContext should be
moved to query-spi pkg, yet to be added.
public interface FetchPlanner {
/// Plan what index data to prefetch to help prune the segment before
processing it. For example, one can fetch bloom
@@ -34,7 +38,8 @@ public interface FetchPlanner {
///
/// @param indexSegment segment to be pruned.
/// @param queryContext context extracted from the query.
- /// @return context to guide data prefetching.
+ /// @return context to guide data prefetching, or null if the segment needs
no fetch.
+ @Nullable
FetchContext planFetchForPruning(IndexSegment indexSegment, QueryContext
queryContext);
/// Plan what index data to prefetch to process it. For example, one can
fetch all types of index for columns tracked
@@ -42,6 +47,7 @@ public interface FetchPlanner {
///
/// @param indexSegment segment to be processed.
/// @param queryContext context extracted from the query.
- /// @return context to guide data prefetching.
+ /// @return context to guide data prefetching, or null if the segment needs
no fetch.
+ @Nullable
FetchContext planFetchForProcessing(IndexSegment indexSegment, QueryContext
queryContext);
}
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/query/pruner/BloomFilterSegmentPruner.java
b/pinot-core/src/main/java/org/apache/pinot/core/query/pruner/BloomFilterSegmentPruner.java
index d3b6d00b363..388f7e9ec2d 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/query/pruner/BloomFilterSegmentPruner.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/query/pruner/BloomFilterSegmentPruner.java
@@ -89,11 +89,12 @@ public class BloomFilterSegmentPruner extends
ValueBasedSegmentPruner {
int numSegments = segments.size();
FetchContext[] fetchContexts = new FetchContext[numSegments];
try {
- // Prefetch bloom filter for columns within the EQ/IN predicate if exists
+ // Prefetch bloom filter for columns within the EQ/IN predicate if
exists. The planner returns null for a segment
+ // that needs no fetch, which is then pruned without the fetch lifecycle.
for (int i = 0; i < numSegments; i++) {
IndexSegment segment = segments.get(i);
FetchContext fetchContext = _fetchPlanner.planFetchForPruning(segment,
query);
- if (!fetchContext.isEmpty()) {
+ if (fetchContext != null && !fetchContext.isEmpty()) {
segment.prefetch(fetchContext);
fetchContexts[i] = fetchContext;
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/plan/maker/InstancePlanMakerPrefetchTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/plan/maker/InstancePlanMakerPrefetchTest.java
new file mode 100644
index 00000000000..e72da4411b9
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/plan/maker/InstancePlanMakerPrefetchTest.java
@@ -0,0 +1,283 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.plan.maker;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.apache.commons.io.FileUtils;
+import org.apache.pinot.common.datatable.DataTable;
+import org.apache.pinot.core.operator.blocks.InstanceResponseBlock;
+import org.apache.pinot.core.operator.blocks.results.SelectionResultsBlock;
+import org.apache.pinot.core.query.executor.ResultsBlockStreamer;
+import org.apache.pinot.core.query.prefetch.DefaultFetchPlanner;
+import org.apache.pinot.core.query.prefetch.FetchPlanner;
+import org.apache.pinot.core.query.prefetch.FetchPlannerRegistry;
+import org.apache.pinot.core.query.request.context.QueryContext;
+import
org.apache.pinot.core.query.request.context.utils.QueryContextConverterUtils;
+import
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentLoader;
+import
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
+import org.apache.pinot.segment.local.segment.index.loader.IndexLoadingConfig;
+import org.apache.pinot.segment.local.segment.readers.GenericRowRecordReader;
+import org.apache.pinot.segment.spi.FetchContext;
+import org.apache.pinot.segment.spi.ImmutableSegment;
+import org.apache.pinot.segment.spi.IndexSegment;
+import org.apache.pinot.segment.spi.SegmentContext;
+import org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.query.QueryThreadContext;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+
+/// With prefetch enabled, [InstancePlanMakerImplV2] runs the fetch lifecycle
only for segments for which the
+/// [FetchPlanner] plans a fetch context. A segment for which the planner
returns null (it needs no fetch) is planned
+/// and executed as without prefetch.
+public class InstancePlanMakerPrefetchTest {
+ private static final String TABLE_NAME = "testTable";
+ private static final String SEGMENT_NAME = "testSegment";
+ private static final int NUM_DOCS = 100;
+ // Every tenth document matches
+ private static final String QUERY = "SELECT COUNT(*), SUM(m) FROM testTable
WHERE d = 'v3'";
+ // A selection-only query is streamed; the limit keeps every matching row of
both segments
+ private static final String STREAMING_QUERY = "SELECT m FROM testTable WHERE
d = 'v3' LIMIT 1000";
+ private static final long MATCHES_PER_SEGMENT = NUM_DOCS / 10;
+ private static final double SUM_PER_SEGMENT = 3 + 13 + 23 + 33 + 43 + 53 +
63 + 73 + 83 + 93;
+
+ private File _tempDir;
+ private ImmutableSegment _segment;
+ private ExecutorService _executorService;
+
+ @BeforeClass
+ public void setUp()
+ throws Exception {
+ _tempDir =
Files.createTempDirectory("instance-plan-maker-prefetch-test").toFile();
+ Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+ .addSingleValueDimension("d", DataType.STRING)
+ .addMetric("m", DataType.LONG)
+ .build();
+ TableConfig tableConfig = new
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME).build();
+ List<GenericRow> rows = new ArrayList<>(NUM_DOCS);
+ for (int i = 0; i < NUM_DOCS; i++) {
+ GenericRow row = new GenericRow();
+ row.putValue("d", "v" + (i % 10));
+ row.putValue("m", (long) i);
+ rows.add(row);
+ }
+ SegmentGeneratorConfig config = new SegmentGeneratorConfig(tableConfig,
schema);
+ config.setTableName(TABLE_NAME);
+ config.setSegmentName(SEGMENT_NAME);
+ config.setOutDir(_tempDir.getAbsolutePath());
+ SegmentIndexCreationDriverImpl driver = new
SegmentIndexCreationDriverImpl();
+ driver.init(config, new GenericRowRecordReader(rows));
+ driver.build();
+ _segment =
+ ImmutableSegmentLoader.load(new File(_tempDir, SEGMENT_NAME), new
IndexLoadingConfig(tableConfig, schema));
+ _executorService =
QueryThreadContext.contextAwareExecutorService(Executors.newFixedThreadPool(2));
+ }
+
+ @AfterClass
+ public void tearDown() {
+ _executorService.shutdownNow();
+ _segment.destroy();
+ FileUtils.deleteQuietly(_tempDir);
+ }
+
+ @Test
+ public void testPrefetchRunsFetchLifecycleOnlyForPlannedSegments()
+ throws Exception {
+ IndexSegment local = spy(_segment);
+ IndexSegment remote = spy(_segment);
+
+ assertResult(execute(plannerSkipping(Set.of(local)), List.of(local,
remote), true), 2);
+
+ verifyFetchLifecycle(remote);
+ verifyNoFetch(local);
+ }
+
+ /// Streaming selection has its own start/stop and per-segment fetch
lifecycle; only the remote segment gets it.
+ @Test
+ public void testStreamingSelectionRunsFetchLifecycleOnlyForPlannedSegments()
+ throws Exception {
+ IndexSegment local = spy(_segment);
+ IndexSegment remote = spy(_segment);
+
+ List<Long> streamedValues =
executeStreaming(plannerSkipping(Set.of(local)), List.of(local, remote));
+
+ // Each segment streams every matching value once
+ List<Long> expectedValues = new ArrayList<>();
+ for (long value = 3; value < NUM_DOCS; value += 10) {
+ expectedValues.add(value);
+ expectedValues.add(value);
+ }
+ assertEquals(streamedValues, expectedValues);
+ verifyFetchLifecycle(remote);
+ verifyNoFetch(local);
+ }
+
+ @Test
+ public void testPrefetchWithoutPlannedSegmentsRunsNoFetchLifecycle()
+ throws Exception {
+ IndexSegment first = spy(_segment);
+ IndexSegment second = spy(_segment);
+
+ assertResult(execute(plannerSkipping(Set.of(first, second)),
List.of(first, second), true), 2);
+
+ verifyNoFetch(first);
+ verifyNoFetch(second);
+ }
+
+ @Test
+ public void testDisabledPrefetchRunsNoFetchLifecycle()
+ throws Exception {
+ IndexSegment segment = spy(_segment);
+
+ assertResult(execute(plannerSkipping(Set.of()), List.of(segment), false),
1);
+
+ verifyNoFetch(segment);
+ }
+
+ /// The default planner never returns null, so every segment keeps the fetch
lifecycle as before.
+ @Test
+ public void testDefaultPlannerRunsFetchLifecycleForEverySegment()
+ throws Exception {
+ IndexSegment first = spy(_segment);
+ IndexSegment second = spy(_segment);
+
+ assertResult(execute(new DefaultFetchPlanner(), List.of(first, second),
true), 2);
+
+ for (IndexSegment segment : List.of(first, second)) {
+ verify(segment).prefetch(any());
+ verify(segment).acquire(any());
+ verify(segment, atLeastOnce()).release(any());
+ }
+ }
+
+ /// A planner that plans with the default planner, except for the given
segments, which need no fetch.
+ private static FetchPlanner plannerSkipping(Set<IndexSegment>
segmentsWithoutFetch) {
+ FetchPlanner defaultPlanner = new DefaultFetchPlanner();
+ return new FetchPlanner() {
+ @Override
+ public FetchContext planFetchForPruning(IndexSegment indexSegment,
QueryContext queryContext) {
+ return segmentsWithoutFetch.contains(indexSegment) ? null
+ : defaultPlanner.planFetchForPruning(indexSegment, queryContext);
+ }
+
+ @Override
+ public FetchContext planFetchForProcessing(IndexSegment indexSegment,
QueryContext queryContext) {
+ return segmentsWithoutFetch.contains(indexSegment) ? null
+ : defaultPlanner.planFetchForProcessing(indexSegment,
queryContext);
+ }
+ };
+ }
+
+ private DataTable execute(FetchPlanner fetchPlanner, List<IndexSegment>
segments, boolean enablePrefetch)
+ throws Exception {
+ QueryContext queryContext =
QueryContextConverterUtils.getQueryContext(QUERY);
+ queryContext.setEndTimeMs(System.currentTimeMillis() + 60_000);
+ queryContext.setEnablePrefetch(enablePrefetch);
+ List<SegmentContext> segmentContexts = new ArrayList<>(segments.size());
+ for (IndexSegment segment : segments) {
+ segmentContexts.add(new SegmentContext(segment));
+ }
+ try (QueryThreadContext ignored = QueryThreadContext.openForSseTest()) {
+ InstanceResponseBlock responseBlock = planMakerWith(fetchPlanner)
+ .makeInstancePlan(segmentContexts, queryContext,
_executorService).execute();
+ assertTrue(responseBlock.getExceptions().isEmpty(), "Query failed: " +
responseBlock.getExceptions());
+ return responseBlock.toDataTable();
+ }
+ }
+
+ /// Runs the streaming selection with prefetch enabled, and returns the
streamed values of `m` in ascending order.
+ private List<Long> executeStreaming(FetchPlanner fetchPlanner,
List<IndexSegment> segments)
+ throws Exception {
+ QueryContext queryContext =
QueryContextConverterUtils.getQueryContext(STREAMING_QUERY);
+ queryContext.setEndTimeMs(System.currentTimeMillis() + 60_000);
+ queryContext.setEnablePrefetch(true);
+ List<SegmentContext> segmentContexts = new ArrayList<>(segments.size());
+ for (IndexSegment segment : segments) {
+ segmentContexts.add(new SegmentContext(segment));
+ }
+ // The streamer is invoked on the response thread, not by the combine
workers
+ List<Long> streamedValues = new ArrayList<>();
+ ResultsBlockStreamer streamer = block -> {
+ for (Object[] row : ((SelectionResultsBlock) block).getRows()) {
+ streamedValues.add(((Number) row[0]).longValue());
+ }
+ };
+ try (QueryThreadContext ignored = QueryThreadContext.openForSseTest()) {
+ InstanceResponseBlock responseBlock = planMakerWith(fetchPlanner)
+ .makeStreamingInstancePlan(segmentContexts, queryContext,
_executorService, streamer).execute();
+ assertTrue(responseBlock.getExceptions().isEmpty(), "Query failed: " +
responseBlock.getExceptions());
+ }
+ Collections.sort(streamedValues);
+ return streamedValues;
+ }
+
+ /// The plan maker takes the registered planner at construction.
+ private static InstancePlanMakerImplV2 planMakerWith(FetchPlanner
fetchPlanner) {
+ try (MockedStatic<FetchPlannerRegistry> registry =
mockStatic(FetchPlannerRegistry.class)) {
+ registry.when(FetchPlannerRegistry::getPlanner).thenReturn(fetchPlanner);
+ return new InstancePlanMakerImplV2();
+ }
+ }
+
+ private static void assertResult(DataTable dataTable, int numSegments) {
+ assertEquals(dataTable.getNumberOfRows(), 1);
+ assertEquals(dataTable.getLong(0, 0), MATCHES_PER_SEGMENT * numSegments);
+ assertEquals(dataTable.getDouble(0, 1), SUM_PER_SEGMENT * numSegments);
+ }
+
+ /// The segment is prefetched, acquired and released with one fetch context
planned for the filter column.
+ private static void verifyFetchLifecycle(IndexSegment segment) {
+ ArgumentCaptor<FetchContext> fetchContext =
ArgumentCaptor.forClass(FetchContext.class);
+ verify(segment).prefetch(fetchContext.capture());
+ verify(segment).acquire(fetchContext.getValue());
+ verify(segment, atLeastOnce()).release(fetchContext.getValue());
+
assertTrue(fetchContext.getValue().getColumnToIndexList().containsKey("d"));
+ }
+
+ private static void verifyNoFetch(IndexSegment segment) {
+ verify(segment, never()).prefetch(any());
+ verify(segment, never()).acquire(any());
+ verify(segment, never()).release(any());
+ }
+}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/query/pruner/BloomFilterSegmentPrunerTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/query/pruner/BloomFilterSegmentPrunerTest.java
index d7b31670c16..fba0bde5e1d 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/query/pruner/BloomFilterSegmentPrunerTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/query/pruner/BloomFilterSegmentPrunerTest.java
@@ -37,6 +37,9 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.pinot.core.query.prefetch.DefaultFetchPlanner;
+import org.apache.pinot.core.query.prefetch.FetchPlanner;
+import org.apache.pinot.core.query.prefetch.FetchPlannerRegistry;
import org.apache.pinot.core.query.request.context.QueryContext;
import
org.apache.pinot.core.query.request.context.utils.QueryContextConverterUtils;
import
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentLoader;
@@ -66,6 +69,7 @@ import org.apache.pinot.spi.query.RemoteAccessStats;
import org.apache.pinot.spi.utils.ReadMode;
import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -74,6 +78,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -369,6 +374,51 @@ public class BloomFilterSegmentPrunerTest {
}
}
+ @Test
+ public void testPrefetchSkipsSegmentsWithoutPlannedFetch()
+ throws Exception {
+ // The planner returns null for the local segments (no fetch needed): they
are pruned without any fetch call, while
+ // the remote segment keeps the lifecycle
+ IndexSegment localKept = mockIndexSegment(new String[]{"1.0"});
+ IndexSegment localPruned = mockIndexSegment(new String[]{"2.0"});
+ IndexSegment remoteKept = mockIndexSegment(new String[]{"1.0"});
+ Set<IndexSegment> localSegments = Set.of(localKept, localPruned);
+ FetchPlanner defaultPlanner = new DefaultFetchPlanner();
+ FetchPlanner fetchPlanner = new FetchPlanner() {
+ @Override
+ public FetchContext planFetchForPruning(IndexSegment indexSegment,
QueryContext queryContext) {
+ return localSegments.contains(indexSegment) ? null
+ : defaultPlanner.planFetchForPruning(indexSegment, queryContext);
+ }
+
+ @Override
+ public FetchContext planFetchForProcessing(IndexSegment indexSegment,
QueryContext queryContext) {
+ return localSegments.contains(indexSegment) ? null
+ : defaultPlanner.planFetchForProcessing(indexSegment,
queryContext);
+ }
+ };
+ BloomFilterSegmentPruner pruner = new BloomFilterSegmentPruner();
+ // The pruner takes the registered planner at init
+ try (MockedStatic<FetchPlannerRegistry> registry =
mockStatic(FetchPlannerRegistry.class)) {
+ registry.when(FetchPlannerRegistry::getPlanner).thenReturn(fetchPlanner);
+ pruner.init(new PinotConfiguration());
+ }
+
+ assertEquals(pruner.prune(List.of(localKept, localPruned, remoteKept),
prefetchQuery()),
+ List.of(localKept, remoteKept));
+ for (IndexSegment local : localSegments) {
+ verify(local, never()).prefetch(any(FetchContext.class));
+ verify(local, never()).acquire(any(FetchContext.class));
+ verify(local, never()).release(any(FetchContext.class));
+ }
+ assertReleasedPrefetch(remoteKept, 1);
+
+ // With only local segments, pruning is unchanged and still makes no fetch
call
+ assertEquals(pruner.prune(List.of(localKept, localPruned),
prefetchQuery()), List.of(localKept));
+ verify(localKept, never()).prefetch(any(FetchContext.class));
+ verify(localPruned, never()).prefetch(any(FetchContext.class));
+ }
+
private QueryContext prefetchQuery() {
QueryContext query = QueryContextConverterUtils.getQueryContext(
"SELECT COUNT(*) FROM testTable WHERE column = 1.0");
diff --git
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImpl.java
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImpl.java
index b0ac2d625dd..01540afff9e 100644
---
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImpl.java
+++
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImpl.java
@@ -341,6 +341,11 @@ public class ImmutableSegmentImpl implements
ImmutableSegment {
return _segmentMetadata.getSchema().getPhysicalColumnNames();
}
+ @Override
+ public boolean requiresFetch() {
+ return _segmentDirectory.requiresFetch();
+ }
+
@Override
public void prefetch(FetchContext fetchContext) {
_segmentDirectory.prefetch(fetchContext);
diff --git
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/IndexSegment.java
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/IndexSegment.java
index dcc6605f312..b7a45dcf323 100644
---
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/IndexSegment.java
+++
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/IndexSegment.java
@@ -122,6 +122,14 @@ public interface IndexSegment {
/// values from the same segment.
Object getValue(int docId, String column);
+ /// Returns whether this segment serves buffers through [#prefetch],
[#acquire] and [#release]. When it returns false,
+ /// those calls are no-ops, so a fetch planner can skip planning a
[FetchContext] for the segment, letting query
+ /// execution skip the fetch lifecycle altogether. Segments that override
the fetch calls must return true. The value
+ /// must not change over the lifetime of the segment.
+ default boolean requiresFetch() {
+ return false;
+ }
+
/// Hints the segment to begin prefetching buffers for specified columns.
/// Typically, this should be an async call made before operating on the
segment.
/// @param fetchContext context for this segment's fetch
diff --git
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/store/SegmentDirectory.java
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/store/SegmentDirectory.java
index 8a4267f949f..2cda5a5bea3 100644
---
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/store/SegmentDirectory.java
+++
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/store/SegmentDirectory.java
@@ -110,6 +110,14 @@ public abstract class SegmentDirectory implements
Closeable {
/// @return a set of columns with such index type.
public abstract Set<String> getColumnsWithIndex(IndexType<?, ?, ?> type);
+ /// Returns whether this segment directory serves buffers through
[#prefetch], [#acquire] and [#release]. When it
+ /// returns false, those calls are no-ops, so a fetch planner can skip
planning a [FetchContext] for the segment,
+ /// letting query execution skip the fetch lifecycle altogether. Directories
that override the fetch calls must
+ /// return true. The value must not change over the lifetime of the
directory.
+ public boolean requiresFetch() {
+ return false;
+ }
+
/// This is a hint to the segment directory, to begin prefetching buffers
for given context.
/// Typically, this should be an async call made before operating on the
segment.
/// @param fetchContext context for this segment's fetch
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]