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

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


The following commit(s) were added to refs/heads/master by this push:
     new 75fb5d5ecd92 fix(common): avoid archive lookups for active clean and 
rollback instants (#20340)
75fb5d5ecd92 is described below

commit 75fb5d5ecd922ed873bcd6bb31382b6630f36e67
Author: Danny Chan <[email protected]>
AuthorDate: Thu Oct 15 17:38:48 2026 +0800

    fix(common): avoid archive lookups for active clean and rollback instants 
(#20340)
---
 .../timeline/TestCompletionTimeQueryView.java      |  3 ++
 .../versioning/v2/CompletionTimeQueryViewV2.java   | 21 ++------
 .../table/read/TestIncrementalQueryAnalyzer.java   | 59 ++++++++++++++++++++++
 3 files changed, 67 insertions(+), 16 deletions(-)

diff --git 
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/timeline/TestCompletionTimeQueryView.java
 
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/timeline/TestCompletionTimeQueryView.java
index c59fcaa3b8d1..ac568c98cbb8 100644
--- 
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/timeline/TestCompletionTimeQueryView.java
+++ 
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/timeline/TestCompletionTimeQueryView.java
@@ -225,6 +225,9 @@ public class TestCompletionTimeQueryView {
 
     try (CompletionTimeQueryView view =
              
metaClient.getTableFormat().getTimelineFactory().createCompletionTimeQueryView(metaClient))
 {
+      assertFalse(view.isArchived(staleRollback));
+      assertTrue(view.isArchived(String.format("%08d", 4)));
+      assertFalse(view.isArchived(String.format("%08d", 7)));
       // archived commits newer than the rollback are found, not reported as 
pending
       assertThat(view.getCompletionTime(String.format("%08d", 4)).orElse(""), 
is(String.format("%08d", 1004)));
       assertThat(getInstantTimeSetFormattedString(view, 3 + 1000, 6 + 1000), 
is("00000003,00000004,00000005,00000006"));
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/versioning/v2/CompletionTimeQueryViewV2.java
 
b/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/versioning/v2/CompletionTimeQueryViewV2.java
index f3126e9ee40c..fc7fa7340d8b 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/versioning/v2/CompletionTimeQueryViewV2.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/versioning/v2/CompletionTimeQueryViewV2.java
@@ -38,7 +38,6 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.function.Function;
@@ -89,11 +88,6 @@ public class CompletionTimeQueryViewV2 implements 
CompletionTimeQueryView, Seria
    */
   private final String firstActiveWriteInstant;
 
-  /**
-   * The savepointed instants on the active timeline.
-   */
-  private final Set<String> activeSavepointedInstants;
-
   /**
    * The constructor.
    *
@@ -103,15 +97,11 @@ public class CompletionTimeQueryViewV2 implements 
CompletionTimeQueryView, Seria
     this.metaClient = metaClient;
     this.instantTimeToCompletionTimeMap = new ConcurrentHashMap<>();
     HoodieTimeline activeTimeline = metaClient.getActiveTimeline();
-    // Instants at or after the cursor are assumed to be on the active 
timeline, so it must start at the first
-    // write instant that is not savepointed: an older non-write instant (e.g. 
a rollback the archiver has not reached)
-    // or a savepointed commit left behind by archival would otherwise hide 
the archived write instants after it.
-    // Instants before the cursor that are still active are loaded eagerly by 
#load. Without such a write instant,
-    // nothing after the last active instant can be archived.
+    // Tables upgraded from pre-v8 can have interleaved archived and active 
instants,
+    // start at the first non-savepointed write so older active instants do 
not hide archived writes,
+    // fall back to the last active instant when no such write exists.
     this.firstActiveWriteInstant = 
activeTimeline.getTimelineOfActions(HoodieTimeline.WRITE_AND_SAVEPOINT_ACTIONS).getFirstNonSavepointCommit()
         
.or(activeTimeline::lastInstant).map(HoodieInstant::requestedTime).orElse("");
-    this.activeSavepointedInstants = 
activeTimeline.getSavePointTimeline().getInstantsAsStream()
-        .map(HoodieInstant::requestedTime).collect(Collectors.toSet());
     this.cursorInstant = this.firstActiveWriteInstant;
     // Note: use getWriteTimeline() to keep sync with the fs view 
visibleCommitsAndCompactionTimeline, see 
AbstractTableFileSystemView.refreshTimeline.
     this.firstNonSavepointCommit = 
activeTimeline.getWriteTimeline().getFirstNonSavepointCommit().map(HoodieInstant::requestedTime).orElse("");
@@ -131,10 +121,9 @@ public class CompletionTimeQueryViewV2 implements 
CompletionTimeQueryView, Seria
    * Returns whether the instant is archived.
    */
   public boolean isArchived(String instantTime) {
-    // with archival beyond savepoint, the write instants between a 
savepointed commit and the first active write
-    // instant are archived as well, while the savepointed commit itself stays 
active.
+    // A savepointed write or an older clean/rollback can still be active 
before the write boundary.
     return InstantComparison.compareTimestamps(instantTime, LESSER_THAN, 
this.firstActiveWriteInstant)
-        && !this.activeSavepointedInstants.contains(instantTime);
+        && !this.metaClient.getActiveTimeline().containsInstant(instantTime);
   }
 
   /**
diff --git 
a/hudi-common/src/test/java/org/apache/hudi/common/table/read/TestIncrementalQueryAnalyzer.java
 
b/hudi-common/src/test/java/org/apache/hudi/common/table/read/TestIncrementalQueryAnalyzer.java
index 4ed4c34e41e0..061fa194147a 100644
--- 
a/hudi-common/src/test/java/org/apache/hudi/common/table/read/TestIncrementalQueryAnalyzer.java
+++ 
b/hudi-common/src/test/java/org/apache/hudi/common/table/read/TestIncrementalQueryAnalyzer.java
@@ -21,21 +21,35 @@ package org.apache.hudi.common.table.read;
 
 import org.apache.hudi.common.table.HoodieTableMetaClient;
 import org.apache.hudi.common.table.log.InstantRange;
+import org.apache.hudi.common.table.timeline.ArchivedTimelineLoader;
 import org.apache.hudi.common.table.timeline.HoodieInstant;
 import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import 
org.apache.hudi.common.table.timeline.versioning.v2.CompletionTimeQueryViewV2;
+import org.apache.hudi.common.testutils.MockHoodieTimeline;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.stream.Stream;
 
+import static 
org.apache.hudi.common.testutils.HoodieTestUtils.INSTANT_GENERATOR;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 class TestIncrementalQueryAnalyzer {
@@ -77,6 +91,51 @@ class TestIncrementalQueryAnalyzer {
     assertThrows(IllegalStateException.class, 
IncrementalQueryAnalyzer.QueryContext.EMPTY::getLastInstant);
   }
 
+  @ParameterizedTest
+  @CsvSource({"clean,false", "clean,true", "rollback,false", "rollback,true"})
+  void testActiveNonWriteDoesNotTriggerArchivedTimelineLookup(String action, 
boolean savepoint) {
+    String savepointTime = "20261001000000000";
+    String nonWriteTime = "20261002000000000";
+    String writeTime = "20261003000000000";
+    String completionTime = "20261005000000000";
+    List<HoodieInstant> instants = new ArrayList<>();
+    if (savepoint) {
+      
instants.add(INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.COMPLETED, 
HoodieTimeline.COMMIT_ACTION, savepointTime, savepointTime));
+      
instants.add(INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.COMPLETED, 
HoodieTimeline.SAVEPOINT_ACTION, savepointTime, savepointTime));
+    }
+    // Both actions finish in the query window, but only the write belongs in 
the result.
+    
instants.add(INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.COMPLETED, 
action, nonWriteTime, completionTime));
+    
instants.add(INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.COMPLETED, 
HoodieTimeline.COMMIT_ACTION, writeTime, completionTime));
+    ArchivedTimelineLoader loader = mock(ArchivedTimelineLoader.class);
+    MockHoodieTimeline active = new MockHoodieTimeline(instants);
+    HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class, 
RETURNS_DEEP_STUBS);
+    when(metaClient.getActiveTimeline()).thenReturn(active);
+    
when(metaClient.getCommitsTimeline()).thenReturn(active.getCommitsTimeline());
+    
when(metaClient.getCommitsAndCompactionTimeline()).thenReturn(active.getCommitsAndCompactionTimeline());
+    
when(metaClient.getTableFormat().getTimelineFactory().createArchivedTimelineLoader()).thenReturn(loader);
+    try (CompletionTimeQueryViewV2 view = new 
CompletionTimeQueryViewV2(metaClient)) {
+      assertEquals(writeTime, view.getCursorInstant());
+      assertFalse(view.isArchived(nonWriteTime));
+      assertFalse(view.isArchived(writeTime));
+      if (savepoint) {
+        assertFalse(view.isArchived(savepointTime));
+      }
+      
when(metaClient.getTableFormat().getTimelineFactory().createCompletionTimeQueryView(metaClient)).thenReturn(view);
+      IncrementalQueryAnalyzer.QueryContext result = 
IncrementalQueryAnalyzer.builder()
+          .metaClient(metaClient)
+          .startCompletionTime(completionTime)
+          .endCompletionTime(completionTime)
+          .rangeType(InstantRange.RangeType.CLOSED_CLOSED)
+          .build().analyze();
+      assertEquals(Collections.singletonList(writeTime), 
result.getInstantTimeList());
+      assertTrue(result.getArchivedInstants().isEmpty());
+      // The one-day lookback is after writeTime, isolating classification 
from archive completion-time loading.
+      verifyNoInteractions(loader);
+      verify(metaClient, never()).getArchivedTimeline(anyString(), eq(false));
+      verify(metaClient, never()).getArchivePath();
+    }
+  }
+
   @Test
   void testBuilderRequiresMetaClientAndRangeType() {
     assertThrows(NullPointerException.class, () -> 
IncrementalQueryAnalyzer.builder()

Reply via email to