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

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


The following commit(s) were added to refs/heads/master by this push:
     new 695c88b5772 [fix](binlog) Refresh incremental partition versions 
before pruning (#68138)
695c88b5772 is described below

commit 695c88b57723ffd93c6a0978daa8725cb21b2562
Author: Luwei <[email protected]>
AuthorDate: Mon Sep 21 10:45:50 2026 +0800

    [fix](binlog) Refresh incremental partition versions before pruning (#68138)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #67181
    
    Problem Summary:
    
    A cloud incremental read may wait for its target transaction to become
    visible before query planning continues. During that wait, the FE
    `CloudPartition` cache can still contain `PARTITION_INIT_VERSION` for a
    partition even though the transaction has already become visible in
    MetaService.
    
    `PruneEmptyPartition` previously relied only on that cached version. It
    could therefore classify the partition as empty and remove it before
    scan-node planning. The later version refresh in
    `ScanNode.setVisibleVersionForOlapScanNodes` could observe the new
    version, but it was already too late because the partition had been
    pruned from the scan.
    
    This PR changes empty-partition pruning for direct cloud incremental
    reads as follows:
    
    - A partition that is already known to be non-empty from the FE cache is
    retained directly. Partition versions are monotonic, so a cached version
    greater than `PARTITION_INIT_VERSION` remains sufficient for this
    pruning decision.
    - Only cached-empty or unknown candidate partitions are refreshed from
    MetaService. This catches transactions that became visible after the FE
    cache entry was populated while avoiding unnecessary RPC work for known
    non-empty partitions.
    - The refreshed and cached results are combined in the original
    candidate-partition order.
    - Normal reads and incremental reads backed by an `OlapTableWrapper`
    with fixed visible versions continue to use their existing
    snapshot-aware path.
    
    The refresh in `ScanNode.setVisibleVersionForOlapScanNodes` is
    intentionally retained. `PruneEmptyPartition` only needs an
    empty-or-non-empty decision, while scan execution still needs the exact
    snapshot versions of all final selected partitions.
    
    ### Release note
    
    Fix cloud incremental reads that could miss newly visible data because a
    stale FE partition-version cache caused premature empty-partition
    pruning.
    
    ### Check List (For Author)
    
    - Test
        - [ ] Regression test
        - [x] Unit Test
            - `PruneEmptyPartitionTest`
            - `OlapScanNodeTest`
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason
    
    - Behavior changed:
        - [ ] No.
    - [x] Yes. Direct cloud incremental reads refresh cached-empty or
    unknown partition versions from MetaService before empty-partition
    pruning.
    
    - Does this need documentation?
        - [x] No.
        - [ ] Yes.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .../doris/catalog/RowBinlogTableWrapper.java       | 23 +++++++
 .../apache/doris/cloud/catalog/CloudPartition.java | 32 +++++++---
 .../apache/doris/catalog/OlapTableWrapperTest.java | 67 ++++++++++++++++++++
 .../rules/rewrite/PruneEmptyPartitionTest.java     | 72 ++++++++++++++++++++++
 4 files changed, 184 insertions(+), 10 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/RowBinlogTableWrapper.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/RowBinlogTableWrapper.java
index 3d88a5ffbf6..d10be2ef76e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/RowBinlogTableWrapper.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/RowBinlogTableWrapper.java
@@ -17,14 +17,21 @@
 
 package org.apache.doris.catalog;
 
+import org.apache.doris.catalog.stream.StreamReadMode;
+import org.apache.doris.cloud.catalog.CloudPartition;
+import org.apache.doris.common.Config;
 import org.apache.doris.common.Pair;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Maps;
 
+import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
 
 /**
  * A lightweight wrapper base for read binlog<Row> of table
@@ -79,6 +86,22 @@ public class RowBinlogTableWrapper extends OlapTableWrapper {
         return KeysType.DUP_KEYS;
     }
 
+    @Override
+    public List<Long> selectNonEmptyPartitionIds(Collection<Long> partitionIds,
+            Optional<StreamReadMode> streamReadMode) {
+        if (Config.isCloudMode() && !hasFixedVisibleVersions()) {
+            // A row-binlog scan can start immediately after its target 
transaction becomes visible. Refresh
+            // cached-empty or unknown partitions so an older cache entry 
cannot prune newly visible binlog data.
+            List<CloudPartition> partitions = partitionIds.stream()
+                    .map(this::getPartition)
+                    .filter(Objects::nonNull)
+                    .map(partition -> (CloudPartition) partition)
+                    .collect(Collectors.toList());
+            return CloudPartition.selectNonEmptyPartitionIdsFromMs(partitions);
+        }
+        return super.selectNonEmptyPartitionIds(partitionIds, streamReadMode);
+    }
+
     @Override
     public boolean equals(Object obj) {
         if (!super.equals(obj)) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudPartition.java 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudPartition.java
index 07c2a6d504e..9c788a9ab7f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudPartition.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/cloud/catalog/CloudPartition.java
@@ -213,25 +213,37 @@ public class CloudPartition extends Partition {
 
     // Select the non-empty partitions and return the ids.
     public static List<Long> selectNonEmptyPartitionIds(List<CloudPartition> 
partitions) {
-        List<Long> nonEmptyPartitionIds = partitions.stream()
-                .filter(CloudPartition::hasDataCached)
-                .map(CloudPartition::getId)
-                .collect(Collectors.toList());
-        if (nonEmptyPartitionIds.size() == partitions.size()) {
+        return selectNonEmptyPartitionIds(partitions, false);
+    }
+
+    // Select non-empty partitions while bypassing the version cache for 
cached-empty or unknown partitions.
+    public static List<Long> 
selectNonEmptyPartitionIdsFromMs(List<CloudPartition> partitions) {
+        return selectNonEmptyPartitionIds(partitions, true);
+    }
+
+    private static List<Long> selectNonEmptyPartitionIds(List<CloudPartition> 
partitions, boolean forceRefresh) {
+        List<Long> nonEmptyPartitionIds = new ArrayList<>(partitions.size());
+        List<CloudPartition> unknowns = new ArrayList<>(partitions.size());
+        for (CloudPartition partition : partitions) {
+            if (partition.hasDataCached()) {
+                nonEmptyPartitionIds.add(partition.getId());
+            } else {
+                unknowns.add(partition);
+            }
+        }
+        if (unknowns.isEmpty()) {
             return nonEmptyPartitionIds;
         }
 
-        List<CloudPartition> unknowns = partitions.stream()
-                .filter(p -> !p.hasDataCached())
-                .collect(Collectors.toList());
-
         SummaryProfile profile = getSummaryProfile();
         if (profile != null) {
             profile.incGetPartitionVersionByHasDataCount();
         }
 
         try {
-            List<Long> versions = 
CloudPartition.getSnapshotVisibleVersion(unknowns);
+            List<Long> versions = forceRefresh
+                    ? CloudPartition.getSnapshotVisibleVersionFromMs(unknowns, 
false)
+                    : CloudPartition.getSnapshotVisibleVersion(unknowns);
 
             int size = versions.size();
             for (int i = 0; i < size; i++) {
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableWrapperTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableWrapperTest.java
index 3d084f9e270..8c2c96ca4fc 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableWrapperTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableWrapperTest.java
@@ -18,13 +18,21 @@
 package org.apache.doris.catalog;
 
 import org.apache.doris.binlog.BinlogTestUtils;
+import org.apache.doris.cloud.catalog.CloudPartition;
+import org.apache.doris.common.Config;
 import org.apache.doris.thrift.TStorageType;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
 
+import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
 public class OlapTableWrapperTest {
@@ -117,4 +125,63 @@ public class OlapTableWrapperTest {
         
Assertions.assertEquals(table.getSchemaByIndexId(table.getBaseIndexId()), 
wrapper.getSchemaByIndexId(table.getBaseIndexId()));
         
Assertions.assertEquals(table.getIndexSchemaVersion(table.getBaseIndexId()), 
wrapper.getIndexSchemaVersion(table.getBaseIndexId()));
     }
+
+    @Test
+    public void testCloudRowBinlogWrapperRefreshesCachedEmptyPartitions() {
+        long stalePartitionId = 100L;
+        long cachedNonEmptyPartitionId = 101L;
+        List<Long> partitionIds = ImmutableList.of(stalePartitionId, 
cachedNonEmptyPartitionId);
+
+        CloudPartition stalePartition = Mockito.mock(CloudPartition.class);
+        Mockito.when(stalePartition.getId()).thenReturn(stalePartitionId);
+        // Simulate another query refreshing the shared cache after the first 
cached-state check.
+        Mockito.when(stalePartition.hasDataCached()).thenReturn(false, true);
+        CloudPartition cachedNonEmptyPartition = 
Mockito.mock(CloudPartition.class);
+        
Mockito.when(cachedNonEmptyPartition.getId()).thenReturn(cachedNonEmptyPartitionId);
+        Mockito.when(cachedNonEmptyPartition.hasDataCached()).thenReturn(true);
+
+        OlapTable table = 
Mockito.spy(newTestTable(BinlogTestUtils.newTestRowBinlogConfig(true, true)));
+        
Mockito.doReturn(stalePartition).when(table).getPartition(stalePartitionId);
+        
Mockito.doReturn(cachedNonEmptyPartition).when(table).getPartition(cachedNonEmptyPartitionId);
+        RowBinlogTableWrapper wrapper = new RowBinlogTableWrapper(table);
+
+        try (MockedStatic<Config> mockedConfig = 
Mockito.mockStatic(Config.class);
+                MockedStatic<CloudPartition> mockedPartition = 
Mockito.mockStatic(
+                        CloudPartition.class, Mockito.CALLS_REAL_METHODS)) {
+            mockedConfig.when(Config::isCloudMode).thenReturn(true);
+            mockedPartition.when(() -> 
CloudPartition.getSnapshotVisibleVersionFromMs(
+                    ImmutableList.of(stalePartition), 
false)).thenReturn(ImmutableList.of(2L));
+            mockedPartition.clearInvocations();
+
+            
Assertions.assertEquals(ImmutableList.of(cachedNonEmptyPartitionId, 
stalePartitionId),
+                    wrapper.selectNonEmptyPartitionIds(partitionIds, 
Optional.empty()));
+            mockedPartition.verify(() -> 
CloudPartition.getSnapshotVisibleVersionFromMs(
+                    ImmutableList.of(stalePartition), false));
+            Mockito.verify(stalePartition).hasDataCached();
+            Mockito.verify(cachedNonEmptyPartition).hasDataCached();
+        }
+    }
+
+    @Test
+    public void 
testCloudRowBinlogWrapperWithFixedVisibleVersionsUsesOriginTable() {
+        long emptyPartitionId = 100L;
+        long nonEmptyPartitionId = 101L;
+        List<Long> partitionIds = ImmutableList.of(emptyPartitionId, 
nonEmptyPartitionId);
+
+        OlapTable table = 
Mockito.spy(newTestTable(BinlogTestUtils.newTestRowBinlogConfig(true, true)));
+        Mockito.doReturn(ImmutableList.of(nonEmptyPartitionId)).when(table)
+                .selectNonEmptyPartitionIds(partitionIds, Optional.empty());
+        RowBinlogTableWrapper wrapper = new RowBinlogTableWrapper(table, 
Collections.emptyMap(),
+                ImmutableMap.of(emptyPartitionId, 
Partition.PARTITION_INIT_VERSION, nonEmptyPartitionId, 2L));
+
+        try (MockedStatic<Config> mockedConfig = 
Mockito.mockStatic(Config.class);
+                MockedStatic<CloudPartition> mockedPartition = 
Mockito.mockStatic(CloudPartition.class)) {
+            mockedConfig.when(Config::isCloudMode).thenReturn(true);
+
+            Assertions.assertEquals(ImmutableList.of(nonEmptyPartitionId),
+                    wrapper.selectNonEmptyPartitionIds(partitionIds, 
Optional.empty()));
+            Mockito.verify(table).selectNonEmptyPartitionIds(partitionIds, 
Optional.empty());
+            mockedPartition.verifyNoInteractions();
+        }
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PruneEmptyPartitionTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PruneEmptyPartitionTest.java
new file mode 100644
index 00000000000..819b2b54197
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PruneEmptyPartitionTest.java
@@ -0,0 +1,72 @@
+// 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.doris.nereids.rules.rewrite;
+
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.cloud.catalog.CloudPartition;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.util.MemoPatternMatchSupported;
+import org.apache.doris.nereids.util.MemoTestUtils;
+import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.nereids.util.PlanConstructor;
+import org.apache.doris.qe.ConnectContext;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.Optional;
+
+class PruneEmptyPartitionTest implements MemoPatternMatchSupported {
+
+    @Test
+    void testNormalReadUsesCachedPartitionVersions() {
+        long emptyPartitionId = 100L;
+        long nonEmptyPartitionId = 101L;
+        List<Long> partitionIds = ImmutableList.of(emptyPartitionId, 
nonEmptyPartitionId);
+
+        CloudPartition emptyPartition = Mockito.mock(CloudPartition.class);
+        Mockito.when(emptyPartition.getId()).thenReturn(emptyPartitionId);
+        CloudPartition nonEmptyPartition = Mockito.mock(CloudPartition.class);
+        
Mockito.when(nonEmptyPartition.getId()).thenReturn(nonEmptyPartitionId);
+
+        OlapTable table = Mockito.spy(PlanConstructor.newOlapTable(10L, 
"normal_tbl", 0));
+        Mockito.doReturn(partitionIds).when(table).getPartitionIds();
+        
Mockito.doReturn(emptyPartition).when(table).getPartition(emptyPartitionId);
+        
Mockito.doReturn(nonEmptyPartition).when(table).getPartition(nonEmptyPartitionId);
+        Mockito.doReturn(ImmutableList.of(nonEmptyPartitionId)).when(table)
+                .selectNonEmptyPartitionIds(Mockito.anyCollection(), 
Mockito.any());
+
+        LogicalOlapScan scan = new LogicalOlapScan(
+                PlanConstructor.getNextRelationId(), table, 
ImmutableList.of("normal_tbl"));
+        ConnectContext connectContext = MemoTestUtils.createConnectContext();
+
+        try (MockedStatic<CloudPartition> mockedPartition = 
Mockito.mockStatic(CloudPartition.class)) {
+            LogicalOlapScan rewritten = (LogicalOlapScan) 
PlanChecker.from(connectContext, scan)
+                    .applyTopDown(new PruneEmptyPartition())
+                    .getPlan();
+
+            Assertions.assertEquals(ImmutableList.of(nonEmptyPartitionId), 
rewritten.getSelectedPartitionIds());
+            Mockito.verify(table).selectNonEmptyPartitionIds(partitionIds, 
Optional.empty());
+            mockedPartition.verifyNoInteractions();
+        }
+    }
+}


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

Reply via email to