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


##########
hudi-cli/src/main/java/org/apache/hudi/cli/commands/RepairsCommand.java:
##########
@@ -189,29 +193,54 @@ public String overwriteHoodieProperties(
 
   @ShellMethod(key = "repair corrupted clean files", value = "repair corrupted 
clean files")
   public void removeCorruptedPendingCleanAction() {
+    removeCorruptedPendingCleanAction(HoodieCLI.getTableMetaClient());
+  }
 
-    HoodieTableMetaClient client = HoodieCLI.getTableMetaClient();
-    HoodieTimeline cleanerTimeline = 
HoodieCLI.getTableMetaClient().getActiveTimeline().getCleanerTimeline();
+  /**
+   * Removes the pending clean instants whose plan is verifiably empty or 
corrupt.
+   * <p>
+   * The plan bytes are read in full before anything is judged. The timeline 
serde wraps every
+   * exception raised while it streams an instant file, a transient read 
failure included, in
+   * the same "unable to read commit metadata" IOException that an empty or 
truncated file
+   * raises, so the message cannot tell a storage outage from corruption. A 
failure of the
+   * read itself is therefore propagated, and only the in-memory decode, which 
no I/O can
+   * disturb, decides that the plan is corrupt.
+   */
+  static void removeCorruptedPendingCleanAction(HoodieTableMetaClient client) {
+    HoodieActiveTimeline activeTimeline = client.getActiveTimeline();
+    HoodieTimeline cleanerTimeline = activeTimeline.getCleanerTimeline();
     log.info("Inspecting pending clean metadata in timeline for corrupted 
files");
     
cleanerTimeline.filterInflightsAndRequested().getInstants().forEach(instant -> {
-      try {
-        CleanerUtils.getCleanerPlan(client, instant);
-      } catch (AvroRuntimeException e) {
-        log.warn("Corruption found. Trying to remove corrupted clean instant 
file: {}", instant);
-        TimelineUtils.deleteInstantFile(client.getStorage(), 
client.getTimelinePath(),
-            instant, client.getInstantFileNameGenerator());
-      } catch (IOException ioe) {
-        if (ioe.getMessage().contains("Not an Avro data file")) {
-          log.warn("Corruption found. Trying to remove corrupted clean instant 
file: {}", instant);
-          TimelineUtils.deleteInstantFile(client.getStorage(), 
client.getTimelinePath(),
-              instant, client.getInstantFileNameGenerator());
-        } else {
-          throw new HoodieIOException(ioe.getMessage(), ioe);
-        }
+      HoodieInstant planInstant = CleanerUtils.getCleanRequestInstant(client, 
instant);
+      byte[] plan;
+      try (InputStream in = 
activeTimeline.getInstantContentStream(planInstant)) {
+        plan = FileIOUtils.readAsByteArray(in);
+      } catch (IOException e) {
+        throw new HoodieIOException("Failed to read the plan of pending clean 
instant " + instant, e);
+      }
+      if (plan.length > 0 && isReadableCleanerPlan(client, plan)) {
+        return;
       }
+      log.warn("Corruption found. Trying to remove corrupted clean instant 
file: {}", instant);
+      TimelineUtils.deleteInstantFile(client.getStorage(), 
client.getTimelinePath(),

Review Comment:
   Not intentional, and it predates the PR -- master reads through 
`CleanerUtils.getCleanerPlan(client, instant)`, which resolves the requested 
instant internally, and then deletes `instant`. Same mismatch, just less 
visible.
   
   Taking the requested instant instead would invert it rather than fix it. 
`TimelineLayout.filterHoodieInstantsByLatestState` groups by `(requestedTime, 
action)` and keeps the highest state, so a clean holding both files is listed 
once, as the inflight instant -- deleting only its plan would strand an 
inflight action with nothing to read.
   
   Fixed by removing the whole pending action. The inflight file goes first, so 
that a failure between the two deletes leaves the action requested, which is a 
state this command already handles, rather than inflight with no plan. 
`testRemoveCorruptedPendingCleanActionRemovesInflightAndItsPlan` pins that one 
pass clears both files.
   



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