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"));
+ }
+}