voonhous commented on code in PR #19239:
URL: https://github.com/apache/hudi/pull/19239#discussion_r3698347802


##########
hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java:
##########
@@ -291,6 +295,28 @@ protected void syncHoodieTable(String tableName, boolean 
useRealtimeInputFormat,
     }
   }
 
+  /**
+   * 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();

Review Comment:
   **minor:** this reads a different timeline than the one the decision 
affects. `metaClient.getCommitsTimeline()` is table-type-aware -- on COW it 
returns `getCommitAndReplaceTimeline()`, dropping DELTA_COMMIT -- while 
`updateLastCommitTimeSynced`, `isAlreadySynced`, and the fallback check all go 
through `syncClient.getActiveTimeline()` (= 
`getCommitsTimeline().filterCompletedInstants()`, always including 
DELTA_COMMIT). The first commit of this branch (0423e103f06e) had the 
consistent accessor; fb8fa74ff0ae switched it to fit the mock. Please restore 
it and stub `syncClient.getActiveTimeline()` in 
`TestHiveSyncToolTimelineMidpoint` instead of mocking `HoodieTableMetaClient` 
-- it also simplifies the test.
   
   ```suggestion
       // getActiveTimeline() narrows to completed commit actions, matching the 
timeline that
       // updateLastCommitTimeSynced and isAlreadySynced read.
       List<HoodieInstant> completedCommits = 
syncClient.getActiveTimeline().getInstants();
   ```



##########
hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java:
##########
@@ -291,6 +295,28 @@ protected void syncHoodieTable(String tableName, boolean 
useRealtimeInputFormat,
     }
   }
 
+  /**
+   * 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.

Review Comment:
   The archived-timeline scan described here has been unreachable from 
Hive/Glue sync since HUDI-5816 (#8388): `validateAndSyncPartitions` diverts to 
`syncAllPartitions` whenever 
`getActiveTimeline().isBeforeTimelineStarts(marker)`, and since completed 
commit actions are a subset of write actions, that always triggers before 
`TimelineUtils.getCommitsTimelineAfter` could reach its archived-merge branch 
(`writeTimeline.isBeforeTimelineStarts`). The real per-round cost of a stale 
marker is the full metastore + storage partition listing in the fallback. 
Please update the commit message and PR description too -- the mechanism 
matters for choosing the right trigger (see the comment on the gate above).
   
   ```suggestion
      * Advancing it at the midpoint bounds how far it can fall behind, capping 
the repeated
      * full partition listings a stale value forces on every conditional-sync 
round.
   ```



##########
hudi-sync/hudi-hive-sync/src/test/java/org/apache/hudi/hive/TestHiveSyncTool.java:
##########
@@ -2536,20 +2536,24 @@ public void testSyncWithoutDiffs(String syncMode) 
throws Exception {
 
     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);
     HiveTestUtil.removeCommitFromActiveTimeline(commitTime2, COMMIT_ACTION);
     HiveTestUtil.removeCommitFromActiveTimeline(commitTime3, 
DELTA_COMMIT_ACTION);
     reInitHiveSyncClient();
     reSyncHiveTable();
-    assertEquals(commitTime1, 
hiveClient.getLastCommitTimeSynced(tableName).get());
+    assertEquals(commitTime5, 
hiveClient.getLastCommitTimeSynced(tableName).get());

Review Comment:
   **blocker (test coverage):** both updated assertions expect exactly what the 
always-update path writes (`updateLastCommitTimeSynced` stores 
`activeTimeline.lastInstant().requestedTime()`, i.e. 
`commitTime3`/`commitTime5`), so this test no longer discriminates the new 
gate. Verified by mutation: with the whole condition replaced by `if (true)`, 
with the midpoint index changed to `size() - 1`, or with the helper body 
replaced by `return true`, the full `TestHiveSyncTool` suite (274 tests) stays 
green. The previous assertions were the HUDI-1932 (#3053) pin that a no-diff 
conditional sync does not move the marker; nothing replaces that pin 
(`testHiveSyncWithMultiWriter` always has partition changes, so it cannot).
   
   Please split this second block to also pin the negative case: after adding 
`commitTime4`/`commitTime5` but before the `removeCommitFromActiveTimeline` 
calls, sync and assert the marker stays at `commitTime3` -- it is at the 
midpoint of `[100..105]`, so it must not advance:
   
   ```java
   HiveTestUtil.addMORPartitions(0, true, true, true, 
ZonedDateTime.now().plusDays(2), commitTime4, commitTime5);
   // Marker 103 is at the midpoint of [100..105]: no sync condition met, must 
NOT advance.
   reInitHiveSyncClient();
   reSyncHiveTable();
   assertEquals(commitTime3, 
hiveClient.getLastCommitTimeSynced(tableName).get());
   // ... existing removeCommitFromActiveTimeline calls and final sync unchanged
   ```
   
   This passes on the PR branch and fails on all three mutants above.



##########
hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java:
##########
@@ -276,7 +279,8 @@ protected void syncHoodieTable(String tableName, boolean 
useRealtimeInputFormat,
 
       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)) {

Review Comment:
   **major (correctness):** advancing the marker here permanently disables the 
only path that can DROP a metastore partition that was not rewritten. The 
incremental path emits drops only for partitions also present in 
`writtenPartitionsSince` (the 3-arg `HoodieSyncClient.getPartitionEvents` loops 
over `writtenPartitionsOnStorage`, and `syncPartitions` returns early when it 
is empty). Cleaner-deleted partitions, out-of-band directory deletes, and 
manual catalog edits are repaired only by `syncAllPartitions` (HUDI-5816, 
#8388), which fires only when the marker falls before the active timeline 
start. Today a conditional-sync table self-heals every round once the marker 
ages out (at full-listing cost); with this change it never reconciles again. 
This drop path is historically fragile (HUDI-9770, #13794 patched it in Aug 
2025).
   
   Also, the midpoint is a heuristic for a condition the code already computes 
exactly: `validateAndSyncPartitions` knows when the full-listing fallback fired 
(`syncClient.getActiveTimeline().isBeforeTimelineStarts(...)`). The midpoint 
rule additionally fires on roughly half of the no-change rounds where the 
marker is still well inside the active timeline (catalog writes with no 
benefit), and gives no bound at all if the commit rate between syncs outpaces 
the archival window.
   
   Suggested fix: advance the marker exactly in the round that performed 
`syncAllPartitions` -- the catalog is provably consistent with storage at that 
point, the every-round full listing still collapses to once per aging-out 
cycle, and catalog writes are strictly rarer than with the midpoint rule. 
Alternatively, keep the midpoint but force one `syncAllPartitions` in the 
advancing round. Either way, please add a test: remove a partition on storage 
cleaner-style with no new writes and assert it eventually disappears from the 
metastore.



##########
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() {

Review Comment:
   nit, feel free to ignore: this packs the size-1 and size-2 scenarios into 
one method with mid-method mock reassignment, so a failure report will not say 
which size broke. Two `@Test` methods would match the per-scenario style of the 
rest of the class.



##########
hudi-sync/hudi-hive-sync/src/main/java/org/apache/hudi/hive/HiveSyncTool.java:
##########
@@ -291,6 +295,28 @@ protected void syncHoodieTable(String tableName, boolean 
useRealtimeInputFormat,
     }
   }
 
+  /**
+   * 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);

Review Comment:
   nit, optional: `getLastCommitTimeSynced` is now read twice per table per 
round (here and in `validateAndSyncPartitions`). Free on the cached HMS/Glue 
paths, but HMS JDBC-fallback mode re-queries the metastore each time. Consider 
hoisting the read into `syncHoodieTable` and passing it to both.



##########
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.

Review Comment:
   **minor (coverage):** three branches of the helper are untested:
   
   - The "excludes non-commit actions (clean, rollback)" claim is unpinned: 
`MockHoodieTimeline` can only build `COMMIT_ACTION` instants, and the 
`midpointNarrowsToCommitsTimeline` test added in 937c446f7655 was deleted by 
the fb8fa74ff0ae refactor. What survives pins the accessor name only.
   - Marker ahead of the whole timeline (rollback/restore shrank it): the 
helper returns false, which is right, but no test says so. Add: marker `"106"` 
vs completed `[100, 102, 104]` and assert false.
   - COW is never exercised (both integration tests use MOR, this class stubs 
the accessor), so the table-type branch inside 
`metaClient.getCommitsTimeline()` runs in no test.
   
   Cheapest fix: add the marker-ahead case here, and one rollback instant in 
the `testSyncWithoutDiffs` flow (`HiveTestUtil.addRollbackInstantToTable`, 
already used at `TestHiveSyncTool.java:608`) so the non-commit narrowing is 
pinned end to end.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to