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 667626cb9b37 chore(glue-sync): Ignore EntityNotFoundException when 
dropping Glue partitions (#19142)
667626cb9b37 is described below

commit 667626cb9b37719d222b7840cdb5ab8f0a41110d
Author: wangxianghu <[email protected]>
AuthorDate: Fri Jul 3 09:51:35 2026 +0800

    chore(glue-sync): Ignore EntityNotFoundException when dropping Glue 
partitions (#19142)
---
 .../hudi/aws/sync/AWSGlueCatalogSyncClient.java    | 20 +++++++-
 .../hudi/aws/sync/TestAWSGlueSyncClient.java       | 56 ++++++++++++++++++++++
 2 files changed, 74 insertions(+), 2 deletions(-)

diff --git 
a/hudi-aws/src/main/java/org/apache/hudi/aws/sync/AWSGlueCatalogSyncClient.java 
b/hudi-aws/src/main/java/org/apache/hudi/aws/sync/AWSGlueCatalogSyncClient.java
index 884b5c53a25d..10f0b5a1592d 100644
--- 
a/hudi-aws/src/main/java/org/apache/hudi/aws/sync/AWSGlueCatalogSyncClient.java
+++ 
b/hudi-aws/src/main/java/org/apache/hudi/aws/sync/AWSGlueCatalogSyncClient.java
@@ -71,6 +71,7 @@ import 
software.amazon.awssdk.services.glue.model.GetPartitionsRequest;
 import software.amazon.awssdk.services.glue.model.GetPartitionsResponse;
 import software.amazon.awssdk.services.glue.model.GetTableRequest;
 import software.amazon.awssdk.services.glue.model.KeySchemaElement;
+import software.amazon.awssdk.services.glue.model.PartitionError;
 import software.amazon.awssdk.services.glue.model.PartitionIndex;
 import software.amazon.awssdk.services.glue.model.PartitionIndexDescriptor;
 import software.amazon.awssdk.services.glue.model.PartitionInput;
@@ -137,6 +138,7 @@ public class AWSGlueCatalogSyncClient extends 
HoodieSyncClient {
   private static final int MAX_PARTITIONS_PER_CHANGE_REQUEST = 100;
   private static final int MAX_PARTITIONS_PER_READ_REQUEST = 1000;
   private static final int MAX_DELETE_PARTITIONS_PER_REQUEST = 25;
+  private static final String ENTITY_NOT_FOUND_ERROR_CODE = 
"EntityNotFoundException";
   protected final GlueAsyncClient awsGlue;
   private static final String GLUE_PARTITION_INDEX_ENABLE = 
"partition_filtering.enabled";
   private static final int PARTITION_INDEX_MAX_NUMBER = 3;
@@ -437,8 +439,22 @@ public class AWSGlueCatalogSyncClient extends 
HoodieSyncClient {
 
       BatchDeletePartitionResponse response = future.get();
       if (CollectionUtils.nonEmpty(response.errors())) {
-        throw new HoodieGlueSyncException("Fail to drop partitions to " + 
tableId(databaseName, tableName)
-            + " with error(s): " + response.errors());
+        // Dropping a partition that no longer exists is a no-op for an 
idempotent cleanup, so
+        // ignore EntityNotFoundException errors and only fail on other (e.g. 
permission/throttling) errors.
+        Map<Boolean, List<PartitionError>> errorsByIgnorable = 
response.errors().stream()
+            .collect(Collectors.partitioningBy(
+                error -> 
ENTITY_NOT_FOUND_ERROR_CODE.equals(error.errorDetail().errorCode())));
+        List<PartitionError> ignorableErrors = errorsByIgnorable.get(true);
+        if (!ignorableErrors.isEmpty()) {
+          log.info("Ignored dropping {} non-existent partition(s) from table 
{}: {}", ignorableErrors.size(),
+              tableId(databaseName, tableName),
+              
ignorableErrors.stream().map(PartitionError::partitionValues).collect(Collectors.toList()));
+        }
+        List<PartitionError> realErrors = errorsByIgnorable.get(false);
+        if (!realErrors.isEmpty()) {
+          throw new HoodieGlueSyncException("Fail to drop partitions to " + 
tableId(databaseName, tableName)
+              + " with error(s): " + realErrors);
+        }
       }
     } catch (Exception e) {
       throw new HoodieGlueSyncException("Fail to drop partitions to " + 
tableId(databaseName, tableName), e);
diff --git 
a/hudi-aws/src/test/java/org/apache/hudi/aws/sync/TestAWSGlueSyncClient.java 
b/hudi-aws/src/test/java/org/apache/hudi/aws/sync/TestAWSGlueSyncClient.java
index f4822e32f05d..8c3b24c0b677 100644
--- a/hudi-aws/src/test/java/org/apache/hudi/aws/sync/TestAWSGlueSyncClient.java
+++ b/hudi-aws/src/test/java/org/apache/hudi/aws/sync/TestAWSGlueSyncClient.java
@@ -647,6 +647,62 @@ class TestAWSGlueSyncClient {
     assertTrue(ex.getCause().getCause().getMessage().contains("Fail to drop 
partitions"));
   }
 
+  @Test
+  void testDropPartitions_IgnoresEntityNotFound() {
+    String tableName = "tbl";
+    List<String> toDrop = List.of("2025/05/19");
+
+    // Glue reports EntityNotFoundException for a partition that no longer 
exists; it should be ignored.
+    ErrorDetail detail = 
ErrorDetail.builder().errorCode(EntityNotFoundException.class.getSimpleName()).build();
+    PartitionError pe = 
PartitionError.builder().partitionValues(Arrays.asList("2025", "05", 
"19")).errorDetail(detail).build();
+    BatchDeletePartitionResponse resp = BatchDeletePartitionResponse.builder()
+        .errors(Collections.singletonList(pe))
+        .build();
+    
when(mockAwsGlue.batchDeletePartition(any(BatchDeletePartitionRequest.class)))
+        .thenReturn(CompletableFuture.completedFuture(resp));
+
+    // should swallow the EntityNotFound error and not throw
+    awsGlueSyncClient.dropPartitions(tableName, toDrop);
+
+    
verify(mockAwsGlue).batchDeletePartition(any(BatchDeletePartitionRequest.class));
+  }
+
+  @Test
+  void testDropPartitions_MixedErrorsStillThrow() {
+    String tableName = "tbl";
+    List<String> toDrop = Arrays.asList("2025/05/19", "2025/05/18");
+
+    // One ignorable EntityNotFound error and one real error -> should still 
throw for the real one.
+    PartitionError ignorable = PartitionError.builder()
+        .partitionValues(Arrays.asList("2025", "05", "19"))
+        
.errorDetail(ErrorDetail.builder().errorCode(EntityNotFoundException.class.getSimpleName()).build())
+        .build();
+    PartitionError real = PartitionError.builder()
+        .partitionValues(Arrays.asList("2025", "05", "18"))
+        
.errorDetail(ErrorDetail.builder().errorCode("InternalServiceException").build())
+        .build();
+    BatchDeletePartitionResponse resp = BatchDeletePartitionResponse.builder()
+        .errors(Arrays.asList(ignorable, real))
+        .build();
+    
when(mockAwsGlue.batchDeletePartition(any(BatchDeletePartitionRequest.class)))
+        .thenReturn(CompletableFuture.completedFuture(resp));
+
+    HoodieGlueSyncException ex = assertThrows(
+        HoodieGlueSyncException.class,
+        () -> awsGlueSyncClient.dropPartitions(tableName, toDrop)
+    );
+    // Walk the full cause chain: the error list is nested a few wrappers deep.
+    StringBuilder chain = new StringBuilder();
+    for (Throwable t = ex; t != null; t = t.getCause()) {
+      chain.append(t.getMessage()).append('\n');
+    }
+    String messages = chain.toString();
+    assertTrue(messages.contains("Fail to drop partitions"));
+    // Only the real error should be surfaced, not the ignored EntityNotFound 
one.
+    assertTrue(messages.contains("InternalServiceException"));
+    
assertFalse(messages.contains(EntityNotFoundException.class.getSimpleName()));
+  }
+
   @Disabled("Integration test – requires real AWS environment")
   @Test
   void testIntegrationTableExists_RealGlueEnvironment() {

Reply via email to