This is an automated email from the ASF dual-hosted git repository.
dimas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push:
new 5c38780e Handle more cloud errors (#864)
5c38780e is described below
commit 5c38780e6130dd5c18346437bc2a6a27e41e9bf6
Author: Andrew Guterman <[email protected]>
AuthorDate: Mon Jan 27 19:55:14 2025 -0800
Handle more cloud errors (#864)
* Forward cloud exception status codes
* Update mappings
* Add 504 => 500 test
* 3xx => 422, 408 => 408, 504 => 504, 5xx => 502, cleanup tests
---
.../src/main/kotlin/polaris-quarkus.gradle.kts | 1 -
.../quarkus/IcebergExceptionMapperTest.java | 62 +++++++++--
service/common/build.gradle.kts | 2 +
.../service/exception/IcebergExceptionMapper.java | 117 +++++++++++++++------
4 files changed, 138 insertions(+), 44 deletions(-)
diff --git a/build-logic/src/main/kotlin/polaris-quarkus.gradle.kts
b/build-logic/src/main/kotlin/polaris-quarkus.gradle.kts
index 20757350..21abf364 100644
--- a/build-logic/src/main/kotlin/polaris-quarkus.gradle.kts
+++ b/build-logic/src/main/kotlin/polaris-quarkus.gradle.kts
@@ -17,7 +17,6 @@
* under the License.
*/
-import gradle.kotlin.dsl.accessors._fa00c0b20184971a79f32516372275b9.testing
import org.gradle.api.attributes.TestSuiteType
import org.gradle.api.plugins.jvm.JvmTestSuite
import org.gradle.kotlin.dsl.register
diff --git
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/IcebergExceptionMapperTest.java
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/IcebergExceptionMapperTest.java
index 2b5a2661..ee3ec3e3 100644
---
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/IcebergExceptionMapperTest.java
+++
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/IcebergExceptionMapperTest.java
@@ -16,13 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.polaris.service.dropwizard;
+package org.apache.polaris.service.quarkus;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import com.azure.core.exception.AzureException;
+import com.azure.core.exception.HttpResponseException;
+import com.azure.core.http.HttpResponse;
import com.google.cloud.storage.StorageException;
import jakarta.ws.rs.core.Response;
+import java.util.Map;
import java.util.stream.Stream;
import org.apache.polaris.service.exception.IcebergExceptionMapper;
import org.junit.jupiter.params.ParameterizedTest;
@@ -33,14 +38,40 @@ import software.amazon.awssdk.services.s3.model.S3Exception;
class IcebergExceptionMapperTest {
static Stream<Arguments> fileIOExceptionMapping() {
- return Stream.of(
- Arguments.of(new AzureException("Unknown"), 500),
- Arguments.of(new AzureException("Forbidden"), 403),
- Arguments.of(new AzureException("FORBIDDEN"), 403),
- Arguments.of(new AzureException("Not Authorized"), 403),
- Arguments.of(new AzureException("Access Denied"), 403),
- Arguments.of(S3Exception.builder().message("Access denied").build(),
403),
- Arguments.of(new StorageException(1, "access denied"), 403));
+ Map<Integer, Integer> cloudCodeMappings =
+ Map.of(
+ // Map of HTTP code returned from a cloud provider to the HTTP
code Polaris is expected
+ // to return
+ 302, 422,
+ 400, 400,
+ 401, 403,
+ 403, 403,
+ 404, 400,
+ 408, 408,
+ 429, 429,
+ 503, 502,
+ 504, 504);
+
+ return Stream.concat(
+ Stream.of(
+ Arguments.of(new AzureException("Unknown"), 500),
+ Arguments.of(new AzureException("Forbidden"), 403),
+ Arguments.of(new AzureException("FORBIDDEN"), 403),
+ Arguments.of(new AzureException("Not Authorized"), 403),
+ Arguments.of(new AzureException("Access Denied"), 403),
+ Arguments.of(S3Exception.builder().message("Access
denied").build(), 403),
+ Arguments.of(new StorageException(1, "access denied"), 403)),
+ cloudCodeMappings.entrySet().stream()
+ .flatMap(
+ entry ->
+ Stream.of(
+ Arguments.of(
+ new HttpResponseException("",
mockAzureResponse(entry.getKey()), ""),
+ entry.getValue()),
+ Arguments.of(
+
S3Exception.builder().message("").statusCode(entry.getKey()).build(),
+ entry.getValue()),
+ Arguments.of(new StorageException(entry.getKey(), ""),
entry.getValue()))));
}
@ParameterizedTest
@@ -52,4 +83,17 @@ class IcebergExceptionMapperTest {
assertThat(response.getEntity()).extracting("message").isEqualTo(ex.getMessage());
}
}
+
+ /**
+ * Creates a mock of the Azure-specific HttpResponse object, as it's quite
difficult to construct
+ * a "real" one.
+ *
+ * @param statusCode
+ * @return
+ */
+ private static HttpResponse mockAzureResponse(int statusCode) {
+ HttpResponse res = mock(HttpResponse.class);
+ when(res.getStatusCode()).thenReturn(statusCode);
+ return res;
+ }
}
diff --git a/service/common/build.gradle.kts b/service/common/build.gradle.kts
index a44e8e13..79d2809c 100644
--- a/service/common/build.gradle.kts
+++ b/service/common/build.gradle.kts
@@ -85,6 +85,8 @@ dependencies {
implementation(platform(libs.azuresdk.bom))
implementation("com.azure:azure-core")
+ implementation("com.azure:azure-storage-blob")
+ implementation("com.azure:azure-storage-file-datalake")
testImplementation(platform(libs.junit.bom))
testImplementation("org.junit.jupiter:junit-jupiter")
diff --git
a/service/common/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
b/service/common/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
index 6a210783..0504dd17 100644
---
a/service/common/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
+++
b/service/common/src/main/java/org/apache/polaris/service/exception/IcebergExceptionMapper.java
@@ -19,12 +19,14 @@
package org.apache.polaris.service.exception;
import com.azure.core.exception.AzureException;
+import com.azure.core.exception.HttpResponseException;
import com.google.cloud.storage.StorageException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import java.util.Arrays;
@@ -73,40 +75,7 @@ public class IcebergExceptionMapper implements
ExceptionMapper<RuntimeException>
@Override
public Response toResponse(RuntimeException runtimeException) {
LOGGER.info("Handling runtimeException {}", runtimeException.getMessage());
- int responseCode =
- switch (runtimeException) {
- case NoSuchNamespaceException e ->
Response.Status.NOT_FOUND.getStatusCode();
- case NoSuchIcebergTableException e ->
Response.Status.NOT_FOUND.getStatusCode();
- case NoSuchTableException e ->
Response.Status.NOT_FOUND.getStatusCode();
- case NoSuchViewException e ->
Response.Status.NOT_FOUND.getStatusCode();
- case NotFoundException e ->
Response.Status.NOT_FOUND.getStatusCode();
- case AlreadyExistsException e ->
Response.Status.CONFLICT.getStatusCode();
- case CommitFailedException e ->
Response.Status.CONFLICT.getStatusCode();
- case UnprocessableEntityException e -> 422;
- case CherrypickAncestorCommitException e ->
Response.Status.BAD_REQUEST.getStatusCode();
- case CommitStateUnknownException e ->
Response.Status.BAD_REQUEST.getStatusCode();
- case DuplicateWAPCommitException e ->
Response.Status.BAD_REQUEST.getStatusCode();
- case ForbiddenException e ->
Response.Status.FORBIDDEN.getStatusCode();
- case jakarta.ws.rs.ForbiddenException e ->
Response.Status.FORBIDDEN.getStatusCode();
- case NotAuthorizedException e ->
Response.Status.UNAUTHORIZED.getStatusCode();
- case NamespaceNotEmptyException e ->
Response.Status.BAD_REQUEST.getStatusCode();
- case ValidationException e ->
Response.Status.BAD_REQUEST.getStatusCode();
- case ServiceUnavailableException e ->
Response.Status.SERVICE_UNAVAILABLE.getStatusCode();
- case RuntimeIOException e ->
Response.Status.SERVICE_UNAVAILABLE.getStatusCode();
- case ServiceFailureException e ->
Response.Status.SERVICE_UNAVAILABLE.getStatusCode();
- case CleanableFailure e ->
Response.Status.BAD_REQUEST.getStatusCode();
- case RESTException e ->
Response.Status.SERVICE_UNAVAILABLE.getStatusCode();
- case IllegalArgumentException e ->
Response.Status.BAD_REQUEST.getStatusCode();
- case UnsupportedOperationException e ->
Response.Status.NOT_ACCEPTABLE.getStatusCode();
- case S3Exception e when doesAnyThrowableContainAccessDeniedHint(e) ->
- Response.Status.FORBIDDEN.getStatusCode();
- case AzureException e when
doesAnyThrowableContainAccessDeniedHint(e) ->
- Response.Status.FORBIDDEN.getStatusCode();
- case StorageException e when
doesAnyThrowableContainAccessDeniedHint(e) ->
- Response.Status.FORBIDDEN.getStatusCode();
- case WebApplicationException e -> e.getResponse().getStatus();
- default -> Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();
- };
+ int responseCode = mapExceptionToResponseCode(runtimeException);
if (responseCode == Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())
{
LOGGER.error("Unhandled exception returning INTERNAL_SERVER_ERROR",
runtimeException);
}
@@ -144,4 +113,84 @@ public class IcebergExceptionMapper implements
ExceptionMapper<RuntimeException>
public static Collection<String> getAccessDeniedHints() {
return ImmutableSet.copyOf(ACCESS_DENIED_HINTS);
}
+
+ static int mapExceptionToResponseCode(RuntimeException rex) {
+ // Cloud exceptions
+ if (rex instanceof S3Exception
+ || rex instanceof AzureException
+ || rex instanceof StorageException) {
+ return mapCloudExceptionToResponseCode(rex);
+ }
+
+ // Non-cloud exceptions
+ return switch (rex) {
+ case NoSuchNamespaceException e -> Status.NOT_FOUND.getStatusCode();
+ case NoSuchIcebergTableException e -> Status.NOT_FOUND.getStatusCode();
+ case NoSuchTableException e -> Status.NOT_FOUND.getStatusCode();
+ case NoSuchViewException e -> Status.NOT_FOUND.getStatusCode();
+ case NotFoundException e -> Status.NOT_FOUND.getStatusCode();
+ case AlreadyExistsException e -> Status.CONFLICT.getStatusCode();
+ case CommitFailedException e -> Status.CONFLICT.getStatusCode();
+ case UnprocessableEntityException e -> 422;
+ case CherrypickAncestorCommitException e ->
Status.BAD_REQUEST.getStatusCode();
+ case CommitStateUnknownException e -> Status.BAD_REQUEST.getStatusCode();
+ case DuplicateWAPCommitException e -> Status.BAD_REQUEST.getStatusCode();
+ case ForbiddenException e -> Status.FORBIDDEN.getStatusCode();
+ case jakarta.ws.rs.ForbiddenException e ->
Status.FORBIDDEN.getStatusCode();
+ case NotAuthorizedException e -> Status.UNAUTHORIZED.getStatusCode();
+ case NamespaceNotEmptyException e -> Status.BAD_REQUEST.getStatusCode();
+ case ValidationException e -> Status.BAD_REQUEST.getStatusCode();
+ case ServiceUnavailableException e ->
Status.SERVICE_UNAVAILABLE.getStatusCode();
+ case RuntimeIOException e -> Status.SERVICE_UNAVAILABLE.getStatusCode();
+ case ServiceFailureException e ->
Status.SERVICE_UNAVAILABLE.getStatusCode();
+ case CleanableFailure e -> Status.BAD_REQUEST.getStatusCode();
+ case RESTException e -> Status.SERVICE_UNAVAILABLE.getStatusCode();
+ case IllegalArgumentException e -> Status.BAD_REQUEST.getStatusCode();
+ case UnsupportedOperationException e ->
Status.NOT_ACCEPTABLE.getStatusCode();
+ case WebApplicationException e -> e.getResponse().getStatus();
+ default -> Status.INTERNAL_SERVER_ERROR.getStatusCode();
+ };
+ }
+
+ static int mapCloudExceptionToResponseCode(RuntimeException rex) {
+ if (doesAnyThrowableContainAccessDeniedHint(rex)) {
+ return Status.FORBIDDEN.getStatusCode();
+ }
+
+ int httpCode =
+ switch (rex) {
+ case S3Exception s3e -> s3e.statusCode();
+ case HttpResponseException hre -> hre.getResponse().getStatusCode();
+ case StorageException se -> se.getCode();
+ default -> -1;
+ };
+ Status httpStatus = Status.fromStatusCode(httpCode);
+ Status.Family httpFamily = Status.Family.familyOf(httpCode);
+
+ if (httpStatus == Status.NOT_FOUND) {
+ return Status.BAD_REQUEST.getStatusCode();
+ }
+ if (httpStatus == Status.UNAUTHORIZED) {
+ return Status.FORBIDDEN.getStatusCode();
+ }
+ if (httpStatus == Status.BAD_REQUEST
+ || httpStatus == Status.FORBIDDEN
+ || httpStatus == Status.REQUEST_TIMEOUT
+ || httpStatus == Status.TOO_MANY_REQUESTS
+ || httpStatus == Status.GATEWAY_TIMEOUT) {
+ return httpCode;
+ }
+ if (httpFamily == Status.Family.REDIRECTION) {
+ // Currently Polaris doesn't know how to follow redirects from cloud
providers, thus clients
+ // shouldn't expect it to.
+ // This is a 4xx error to indicate that the client may be able to
resolve this by changing
+ // some data, such as their catalog's region.
+ return 422;
+ }
+ if (httpFamily == Status.Family.SERVER_ERROR) {
+ return Status.BAD_GATEWAY.getStatusCode();
+ }
+
+ return Status.INTERNAL_SERVER_ERROR.getStatusCode();
+ }
}