From c8a2184c51cc92ec35c759eff68e614fc78fb2e6 Mon Sep 17 00:00:00 2001 From: Gaurav Purohit Date: Tue, 5 Jul 2022 18:57:45 +0530 Subject: [PATCH] feat: Error Details Improvement (#1929) feat: Error Details Improvement --- .../cloud/spanner/AbortedException.java | 12 ++- ...minRequestsPerMinuteExceededException.java | 12 ++- .../spanner/DatabaseNotFoundException.java | 13 ++- .../spanner/InstanceNotFoundException.java | 12 ++- .../spanner/SessionNotFoundException.java | 13 ++- .../cloud/spanner/SpannerException.java | 84 ++++++++++++++++++- .../spanner/SpannerExceptionFactory.java | 38 ++++++--- 7 files changed, 164 insertions(+), 20 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbortedException.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbortedException.java index b3fb0b05328..21b0bb2224a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbortedException.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbortedException.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import com.google.api.gax.rpc.ApiException; import javax.annotation.Nullable; /** @@ -34,6 +35,15 @@ public class AbortedException extends SpannerException { /** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */ AbortedException( DoNotConstructDirectly token, @Nullable String message, @Nullable Throwable cause) { - super(token, ErrorCode.ABORTED, IS_RETRYABLE, message, cause); + this(token, message, cause, null); + } + + /** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */ + AbortedException( + DoNotConstructDirectly token, + @Nullable String message, + @Nullable Throwable cause, + @Nullable ApiException apiException) { + super(token, ErrorCode.ABORTED, IS_RETRYABLE, message, cause, apiException); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AdminRequestsPerMinuteExceededException.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AdminRequestsPerMinuteExceededException.java index 11870c94d00..72d8b0ab15d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AdminRequestsPerMinuteExceededException.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AdminRequestsPerMinuteExceededException.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import com.google.api.gax.rpc.ApiException; import javax.annotation.Nullable; /** @@ -31,6 +32,15 @@ public class AdminRequestsPerMinuteExceededException extends SpannerException { /** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */ AdminRequestsPerMinuteExceededException( DoNotConstructDirectly token, @Nullable String message, @Nullable Throwable cause) { - super(token, ErrorCode.RESOURCE_EXHAUSTED, true, message, cause); + this(token, message, cause, null); + } + + /** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */ + AdminRequestsPerMinuteExceededException( + DoNotConstructDirectly token, + @Nullable String message, + @Nullable Throwable cause, + @Nullable ApiException apiException) { + super(token, ErrorCode.RESOURCE_EXHAUSTED, true, message, cause, apiException); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseNotFoundException.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseNotFoundException.java index aafd799103e..cc4a2e32f0b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseNotFoundException.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseNotFoundException.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import com.google.api.gax.rpc.ApiException; import com.google.cloud.spanner.SpannerException.ResourceNotFoundException; import com.google.rpc.ResourceInfo; import javax.annotation.Nullable; @@ -34,6 +35,16 @@ public class DatabaseNotFoundException extends ResourceNotFoundException { @Nullable String message, ResourceInfo resourceInfo, @Nullable Throwable cause) { - super(token, message, resourceInfo, cause); + this(token, message, resourceInfo, cause, null); + } + + /** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */ + DatabaseNotFoundException( + DoNotConstructDirectly token, + @Nullable String message, + ResourceInfo resourceInfo, + @Nullable Throwable cause, + @Nullable ApiException apiException) { + super(token, message, resourceInfo, cause, apiException); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceNotFoundException.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceNotFoundException.java index 6c179ca9b60..82c451f9475 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceNotFoundException.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/InstanceNotFoundException.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import com.google.api.gax.rpc.ApiException; import com.google.cloud.spanner.SpannerException.ResourceNotFoundException; import com.google.rpc.ResourceInfo; import javax.annotation.Nullable; @@ -34,6 +35,15 @@ public class InstanceNotFoundException extends ResourceNotFoundException { @Nullable String message, ResourceInfo resourceInfo, @Nullable Throwable cause) { - super(token, message, resourceInfo, cause); + this(token, message, resourceInfo, cause, null); + } + /** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */ + InstanceNotFoundException( + DoNotConstructDirectly token, + @Nullable String message, + ResourceInfo resourceInfo, + @Nullable Throwable cause, + @Nullable ApiException apiException) { + super(token, message, resourceInfo, cause, apiException); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionNotFoundException.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionNotFoundException.java index 4e3e08c5c21..f4a62b1954a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionNotFoundException.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionNotFoundException.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import com.google.api.gax.rpc.ApiException; import com.google.cloud.spanner.SpannerException.ResourceNotFoundException; import com.google.rpc.ResourceInfo; import javax.annotation.Nullable; @@ -34,6 +35,16 @@ public class SessionNotFoundException extends ResourceNotFoundException { @Nullable String message, ResourceInfo resourceInfo, @Nullable Throwable cause) { - super(token, message, resourceInfo, cause); + this(token, message, resourceInfo, cause, null); + } + + /** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */ + SessionNotFoundException( + DoNotConstructDirectly token, + @Nullable String message, + ResourceInfo resourceInfo, + @Nullable Throwable cause, + @Nullable ApiException apiException) { + super(token, message, resourceInfo, cause, apiException); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerException.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerException.java index 8c3af71547e..58076570c20 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerException.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerException.java @@ -16,6 +16,8 @@ package com.google.cloud.spanner; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.ErrorDetails; import com.google.cloud.grpc.BaseGrpcServiceException; import com.google.common.base.Preconditions; import com.google.protobuf.util.Durations; @@ -24,6 +26,7 @@ import io.grpc.Metadata; import io.grpc.Status; import io.grpc.protobuf.ProtoUtils; +import java.util.Map; import javax.annotation.Nullable; /** Base exception type for all exceptions produced by the Cloud Spanner service. */ @@ -36,8 +39,9 @@ public abstract static class ResourceNotFoundException extends SpannerException DoNotConstructDirectly token, @Nullable String message, ResourceInfo resourceInfo, - @Nullable Throwable cause) { - super(token, ErrorCode.NOT_FOUND, /* retryable */ false, message, cause); + @Nullable Throwable cause, + @Nullable ApiException apiException) { + super(token, ErrorCode.NOT_FOUND, /* retryable */ false, message, cause, apiException); this.resourceInfo = resourceInfo; } @@ -51,6 +55,7 @@ public String getResourceName() { ProtoUtils.keyForProto(RetryInfo.getDefaultInstance()); private final ErrorCode code; + private final ApiException apiException; /** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */ SpannerException( @@ -59,11 +64,23 @@ public String getResourceName() { boolean retryable, @Nullable String message, @Nullable Throwable cause) { + this(token, code, retryable, message, cause, null); + } + + /** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */ + SpannerException( + DoNotConstructDirectly token, + ErrorCode code, + boolean retryable, + @Nullable String message, + @Nullable Throwable cause, + @Nullable ApiException apiException) { super(message, cause, code.getCode(), retryable); if (token != DoNotConstructDirectly.ALLOWED) { throw new AssertionError("Do not construct directly: use SpannerExceptionFactory"); } this.code = Preconditions.checkNotNull(code); + this.apiException = apiException; } /** Returns the error code associated with this exception. */ @@ -95,4 +112,67 @@ static long extractRetryDelay(Throwable cause) { } return -1L; } + + /** + * Checks the underlying reason of the exception and if it's {@link ApiException} then return the + * reason otherwise null. + * + * @see Reason + * @return the reason of an error. + */ + public String getReason() { + if (this.apiException != null) { + return this.apiException.getReason(); + } + return null; + } + + /** + * Checks the underlying reason of the exception and if it's {@link ApiException} then return the + * specific domain otherwise null. + * + * @see Domain + * @return the logical grouping to which the "reason" belongs. + */ + public String getDomain() { + if (this.apiException != null) { + return this.apiException.getDomain(); + } + return null; + } + + /** + * Checks the underlying reason of the exception and if it's {@link ApiException} then return a + * map of key-value pairs otherwise null. + * + * @see Metadata + * @return the map of additional structured details about an error. + */ + public Map getMetadata() { + if (this.apiException != null) { + return this.apiException.getMetadata(); + } + return null; + } + + /** + * Checks the underlying reason of the exception and if it's {@link ApiException} then return the + * ErrorDetails otherwise null. + * + * @see Status + * @see Error + * Details + * @return An object containing getters for structured objects from error_details.proto. + */ + public ErrorDetails getErrorDetails() { + if (this.apiException != null) { + return this.apiException.getErrorDetails(); + } + return null; + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java index da17680d6db..2c52192d214 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java @@ -251,12 +251,15 @@ static StatusRuntimeException createAbortedExceptionWithRetryDelay( } static SpannerException newSpannerExceptionPreformatted( - ErrorCode code, @Nullable String message, @Nullable Throwable cause) { + ErrorCode code, + @Nullable String message, + @Nullable Throwable cause, + @Nullable ApiException apiException) { // This is the one place in the codebase that is allowed to call constructors directly. DoNotConstructDirectly token = DoNotConstructDirectly.ALLOWED; switch (code) { case ABORTED: - return new AbortedException(token, message, cause); + return new AbortedException(token, message, cause, apiException); case RESOURCE_EXHAUSTED: ErrorInfo info = extractErrorInfo(cause); if (info != null @@ -265,26 +268,35 @@ static SpannerException newSpannerExceptionPreformatted( && AdminRequestsPerMinuteExceededException.ADMIN_REQUESTS_LIMIT_VALUE.equals( info.getMetadataMap() .get(AdminRequestsPerMinuteExceededException.ADMIN_REQUESTS_LIMIT_KEY))) { - return new AdminRequestsPerMinuteExceededException(token, message, cause); + return new AdminRequestsPerMinuteExceededException(token, message, cause, apiException); } case NOT_FOUND: ResourceInfo resourceInfo = extractResourceInfo(cause); if (resourceInfo != null) { switch (resourceInfo.getResourceType()) { case SESSION_RESOURCE_TYPE: - return new SessionNotFoundException(token, message, resourceInfo, cause); + return new SessionNotFoundException( + token, message, resourceInfo, cause, apiException); case DATABASE_RESOURCE_TYPE: - return new DatabaseNotFoundException(token, message, resourceInfo, cause); + return new DatabaseNotFoundException( + token, message, resourceInfo, cause, apiException); case INSTANCE_RESOURCE_TYPE: - return new InstanceNotFoundException(token, message, resourceInfo, cause); + return new InstanceNotFoundException( + token, message, resourceInfo, cause, apiException); } } // Fall through to the default. default: - return new SpannerException(token, code, isRetryable(code, cause), message, cause); + return new SpannerException( + token, code, isRetryable(code, cause), message, cause, apiException); } } + static SpannerException newSpannerExceptionPreformatted( + ErrorCode code, @Nullable String message, @Nullable Throwable cause) { + return newSpannerExceptionPreformatted(code, message, cause, null); + } + private static SpannerException fromApiException(ApiException exception) { Status.Code code; if (exception.getStatusCode() instanceof GrpcStatusCode) { @@ -295,12 +307,12 @@ private static SpannerException fromApiException(ApiException exception) { code = Status.Code.UNKNOWN; } ErrorCode errorCode = ErrorCode.fromGrpcStatus(Status.fromCode(code)); - if (exception.getCause() != null) { - return SpannerExceptionFactory.newSpannerException( - errorCode, exception.getMessage(), exception.getCause()); - } else { - return SpannerExceptionFactory.newSpannerException(errorCode, exception.getMessage()); - } + + return SpannerExceptionFactory.newSpannerExceptionPreformatted( + errorCode, + formatMessage(errorCode, exception.getMessage()), + exception.getCause(), + exception); } private static boolean isRetryable(ErrorCode code, @Nullable Throwable cause) {