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

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


The following commit(s) were added to refs/heads/master by this push:
     new fec6be9e8a [core] Parallelize primary-key sorted-index scan (#9231)
fec6be9e8a is described below

commit fec6be9e8ac5f26cf224bf1950384c81a00005c2
Author: QuakeWang <[email protected]>
AuthorDate: Sat Aug 15 20:12:08 2026 +0800

    [core] Parallelize primary-key sorted-index scan (#9231)
---
 .../paimon/globalindex/GlobalIndexEvaluator.java   | 12 ++-
 .../globalindex/GlobalIndexEvaluatorTest.java      | 27 +++++++
 .../table/source/PrimaryKeySortedIndexScan.java    | 38 +++++++++-
 .../source/PrimaryKeySortedIndexScanTest.java      | 85 ++++++++++++++++++++++
 4 files changed, 157 insertions(+), 5 deletions(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexEvaluator.java
 
b/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexEvaluator.java
index a23bd2c76a..f4a55cfbec 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexEvaluator.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexEvaluator.java
@@ -69,10 +69,18 @@ public class GlobalIndexEvaluator implements Closeable {
     }
 
     public Optional<GlobalIndexResult> evaluate(@Nullable Predicate predicate) 
{
+        return await(evaluateAsync(predicate));
+    }
+
+    /**
+     * Evaluate the predicate asynchronously. Keep this evaluator open until 
the future completes.
+     */
+    public CompletableFuture<Optional<GlobalIndexResult>> evaluateAsync(
+            @Nullable Predicate predicate) {
         if (predicate == null) {
-            return Optional.empty();
+            return CompletableFuture.completedFuture(Optional.empty());
         }
-        return await(visitAsync(predicate)).map(Evaluation::result);
+        return visitAsync(predicate).thenApply(result -> 
result.map(Evaluation::result));
     }
 
     /** Evaluate the predicate and return the fields whose supported indexes 
contributed. */
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexEvaluatorTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexEvaluatorTest.java
index 8b7d677ed3..ec193fd5e9 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexEvaluatorTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexEvaluatorTest.java
@@ -101,6 +101,33 @@ class GlobalIndexEvaluatorTest {
         evaluator.close();
     }
 
+    @Test
+    void testEvaluateAsyncDoesNotWaitForReaderResult() {
+        RowType rowType = rowType();
+        CompletableFuture<Optional<GlobalIndexResult>> readerResult = new 
CompletableFuture<>();
+        GlobalIndexEvaluator evaluator =
+                new GlobalIndexEvaluator(
+                        rowType,
+                        fieldId ->
+                                Collections.singletonList(
+                                        new StubGlobalIndexReader(null) {
+                                            @Override
+                                            public 
CompletableFuture<Optional<GlobalIndexResult>>
+                                                    visitEqual(FieldRef 
fieldRef, Object literal) {
+                                                return readerResult;
+                                            }
+                                        }));
+        Predicate predicate = new PredicateBuilder(rowType).equal(0, 42);
+
+        CompletableFuture<Optional<GlobalIndexResult>> result = 
evaluator.evaluateAsync(predicate);
+
+        assertThat(result.isDone()).isFalse();
+        readerResult.complete(Optional.of(resultOf(1, 2, 3)));
+        assertThat(result.join()).isPresent();
+        assertBitmapContainsExactly(result.join().get().results(), 1L, 2L, 3L);
+        evaluator.close();
+    }
+
     @Test
     void testTopNUsesAggregatedReaderAndReusesPredicateCache() {
         RowType rowType = rowType();
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeySortedIndexScan.java
 
b/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeySortedIndexScan.java
index cb03736b7b..b1458eadbc 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeySortedIndexScan.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/table/source/PrimaryKeySortedIndexScan.java
@@ -42,6 +42,7 @@ import org.apache.paimon.predicate.FieldRef;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.utils.FileStorePathFactory;
+import org.apache.paimon.utils.FutureUtils;
 import org.apache.paimon.utils.IOUtils;
 import org.apache.paimon.utils.IndexFilePathFactories;
 import org.apache.paimon.utils.Pair;
@@ -65,6 +66,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.function.Supplier;
 
@@ -246,6 +248,8 @@ public final class PrimaryKeySortedIndexScan {
         }
 
         Map<PkSortedIndexGroup, SharedGlobalIndexReader> sharedReaders = new 
IdentityHashMap<>();
+        List<GlobalIndexEvaluator> evaluators = new ArrayList<>();
+        List<CompletableFuture<Optional<GlobalIndexResult>>> resultFutures = 
new ArrayList<>();
         List<EvaluatedFile> files = new ArrayList<>();
         try {
             for (FilePlan file : plan.files()) {
@@ -277,9 +281,20 @@ public final class PrimaryKeySortedIndexScan {
                                     }
                                     return 
Collections.singletonList(fileLocalReader(file, reader));
                                 });
+                evaluators.add(evaluator);
+                try {
+                    resultFutures.add(evaluator.evaluateAsync(predicate));
+                } catch (RuntimeException e) {
+                    rethrowIfInterrupted(e);
+                    resultFutures.add(FutureUtils.completedExceptionally(e));
+                }
+            }
+
+            for (int i = 0; i < plan.files().size(); i++) {
+                FilePlan file = plan.files().get(i);
                 Optional<GlobalIndexResult> result;
                 try {
-                    result = evaluator.evaluate(predicate);
+                    result = awaitEvaluation(resultFutures.get(i));
                 } catch (RuntimeException e) {
                     rethrowIfInterrupted(e);
                     LOG.warn(
@@ -288,17 +303,34 @@ public final class PrimaryKeySortedIndexScan {
                             file.dataFile().fileName(),
                             e);
                     result = Optional.empty();
-                } finally {
-                    evaluator.close();
                 }
                 files.add(new EvaluatedFile(file, result));
             }
         } finally {
+            IOUtils.closeAllQuietly(evaluators);
             IOUtils.closeAllQuietly(sharedReaders.values());
         }
         return new EvaluatedPlan(plan.snapshotId(), files);
     }
 
+    private static Optional<GlobalIndexResult> awaitEvaluation(
+            CompletableFuture<Optional<GlobalIndexResult>> future) {
+        try {
+            return future.get();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException("Interrupted during index evaluation", 
e);
+        } catch (ExecutionException e) {
+            if (e.getCause() instanceof RuntimeException) {
+                throw (RuntimeException) e.getCause();
+            }
+            if (e.getCause() instanceof Error) {
+                throw (Error) e.getCause();
+            }
+            throw new RuntimeException(e.getCause());
+        }
+    }
+
     private static GlobalIndexReader fileLocalReader(
             FilePlan file, SharedGlobalIndexReader reader) {
         DataFileMeta dataFile = file.dataFile();
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeySortedIndexScanTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeySortedIndexScanTest.java
index b800fb1c61..9281ec395a 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeySortedIndexScanTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/source/PrimaryKeySortedIndexScanTest.java
@@ -54,6 +54,11 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -69,6 +74,86 @@ import static org.mockito.Mockito.when;
 /** Tests source-backed BTree and Bitmap planning in file-local row-position 
space. */
 class PrimaryKeySortedIndexScanTest {
 
+    @Test
+    void testStartsIndependentGroupsBeforeWaitingForResults() throws Exception 
{
+        DataSplit firstSplit = dataSplit(11, 0, dataFile("data-1", 4));
+        DataSplit secondSplit = dataSplit(11, 1, dataFile("data-2", 4));
+        PrimaryKeyIndexDefinition definition =
+                definition(
+                        7,
+                        BTreeGlobalIndexerFactory.IDENTIFIER,
+                        PrimaryKeyIndexDefinition.Family.BTREE);
+        PrimaryKeySortedIndexScan.Plan plan =
+                PrimaryKeySortedIndexScan.plan(
+                        11,
+                        Arrays.asList(firstSplit, secondSplit),
+                        Collections.singletonList(definition),
+                        Arrays.asList(
+                                payloadEntry(0, payload("btree-0", "data-1", 
4, "btree", 7, 4)),
+                                payloadEntry(1, payload("btree-1", "data-2", 
4, "btree", 7, 4))));
+        assertThat(plan.files()).hasSize(2);
+        assertThat(plan.files()).allSatisfy(file -> 
assertThat(file.group(7)).isPresent());
+
+        RowType rowType = RowType.of(new DataField(7, "f7", DataTypes.INT()));
+        Predicate predicate = new PredicateBuilder(rowType).equal(0, 42);
+        CompletableFuture<Optional<GlobalIndexResult>> firstResult = new 
CompletableFuture<>();
+        CompletableFuture<Optional<GlobalIndexResult>> secondResult = new 
CompletableFuture<>();
+        CountDownLatch firstStarted = new CountDownLatch(1);
+        CountDownLatch secondStarted = new CountDownLatch(1);
+        GlobalIndexReader firstReader = mock(GlobalIndexReader.class);
+        when(firstReader.visitEqual(any(), eq(42)))
+                .thenAnswer(
+                        ignored -> {
+                            firstStarted.countDown();
+                            return firstResult;
+                        });
+        GlobalIndexReader secondReader = mock(GlobalIndexReader.class);
+        when(secondReader.visitEqual(any(), eq(42)))
+                .thenAnswer(
+                        ignored -> {
+                            secondStarted.countDown();
+                            return secondResult;
+                        });
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+
+        try {
+            Future<PrimaryKeySortedIndexScan.EvaluatedPlan> evaluated =
+                    executor.submit(
+                            () ->
+                                    PrimaryKeySortedIndexScan.evaluate(
+                                            plan,
+                                            rowType,
+                                            predicate,
+                                            
Collections.singletonList(definition),
+                                            (file,
+                                                    ignoredDefinition,
+                                                    ignoredPayloads,
+                                                    ignoredTotalRowCount) ->
+                                                    
file.dataFile().fileName().equals("data-1")
+                                                            ? firstReader
+                                                            : secondReader));
+
+            assertThat(firstStarted.await(5, TimeUnit.SECONDS)).isTrue();
+            boolean secondStartedBeforeFirstCompleted = secondStarted.await(1, 
TimeUnit.SECONDS);
+            firstResult.complete(Optional.of(GlobalIndexResult.createEmpty()));
+            assertThat(secondStarted.await(5, TimeUnit.SECONDS))
+                    .as("the second index group should eventually start")
+                    .isTrue();
+            
secondResult.complete(Optional.of(GlobalIndexResult.createEmpty()));
+            assertThat(evaluated.get(5, TimeUnit.SECONDS).files()).hasSize(2);
+            verify(firstReader).close();
+            verify(secondReader).close();
+
+            assertThat(secondStartedBeforeFirstCompleted)
+                    .as("the second index group should start before the first 
result completes")
+                    .isTrue();
+        } finally {
+            firstResult.completeExceptionally(new RuntimeException("Test 
cleanup."));
+            secondResult.completeExceptionally(new RuntimeException("Test 
cleanup."));
+            executor.shutdownNow();
+        }
+    }
+
     @Test
     void testPayloadStateIsBuiltOncePerBucketAndDefinition() {
         DataSplit split =

Reply via email to