This is an automated email from the ASF dual-hosted git repository.

jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new 977fe8b6b4 [Cherry-pick to branch-1.3] [#12990] fix(catalog-glue): 
Include the AWS error detail in Glue failure messages (#12991) (#13031)
977fe8b6b4 is described below

commit 977fe8b6b46b1177531339c84a8a6ffe118600c8
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Sep 10 11:23:18 2026 +0800

    [Cherry-pick to branch-1.3] [#12990] fix(catalog-glue): Include the AWS 
error detail in Glue failure messages (#12991) (#13031)
    
    **Cherry-pick Information:**
    - Original commit: 2559337520be561a3489c76ff2fac7699626cf81
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Co-authored-by: Yuhui <[email protected]>
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../catalog/glue/GlueExceptionConverter.java       |  32 +++-
 .../catalog/glue/TestGlueExceptionConverter.java   | 181 +++++++++++++++++++++
 2 files changed, 211 insertions(+), 2 deletions(-)

diff --git 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
index 7857875e0c..c0ca31966c 100644
--- 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
+++ 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java
@@ -18,10 +18,12 @@
  */
 package org.apache.gravitino.catalog.glue;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.exceptions.NoSuchTableException;
 import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
 import org.apache.gravitino.exceptions.TableAlreadyExistsException;
+import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
 import software.amazon.awssdk.services.glue.model.AlreadyExistsException;
 import software.amazon.awssdk.services.glue.model.EntityNotFoundException;
 import software.amazon.awssdk.services.glue.model.GlueException;
@@ -49,7 +51,7 @@ final class GlueExceptionConverter {
     if (e instanceof InvalidInputException) {
       return new IllegalArgumentException(context + ": " + e.getMessage(), e);
     }
-    return new RuntimeException("Glue error: " + context, e);
+    return new RuntimeException("Glue error: " + context + ": " + 
awsErrorDetail(e), e);
   }
 
   /**
@@ -69,6 +71,32 @@ final class GlueExceptionConverter {
     if (e instanceof InvalidInputException) {
       return new IllegalArgumentException(context + ": " + e.getMessage(), e);
     }
-    return new RuntimeException("Glue error: " + context, e);
+    return new RuntimeException("Glue error: " + context + ": " + 
awsErrorDetail(e), e);
+  }
+
+  /**
+   * Renders the AWS-side detail of a Glue exception. AWS names the failing 
action and the resource
+   * there, which is what the caller needs to act on; the error code is 
prefixed so the failure can
+   * be classified at a glance.
+   *
+   * @param e the Glue exception to describe
+   * @return the AWS error code and message, or a best-effort description when 
they are unavailable
+   */
+  private static String awsErrorDetail(GlueException e) {
+    AwsErrorDetails details = e.awsErrorDetails();
+    if (details != null) {
+      String code = details.errorCode();
+      String message = details.errorMessage();
+      if (StringUtils.isNotBlank(code) && StringUtils.isNotBlank(message)) {
+        return "[" + code + "] " + message;
+      }
+      if (StringUtils.isNotBlank(message)) {
+        return message;
+      }
+      if (StringUtils.isNotBlank(code)) {
+        return "[" + code + "]";
+      }
+    }
+    return StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : 
e.getClass().getSimpleName();
   }
 }
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
new file mode 100644
index 0000000000..14ad7d557d
--- /dev/null
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueExceptionConverter.java
@@ -0,0 +1,181 @@
+/*
+ * 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.gravitino.catalog.glue;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
+import org.apache.gravitino.exceptions.TableAlreadyExistsException;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
+import software.amazon.awssdk.services.glue.model.AccessDeniedException;
+import software.amazon.awssdk.services.glue.model.AlreadyExistsException;
+import software.amazon.awssdk.services.glue.model.EntityNotFoundException;
+import software.amazon.awssdk.services.glue.model.GlueException;
+import software.amazon.awssdk.services.glue.model.InvalidInputException;
+
+/** Tests for {@link GlueExceptionConverter}. */
+public class TestGlueExceptionConverter {
+
+  private static final String IAM_MESSAGE =
+      "User: arn:aws:iam::123456789012:user/gravitino is not authorized to 
perform: "
+          + "glue:CreateDatabase on resource: "
+          + "arn:aws:glue:us-east-2:123456789012:database/drop_me3 "
+          + "because no identity-based policy allows the glue:CreateDatabase 
action";
+
+  @Test
+  public void testSchemaAccessDeniedKeepsAwsMessage() {
+    AccessDeniedException e =
+        AccessDeniedException.builder()
+            .message(IAM_MESSAGE)
+            .awsErrorDetails(
+                AwsErrorDetails.builder()
+                    .errorCode("AccessDeniedException")
+                    .errorMessage(IAM_MESSAGE)
+                    .build())
+            .build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema drop_me");
+
+    assertEquals(RuntimeException.class, converted.getClass());
+    assertSame(e, converted.getCause());
+    String message = converted.getMessage();
+    assertTrue(message.contains("schema drop_me"), message);
+    assertTrue(message.contains("[AccessDeniedException] "), message);
+    assertTrue(message.contains("glue:CreateDatabase"), message);
+    assertTrue(message.contains("database/drop_me3"), message);
+  }
+
+  @Test
+  public void testTableAccessDeniedKeepsAwsMessage() {
+    AccessDeniedException e =
+        AccessDeniedException.builder()
+            .message(IAM_MESSAGE)
+            .awsErrorDetails(
+                AwsErrorDetails.builder()
+                    .errorCode("AccessDeniedException")
+                    .errorMessage(IAM_MESSAGE)
+                    .build())
+            .build();
+
+    RuntimeException converted = GlueExceptionConverter.toTableException(e, 
"table ctas_test");
+
+    assertEquals(RuntimeException.class, converted.getClass());
+    assertSame(e, converted.getCause());
+    String message = converted.getMessage();
+    assertTrue(message.contains("table ctas_test"), message);
+    assertTrue(message.contains("AccessDeniedException"), message);
+    assertTrue(message.contains("glue:CreateDatabase"), message);
+  }
+
+  @Test
+  public void testErrorMessageAloneIsSurfaced() {
+    GlueException e =
+        (GlueException)
+            GlueException.builder()
+                .awsErrorDetails(
+                    AwsErrorDetails.builder().errorMessage("throttled by 
Glue").build())
+                .build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema db6a");
+
+    assertTrue(converted.getMessage().contains("schema db6a"), 
converted.getMessage());
+    assertTrue(converted.getMessage().contains("throttled by Glue"), 
converted.getMessage());
+  }
+
+  @Test
+  public void testErrorCodeAloneIsSurfaced() {
+    GlueException e =
+        (GlueException)
+            GlueException.builder()
+                .awsErrorDetails(
+                    
AwsErrorDetails.builder().errorCode("InternalServiceException").build())
+                .build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema db6a");
+
+    assertTrue(
+        converted.getMessage().contains("[InternalServiceException]"), 
converted.getMessage());
+  }
+
+  @Test
+  public void testFallsBackToExceptionMessageWithoutAwsErrorDetails() {
+    GlueException e = (GlueException) 
GlueException.builder().message("connection reset").build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema db6a");
+
+    assertTrue(converted.getMessage().contains("schema db6a"), 
converted.getMessage());
+    assertTrue(converted.getMessage().contains("connection reset"), 
converted.getMessage());
+  }
+
+  @Test
+  public void testFallsBackWhenAwsErrorDetailsAreBlank() {
+    GlueException e =
+        (GlueException)
+            GlueException.builder()
+                .message("connection reset")
+                
.awsErrorDetails(AwsErrorDetails.builder().errorCode("").errorMessage("").build())
+                .build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema db6a");
+
+    assertTrue(converted.getMessage().contains("connection reset"), 
converted.getMessage());
+  }
+
+  @Test
+  public void testFallsBackToExceptionTypeWithoutAnyMessage() {
+    GlueException e = (GlueException) GlueException.builder().build();
+
+    RuntimeException converted = GlueExceptionConverter.toSchemaException(e, 
"schema db6a");
+
+    assertTrue(converted.getMessage().contains("GlueException"), 
converted.getMessage());
+  }
+
+  @Test
+  public void testRecognisedExceptionsKeepTheirMapping() {
+    EntityNotFoundException notFound = 
EntityNotFoundException.builder().message("gone").build();
+    AlreadyExistsException exists = 
AlreadyExistsException.builder().message("dup").build();
+    InvalidInputException invalid = 
InvalidInputException.builder().message("bad name").build();
+
+    assertInstanceOf(
+        NoSuchSchemaException.class,
+        GlueExceptionConverter.toSchemaException(notFound, "schema db6a"));
+    assertInstanceOf(
+        SchemaAlreadyExistsException.class,
+        GlueExceptionConverter.toSchemaException(exists, "schema db6a"));
+    assertInstanceOf(
+        IllegalArgumentException.class,
+        GlueExceptionConverter.toSchemaException(invalid, "schema db6a"));
+
+    assertInstanceOf(
+        NoSuchTableException.class,
+        GlueExceptionConverter.toTableException(notFound, "table ctas_test"));
+    assertInstanceOf(
+        TableAlreadyExistsException.class,
+        GlueExceptionConverter.toTableException(exists, "table ctas_test"));
+    assertInstanceOf(
+        IllegalArgumentException.class,
+        GlueExceptionConverter.toTableException(invalid, "table ctas_test"));
+  }
+}

Reply via email to