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


##########
hudi-common/src/main/java/org/apache/hudi/common/util/ClusteringUtils.java:
##########
@@ -121,14 +121,18 @@ public static Option<HoodieInstant> 
getRequestedClusteringInstant(String timesta
   /**
    * Transitions the provided clustering instant fron inflight to complete 
based on the clustering
    * action type. After HUDI-7905, the new clustering commits are written with 
clustering action.
+   *
+   * @return the completed instant, whose action is the one recorded on the 
timeline. This differs
+   *         from the inflight action: a {@code clustering} inflight instant 
completes as
+   *         {@code replacecommit}.
    */
-  public static <T> void 
transitionClusteringOrReplaceInflightToComplete(boolean shouldLock, 
HoodieInstant clusteringInstant,
-                                                                         
HoodieReplaceCommitMetadata metadata, HoodieActiveTimeline activeTimeline,
-                                                                         
TableFormatCompletionAction tableFormatCompletionAction) {
+  public static <T> HoodieInstant 
transitionClusteringOrReplaceInflightToComplete(boolean shouldLock, 
HoodieInstant clusteringInstant,
+                                                                               
   HoodieReplaceCommitMetadata metadata, HoodieActiveTimeline activeTimeline,
+                                                                               
   TableFormatCompletionAction tableFormatCompletionAction) {
     if 
(clusteringInstant.getAction().equals(HoodieTimeline.CLUSTERING_ACTION)) {
-      activeTimeline.transitionClusterInflightToComplete(shouldLock, 
clusteringInstant, metadata, tableFormatCompletionAction);
+      return activeTimeline.transitionClusterInflightToComplete(shouldLock, 
clusteringInstant, metadata, tableFormatCompletionAction);

Review Comment:
   The contract this javadoc now states -- a `clustering` inflight completes as 
`replacecommit` -- is not pinned by any test in the module that owns it. The 
Flink test mocks this method away entirely 
(`TestHoodieFlinkTableServiceClient.java:256-258`) and the java-client 
functional test only observes it three layers down through the callback. Both 
would still pass if the completed action here ever changed.
   
   That matters more than usual because this is the third time this bug class 
has landed (`bf2b712c1c48` / HUDI-6384, `1338e2998d58` / HUDI-7161, 
`bf3cc4393981` / #18988) and the first time it has had any guard.
   
   `TestClusteringUtils.java:125-126` already drives a real timeline through 
this transition and holds the completed instant, but asserts only the state:
   
   ```java
   HoodieInstant complete = 
metaClient.getActiveTimeline().transitionClusterInflightToComplete(false, 
inflight, new HoodieReplaceCommitMetadata());
   assertEquals(HoodieInstant.State.COMPLETED, complete.getState());
   ```
   
   Please add `assertEquals(HoodieTimeline.REPLACE_COMMIT_ACTION, 
complete.getAction())` there, plus a case that calls 
`ClusteringUtils.transitionClusteringOrReplaceInflightToComplete` directly for 
both inflight kinds (`clustering` and `replacecommit`) and asserts 
`replacecommit` for each. The `replacecommit` case also covers the `else` 
branch on line 135 -- the one a table-version <= 7 writer takes -- which 
neither new test reaches today.



##########
hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/client/TestHoodieFlinkTableServiceClient.java:
##########
@@ -218,15 +225,24 @@ void testCompleteCompactionCommitsAndCleansMarkers() {
 
   @Test
   void testCompleteClusteringCommitsAndCleansMarkers() {
+    ClusteringCallback.MESSAGES.clear();
     HoodieWriteConfig writeConfig = HoodieWriteConfig.newBuilder()
         .withPath(metaClient.getBasePath())
         
.withMetadataConfig(HoodieMetadataConfig.newBuilder().enable(false).build())
+        .withCallbackConfig(HoodieWriteCommitCallbackConfig.newBuilder()
+            .writeCommitCallbackOn("true")
+            .withCallbackClass(ClusteringCallback.class.getName())
+            .build())
         .build();
     HoodieFlinkTable table = mock(HoodieFlinkTable.class);
     HoodieActiveTimeline activeTimeline = mock(HoodieActiveTimeline.class);
     when(table.getActiveTimeline()).thenReturn(activeTimeline);
     
when(table.getInstantGenerator()).thenReturn(metaClient.getInstantGenerator());
     HoodieInstant clusteringInstant = mock(HoodieInstant.class);

Review Comment:
   This test does catch a revert, but by accident rather than by design. 
`clusteringInstant` is never stubbed, so against the pre-fix code 
`clusteringInstant.getAction()` returns null, `Option.of(null)` throws 
(`Option.java:65`), `fireCommitCallbackIfNecessary` swallows it 
(`BaseHoodieClient.java:505-506`), and `MESSAGES` ends up empty. So it is the 
size assertion on line 266 that fails, with `expected: <1> but was: <0>`, which 
does not name the bug. The action assertion on line 267 is tautological -- it 
reads back exactly what line 245 stubbed.
   
   One line makes the mock match reality (a v2 inflight really is `clustering`) 
and turns a revert into the failure that names the defect:
   
   ```suggestion
       HoodieInstant clusteringInstant = mock(HoodieInstant.class);
       
when(clusteringInstant.getAction()).thenReturn(HoodieTimeline.CLUSTERING_ACTION);
   ```
   
   No strictness concern: this class has no 
`@ExtendWith(MockitoExtension.class)`, so the stub going unused on the fixed 
path will not trip `UnnecessaryStubbingException`.



##########
hudi-common/src/main/java/org/apache/hudi/common/util/ClusteringUtils.java:
##########
@@ -121,14 +121,18 @@ public static Option<HoodieInstant> 
getRequestedClusteringInstant(String timesta
   /**
    * Transitions the provided clustering instant fron inflight to complete 
based on the clustering
    * action type. After HUDI-7905, the new clustering commits are written with 
clustering action.
+   *
+   * @return the completed instant, whose action is the one recorded on the 
timeline. This differs
+   *         from the inflight action: a {@code clustering} inflight instant 
completes as
+   *         {@code replacecommit}.
    */
-  public static <T> void 
transitionClusteringOrReplaceInflightToComplete(boolean shouldLock, 
HoodieInstant clusteringInstant,
-                                                                         
HoodieReplaceCommitMetadata metadata, HoodieActiveTimeline activeTimeline,
-                                                                         
TableFormatCompletionAction tableFormatCompletionAction) {
+  public static <T> HoodieInstant 
transitionClusteringOrReplaceInflightToComplete(boolean shouldLock, 
HoodieInstant clusteringInstant,

Review Comment:
   nit, feel free to ignore: the `<T>` type parameter is unused, and now that 
the method returns `HoodieInstant` there is no reading under which it could 
bind. All three call sites are plain calls with no type witness, so dropping it 
is safe:
   
   ```suggestion
     public static HoodieInstant 
transitionClusteringOrReplaceInflightToComplete(boolean shouldLock, 
HoodieInstant clusteringInstant,
                                                                                
 HoodieReplaceCommitMetadata metadata, HoodieActiveTimeline activeTimeline,
                                                                                
 TableFormatCompletionAction tableFormatCompletionAction) {
   ```
   
   The sibling `transitionClusteringOrReplaceRequestedToInflight` on line 143 
does legitimately need its `<T>` -- leave that one alone.



##########
hudi-client/hudi-flink-client/src/test/java/org/apache/hudi/client/TestHoodieFlinkTableServiceClient.java:
##########
@@ -237,13 +253,33 @@ void testCompleteClusteringCommitsAndCleansMarkers() {
       clusteringUtils.when(() -> ClusteringUtils.getInflightClusteringInstant(
           "20260723120000001", activeTimeline, 
metaClient.getInstantGenerator()))
           .thenReturn(Option.of(clusteringInstant));
+      clusteringUtils.when(() -> 
ClusteringUtils.transitionClusteringOrReplaceInflightToComplete(
+          anyBoolean(), any(), any(), any(), any()))
+          .thenReturn(completedInstant);
       markersFactory.when(() -> WriteMarkersFactory.get(any(), any(), 
any())).thenReturn(writeMarkers);
       client.callCompleteClustering(metadata, table, "20260723120000001");
     } finally {
       client.close();
     }
 
     verify(writeMarkers).quietDeleteMarkerDir(any(), any(Integer.class));
+    assertEquals(1, ClusteringCallback.MESSAGES.size(), "callback must fire 
once for the clustering commit");
+    assertEquals(HoodieTimeline.REPLACE_COMMIT_ACTION,
+        ClusteringCallback.MESSAGES.get(0).getCommitActionType().orElse(null));
+  }
+
+  public static class ClusteringCallback implements HoodieWriteCommitCallback {

Review Comment:
   nit, feel free to ignore: `ClusteringCallback` is byte-for-byte identical to 
`RecordingCommitCallback` in 
`TestHoodieJavaClientOnMergeOnReadStorage.java:289` -- same static 
`CopyOnWriteArrayList`, same reflective constructor, same `// config arg 
required for reflective instantiation` comment.
   
   Both modules already consume the client-common test-jar 
(`hudi-client/hudi-flink-client/pom.xml:151-155` and 
`hudi-client/hudi-java-client/pom.xml:96-100`), so this can move into 
hudi-client-common test sources with no pom changes and both tests can share 
one copy. A third copy is likely the next time someone tests a callback.



##########
hudi-client/hudi-java-client/src/test/java/org/apache/hudi/client/functional/TestHoodieJavaClientOnMergeOnReadStorage.java:
##########
@@ -270,15 +270,14 @@ public void testWriteCommitCallbackFiresOnClustering() 
throws Exception {
     client.cluster(clusteringTime.get(), true);
     
assertTrue(metaClient.reloadActiveTimeline().filterCompletedInstants().containsInstant(clusteringTime.get()));
 
-    // The callback must fire once for the clustering completion, reporting 
the action actually on
-    // the timeline (replacecommit for table version < 8, clustering for 8+).
+    // The callback must fire exactly once for the clustering completion, 
reporting the completed
+    // timeline action (replacecommit).
     List<HoodieWriteCommitCallbackMessage> clusteringMessages = 
RecordingCommitCallback.MESSAGES.stream()
         .filter(m -> m.getCommitTime().equals(clusteringTime.get()))
         .collect(Collectors.toList());
     assertEquals(1, clusteringMessages.size(), "callback must fire once for 
the clustering commit");
-    String action = 
clusteringMessages.get(0).getCommitActionType().orElse(null);
-    assertTrue(HoodieTimeline.REPLACE_COMMIT_ACTION.equals(action) || 
HoodieTimeline.CLUSTERING_ACTION.equals(action),
-        "clustering callback must report the timeline action, got: " + action);
+    assertEquals(HoodieTimeline.REPLACE_COMMIT_ACTION, 
clusteringMessages.get(0).getCommitActionType().orElse(null));
+    assertNotNull(clusteringMessages.get(0).getPrevFilePaths(), "prevFilePaths 
must never be null");

Review Comment:
   nit: this assertion cannot fail, and it is not related to the action-type 
fix this PR is making. `getPrevFilePaths()` falls back to 
`Collections.emptyMap()` (`HoodieWriteCommitCallbackMessage.java:158`) and 
`resolvePrevFilePaths` never returns null, so non-nullness is structural. It is 
also a verbatim duplicate of line 236 in this same file, and the invariant 
already has dedicated coverage in client-common at 
`TestHoodieWriteCommitCallbackMessage.java:94` and `:119`.
   
   If you want the line to earn its place, assert the contents instead. For a 
clustering commit the map is guaranteed *empty*: clustering writes new file 
groups, so every stat carries `NULL_COMMIT` and 
`HoodieWriteCommitCallbackUtil.java:74` skips it.
   
   ```suggestion
       // Clustering writes new file groups, so every stat carries NULL_COMMIT 
and no prev path resolves.
       assertTrue(clusteringMessages.get(0).getPrevFilePaths().isEmpty());
   ```
   
   Otherwise just drop the line.



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