This is an automated email from the ASF dual-hosted git repository.
voonhous 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 99ceae1b6459 fix(meta-sync): advance last commit time synced when it
trails the active timeline midpoint (#19239)
99ceae1b6459 is described below
commit 99ceae1b645967dd39c5aee0ed6e904b363027a1
Author: Y Ethan Guo <[email protected]>
AuthorDate: Mon Aug 3 10:50:04 2026 -0700
fix(meta-sync): advance last commit time synced when it trails the active
timeline midpoint (#19239)
* fix(meta-sync): advance last commit time synced when it trails the active
timeline midpoint
* docs(meta-sync): clarify the marker advances on catalog-visible change,
not on data write
* docs(meta-sync): say "last commit time synced" instead of "marker" in the
javadoc
* test(meta-sync): unit-test the timeline-midpoint helper and take the
midpoint over completed commits
Compute the midpoint over completed commit instants only so an inflight
instant cannot shift it, and add TestHiveSyncToolTimelineMidpoint covering
the present/empty guards and the inflight-vs-completed boundary.
* test(meta-sync): narrow the timeline midpoint to completed commits
Use getCommitsTimeline().filterCompletedInstants() so clean, rollback, and
other non-commit instants cannot shift the midpoint, and add a test that
pins the narrowing to the commits timeline.
* refactor(meta-sync): read completed commits via
metaClient.getCommitsTimeline()
Keep the helper and its test identical across lines by reading the commits
timeline from metaClient.getCommitsTimeline(), which is available
everywhere,
rather than the HoodieTimeline interface method.
* test(meta-sync): rename midpoint test for clarity
Rename midpointIsTakenOverCompletedCommitsOnly to
midpointIsComputedFromCompletedCommitsOnly.
---
.../java/org/apache/hudi/hive/HiveSyncTool.java | 28 ++++-
.../org/apache/hudi/hive/TestHiveSyncTool.java | 12 ++-
.../hive/TestHiveSyncToolTimelineMidpoint.java | 114 +++++++++++++++++++++
3 files changed, 149 insertions(+), 5 deletions(-)
diff --git
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java
index 80b83816be8b..4c8aca377089 100644
---
a/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java
+++
b/hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java
@@ -26,6 +26,7 @@ import org.apache.hudi.common.table.HoodieTableMetaClient;
import org.apache.hudi.common.table.timeline.HoodieInstant;
import org.apache.hudi.common.util.ConfigUtils;
import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.VisibleForTesting;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.exception.InvalidTableException;
import org.apache.hudi.sync.common.HoodieSyncClient;
@@ -51,6 +52,8 @@ import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
+import static
org.apache.hudi.common.table.timeline.InstantComparison.LESSER_THAN;
+import static
org.apache.hudi.common.table.timeline.InstantComparison.compareTimestamps;
import static org.apache.hudi.common.util.StringUtils.nonEmpty;
import static
org.apache.hudi.hadoop.utils.HoodieInputFormatUtils.getInputFormatClassName;
import static
org.apache.hudi.hadoop.utils.HoodieInputFormatUtils.getOutputFormatClassName;
@@ -276,7 +279,8 @@ public class HiveSyncTool extends HoodieSyncTool implements
AutoCloseable {
boolean partitionsChanged = validateAndSyncPartitions(tableName,
tableExists);
boolean meetSyncConditions = schemaChanged || propertiesChanged ||
partitionsChanged;
- if (!config.getBoolean(META_SYNC_CONDITIONAL_SYNC) ||
meetSyncConditions) {
+ if (!config.getBoolean(META_SYNC_CONDITIONAL_SYNC) || meetSyncConditions
+ || isLastCommitTimeSyncedBehindTimelineMidpoint(tableName)) {
syncClient.updateLastCommitTimeSynced(tableName);
}
syncClient.updateHoodieWriterVersion(tableName);
@@ -291,6 +295,28 @@ public class HiveSyncTool extends HoodieSyncTool
implements AutoCloseable {
}
}
+ /**
+ * Whether last commit time synced trails the midpoint of the completed
commit instants.
+ * Advancing it at the midpoint bounds how far it can fall behind, capping
the archived-timeline
+ * scans a stale value forces on every conditional-sync round.
+ */
+ @VisibleForTesting
+ boolean isLastCommitTimeSyncedBehindTimelineMidpoint(String tableName) {
+ Option<String> lastCommitTimeSynced =
syncClient.getLastCommitTimeSynced(tableName);
+ if (!lastCommitTimeSynced.isPresent()) {
+ return false;
+ }
+ // Completed commits only: getCommitsTimeline() excludes non-commit
actions (clean, rollback),
+ // and filterCompletedInstants() excludes inflight instants.
+ List<HoodieInstant> completedCommits =
+
syncClient.getMetaClient().getCommitsTimeline().filterCompletedInstants().getInstants();
+ if (completedCommits.isEmpty()) {
+ return false;
+ }
+ String midpointInstantTime = completedCommits.get(completedCommits.size()
/ 2).requestedTime();
+ return compareTimestamps(lastCommitTimeSynced.get(), LESSER_THAN,
midpointInstantTime);
+ }
+
private boolean isAlreadySynced(String tableName) {
return syncClient.getLastCommitTimeSynced(tableName)
.map(lastCommit -> {
diff --git
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
index 9f5211ba5efe..baf90079ada7 100644
---
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
+++
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java
@@ -2536,12 +2536,16 @@ public class TestHiveSyncTool {
HiveTestUtil.addMORPartitions(0, true, true, true,
ZonedDateTime.now().plusDays(2), commitTime2, commitTime3);
+ // No sync condition is met, but the sync marker trails the midpoint of
the active commits
+ // timeline ([100, 101, 102, 103] with midpoint 102), so it advances to
the last commit.
+ reInitHiveSyncClient();
reSyncHiveTable();
- assertEquals(commitTime1,
hiveClient.getLastCommitTimeSynced(tableName).get());
+ assertEquals(commitTime3,
hiveClient.getLastCommitTimeSynced(tableName).get());
// Let the last commit time synced to be before the start of the active
timeline,
- // to trigger the fallback of listing all partitions. There is no
partition change
- // and the last commit time synced should still be the same.
+ // to trigger the fallback of listing all partitions. There is no
partition change,
+ // and the sync marker again trails the timeline midpoint, so it advances
to the
+ // last commit instead of aging out further.
HiveTestUtil.addMORPartitions(0, true, true, true,
ZonedDateTime.now().plusDays(2), commitTime4, commitTime5);
HiveTestUtil.removeCommitFromActiveTimeline(commitTime0, COMMIT_ACTION);
HiveTestUtil.removeCommitFromActiveTimeline(commitTime1,
DELTA_COMMIT_ACTION);
@@ -2549,7 +2553,7 @@ public class TestHiveSyncTool {
HiveTestUtil.removeCommitFromActiveTimeline(commitTime3,
DELTA_COMMIT_ACTION);
reInitHiveSyncClient();
reSyncHiveTable();
- assertEquals(commitTime1,
hiveClient.getLastCommitTimeSynced(tableName).get());
+ assertEquals(commitTime5,
hiveClient.getLastCommitTimeSynced(tableName).get());
}
@ParameterizedTest
diff --git
a/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncToolTimelineMidpoint.java
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncToolTimelineMidpoint.java
new file mode 100644
index 000000000000..3d0b5b3dd431
--- /dev/null
+++
b/hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncToolTimelineMidpoint.java
@@ -0,0 +1,114 @@
+/*
+ * 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.hudi.hive;
+
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import org.apache.hudi.common.testutils.MockHoodieTimeline;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.sync.common.HoodieSyncClient;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link
HiveSyncTool#isLastCommitTimeSyncedBehindTimelineMidpoint}, which decides
+ * whether a no-change conditional sync should still advance last commit time
synced. The helper
+ * reads the completed commits timeline, so these mock {@code
getMetaClient().getCommitsTimeline()};
+ * a call to any other timeline would hit an unstubbed mock and fail.
+ */
+class TestHiveSyncToolTimelineMidpoint {
+
+ private static final String TABLE_NAME = "table";
+
+ @Test
+ void midpointIsComputedFromCompletedCommitsOnly() {
+ // Completed commits [100, 102, 104], midpoint 102; the later inflight 106
must not shift it to 104.
+ HoodieSyncClient syncClient = mockSyncClient(new
MockHoodieTimeline(Stream.of("100", "102", "104"), Stream.of("106")));
+ HiveSyncTool tool = toolWith(syncClient);
+
+ // Not synced yet: nothing to advance.
+ stubLastCommitTimeSynced(syncClient, Option.empty());
+ assertFalse(tool.isLastCommitTimeSyncedBehindTimelineMidpoint(TABLE_NAME));
+
+ // Trails the midpoint.
+ stubLastCommitTimeSynced(syncClient, Option.of("101"));
+ assertTrue(tool.isLastCommitTimeSyncedBehindTimelineMidpoint(TABLE_NAME));
+
+ // At the midpoint is not behind it.
+ stubLastCommitTimeSynced(syncClient, Option.of("102"));
+ assertFalse(tool.isLastCommitTimeSyncedBehindTimelineMidpoint(TABLE_NAME));
+
+ // Past 102 but below 104: behind only if the inflight 106 is wrongly
counted.
+ stubLastCommitTimeSynced(syncClient, Option.of("103"));
+ assertFalse(tool.isLastCommitTimeSyncedBehindTimelineMidpoint(TABLE_NAME));
+ }
+
+ @Test
+ void midpointHandlesSmallTimelines() {
+ // Size 1: the sole commit is the midpoint.
+ HoodieSyncClient syncClient = mockSyncClient(new
MockHoodieTimeline(Stream.of("101"), Stream.empty()));
+ HiveSyncTool tool = toolWith(syncClient);
+ stubLastCommitTimeSynced(syncClient, Option.of("100"));
+ assertTrue(tool.isLastCommitTimeSyncedBehindTimelineMidpoint(TABLE_NAME));
+ stubLastCommitTimeSynced(syncClient, Option.of("101"));
+ assertFalse(tool.isLastCommitTimeSyncedBehindTimelineMidpoint(TABLE_NAME));
+
+ // Size 2: the midpoint (index 1) is the newer commit.
+ syncClient = mockSyncClient(new MockHoodieTimeline(Stream.of("100",
"102"), Stream.empty()));
+ tool = toolWith(syncClient);
+ stubLastCommitTimeSynced(syncClient, Option.of("101"));
+ assertTrue(tool.isLastCommitTimeSyncedBehindTimelineMidpoint(TABLE_NAME));
+ stubLastCommitTimeSynced(syncClient, Option.of("102"));
+ assertFalse(tool.isLastCommitTimeSyncedBehindTimelineMidpoint(TABLE_NAME));
+ }
+
+ @Test
+ void emptyCommitsTimelineIsNotBehind() {
+ HoodieSyncClient syncClient = mockSyncClient(new
MockHoodieTimeline(Stream.empty(), Stream.empty()));
+ HiveSyncTool tool = toolWith(syncClient);
+ stubLastCommitTimeSynced(syncClient, Option.of("103"));
+ assertFalse(tool.isLastCommitTimeSyncedBehindTimelineMidpoint(TABLE_NAME));
+ }
+
+ private static HoodieSyncClient mockSyncClient(HoodieTimeline
commitsTimeline) {
+ HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+ when(metaClient.getCommitsTimeline()).thenReturn(commitsTimeline);
+ HoodieSyncClient syncClient = mock(HoodieSyncClient.class);
+ when(syncClient.getMetaClient()).thenReturn(metaClient);
+ return syncClient;
+ }
+
+ private static HiveSyncTool toolWith(HoodieSyncClient syncClient) {
+ HiveSyncTool tool = mock(HiveSyncTool.class, CALLS_REAL_METHODS);
+ tool.syncClient = syncClient;
+ return tool;
+ }
+
+ private static void stubLastCommitTimeSynced(HoodieSyncClient syncClient,
Option<String> value) {
+ when(syncClient.getLastCommitTimeSynced(TABLE_NAME)).thenReturn(value);
+ }
+}