Skip to content

feat: have DatastoreException extend BaseHttpServiceException, convert gRPC status codes to existing Datastore codes #1409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-datastore'
If you are using Gradle without BOM, add this to your dependencies:

```Groovy
implementation 'com.google.cloud:google-cloud-datastore:2.19.0'
implementation 'com.google.cloud:google-cloud-datastore:2.19.1'
```

If you are using SBT, add this to your dependencies:

```Scala
libraryDependencies += "com.google.cloud" % "google-cloud-datastore" % "2.19.0"
libraryDependencies += "com.google.cloud" % "google-cloud-datastore" % "2.19.1"
```
<!-- {x-version-update-end} -->

Expand Down Expand Up @@ -380,7 +380,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
[kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-datastore/java11.html
[stability-image]: https://img.shields.io/badge/stability-stable-green
[maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-datastore.svg
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-datastore/2.19.0
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-datastore/2.19.1
[authentication]: https://github.com/googleapis/google-cloud-java#authentication
[auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
[predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@

package com.google.cloud.datastore;

import static com.google.cloud.BaseServiceException.isRetryable;

import com.google.api.gax.grpc.GrpcStatusCode;
import com.google.api.gax.rpc.ApiException;
import com.google.api.gax.rpc.ErrorDetails;
import com.google.api.gax.rpc.StatusCode;
import com.google.cloud.BaseServiceException;
import com.google.cloud.RetryHelper.RetryHelperException;
import com.google.cloud.grpc.BaseGrpcServiceException;
import com.google.cloud.http.BaseHttpServiceException;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import io.grpc.StatusException;
import io.grpc.StatusRuntimeException;
import java.io.IOException;
import java.util.Map;
import java.util.Set;

/**
Expand All @@ -34,7 +35,7 @@
* @see <a href="https://cloud.google.com/datastore/docs/concepts/errors#Error_Codes">Google Cloud
* Datastore error codes</a>
*/
public final class DatastoreException extends BaseGrpcServiceException {
public final class DatastoreException extends BaseHttpServiceException {

// see https://cloud.google.com/datastore/docs/concepts/errors#Error_Codes"
private static final Set<Error> RETRYABLE_ERRORS =
Expand All @@ -43,110 +44,106 @@ public final class DatastoreException extends BaseGrpcServiceException {
new Error(4, "DEADLINE_EXCEEDED", false),
new Error(14, "UNAVAILABLE", true));
private static final long serialVersionUID = 2663750991205874435L;
private String reason;
private ApiException apiException;

public DatastoreException(int code, String message, String reason) {
this(code, message, reason, true, null);
this.reason = reason;
}

public DatastoreException(int code, String message, Throwable cause) {
super(code, message, null, true, RETRYABLE_ERRORS, cause);
}

public DatastoreException(int code, String message, String reason, Throwable cause) {
super(message, cause, code, isRetryable(code, reason, true, RETRYABLE_ERRORS));
this.reason = reason;
super(code, message, reason, true, RETRYABLE_ERRORS, cause);
}

public DatastoreException(
int code, String message, String reason, boolean idempotent, Throwable cause) {
super(message, cause, code, isRetryable(code, reason, idempotent, RETRYABLE_ERRORS));
this.reason = reason;
super(code, message, reason, idempotent, RETRYABLE_ERRORS, cause);
}

public DatastoreException(IOException exception) {
super(exception, true);
}

public DatastoreException(ApiException apiException) {
super(apiException);
this.apiException = apiException;
super(exception, true, RETRYABLE_ERRORS);
}

/**
* Checks the underlying reason of the exception and if it's {@link ApiException} then return the
* specific domain otherwise null.
* Translate RetryHelperException to the DatastoreException that caused the error. This method
* will always throw an exception.
*
* @see <a
* href="https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto#L125">Domain</a>
* @return the logical grouping to which the "reason" belongs.
* @throws DatastoreException when {@code ex} was caused by a {@code DatastoreException}
*/
public String getDomain() {
if (this.apiException != null) {
return this.apiException.getDomain();
}
return null;
static DatastoreException translateAndThrow(RetryHelperException ex) {
BaseServiceException.translate(ex);
throw transformThrowable(ex);
}

/**
* Checks the underlying reason of the exception and if it's {@link ApiException} then return a
* map of key-value pairs otherwise null.
*
* @see <a
* href="https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto#L135">Metadata</a>
* @return the map of additional structured details about an error.
*/
public Map<String, String> getMetadata() {
if (this.apiException != null) {
return this.apiException.getMetadata();
static BaseServiceException transformThrowable(Throwable t) {
if (t instanceof BaseServiceException) {
return (BaseServiceException) t;
}
return null;
if (t.getCause() instanceof BaseServiceException) {
return (BaseServiceException) t.getCause();
}
if (t instanceof ApiException) {
return asDatastoreException((ApiException) t);
}
if (t.getCause() instanceof ApiException) {
return asDatastoreException((ApiException) t.getCause());
}
return getDatastoreException(t);
}

/**
* Checks the underlying reason of the exception and if it's {@link ApiException} then return the
* ErrorDetails otherwise null.
*
* @see <a
* href="https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto">Status</a>
* @see <a
* href="https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto">Error
* Details</a>
* @return An object containing getters for structured objects from error_details.proto.
*/
public ErrorDetails getErrorDetails() {
if (this.apiException != null) {
return this.apiException.getErrorDetails();
private static DatastoreException getDatastoreException(Throwable t) {
// unwrap a RetryHelperException if that is what is being translated
if (t instanceof RetryHelperException) {
return new DatastoreException(UNKNOWN_CODE, t.getMessage(), null, t.getCause());
}
return null;
return new DatastoreException(UNKNOWN_CODE, t.getMessage(), t);
}

/**
* Checks the underlying reason of the exception and if it's {@link ApiException} then return the
* reason otherwise null/custom reason.
*
* @see <a
* href="https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto#L117">Reason</a>
* @return the reason of an error.
*/
@Override
public String getReason() {
if (this.apiException != null) {
return this.apiException.getReason();
static DatastoreException asDatastoreException(ApiException apiEx) {
int datastoreStatusCode = 0;
StatusCode statusCode = apiEx.getStatusCode();
if (statusCode instanceof GrpcStatusCode) {
GrpcStatusCode gsc = (GrpcStatusCode) statusCode;
datastoreStatusCode =
GrpcToDatastoreCodeTranslation.grpcCodeToDatastoreStatusCode(gsc.getTransportCode());
}

// If there is a gRPC exception in our cause change pull it's error message up to be our
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// If there is a gRPC exception in our cause change pull it's error message up to be our
// If there is a gRPC exception in our cause, pull it's error message up to be our

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in subsequent commit.

// message otherwise, create a generic error message with the status code.
String statusCodeName = statusCode.getCode().name();
String statusExceptionMessage = getStatusExceptionMessage(apiEx);

String message;
if (statusExceptionMessage != null) {
message = statusCodeName + ": " + statusExceptionMessage;
} else {
message = "Error: " + statusCodeName;
}

String reason = "";
if (Strings.isNullOrEmpty(apiEx.getReason())) {
if (apiEx.getStatusCode() != null) {
reason = apiEx.getStatusCode().getCode().name();
}
}
return this.reason;
// It'd be better to use ExceptionData and BaseServiceException#<init>(ExceptionData) but,
// BaseHttpServiceException does not pass that through so we're stuck using this for now.
// TODO: When we can break the coupling to BaseHttpServiceException replace this
return new DatastoreException(datastoreStatusCode, message, reason, apiEx);
}

/**
* Translate RetryHelperException to the DatastoreException that caused the error. This method
* will always throw an exception.
*
* @throws DatastoreException when {@code ex} was caused by a {@code DatastoreException}
*/
static DatastoreException translateAndThrow(RetryHelperException ex) {
BaseServiceException.translate(ex);
if (ex.getCause() instanceof ApiException) {
throw new DatastoreException((ApiException) ex.getCause());
private static String getStatusExceptionMessage(Exception apiEx) {
if (apiEx.getMessage() != null) {
return apiEx.getMessage();
} else {
Throwable cause = apiEx.getCause();
if (cause instanceof StatusRuntimeException || cause instanceof StatusException) {
return cause.getMessage();
}
return null;
}
throw new DatastoreException(UNKNOWN_CODE, ex.getMessage(), null, ex.getCause());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2024 Google LLC
*
* Licensed 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 com.google.cloud.datastore;

import com.google.api.gax.grpc.GrpcStatusCode;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.rpc.Code;
import io.grpc.Status;
import java.util.Map;
import java.util.function.Function;

final class GrpcToDatastoreCodeTranslation {
/** Mappings between gRPC status codes and their corresponding code numbers. */
private static final ImmutableList<StatusCodeMapping> STATUS_CODE_MAPPINGS =
ImmutableList.of(
StatusCodeMapping.of(Code.OK.getNumber(), Status.Code.OK),
StatusCodeMapping.of(Code.DATA_LOSS.getNumber(), Status.Code.DATA_LOSS),
StatusCodeMapping.of(Code.INVALID_ARGUMENT.getNumber(), Status.Code.INVALID_ARGUMENT),
StatusCodeMapping.of(Code.OUT_OF_RANGE.getNumber(), Status.Code.OUT_OF_RANGE),
StatusCodeMapping.of(Code.UNAUTHENTICATED.getNumber(), Status.Code.UNAUTHENTICATED),
StatusCodeMapping.of(Code.PERMISSION_DENIED.getNumber(), Status.Code.PERMISSION_DENIED),
StatusCodeMapping.of(Code.NOT_FOUND.getNumber(), Status.Code.NOT_FOUND),
StatusCodeMapping.of(Code.ALREADY_EXISTS.getNumber(), Status.Code.ALREADY_EXISTS),
StatusCodeMapping.of(
Code.FAILED_PRECONDITION.getNumber(), Status.Code.FAILED_PRECONDITION),
StatusCodeMapping.of(Code.RESOURCE_EXHAUSTED.getNumber(), Status.Code.RESOURCE_EXHAUSTED),
StatusCodeMapping.of(Code.INTERNAL.getNumber(), Status.Code.INTERNAL),
StatusCodeMapping.of(Code.UNIMPLEMENTED.getNumber(), Status.Code.UNIMPLEMENTED),
StatusCodeMapping.of(Code.UNAVAILABLE.getNumber(), Status.Code.UNAVAILABLE),
StatusCodeMapping.of(Code.DEADLINE_EXCEEDED.getNumber(), Status.Code.DEADLINE_EXCEEDED),
StatusCodeMapping.of(Code.ABORTED.getNumber(), Status.Code.ABORTED),
StatusCodeMapping.of(Code.CANCELLED.getNumber(), Status.Code.CANCELLED),
StatusCodeMapping.of(Code.UNKNOWN.getNumber(), Status.Code.UNKNOWN));

/** Index our {@link StatusCodeMapping} for constant time lookup by {@link Status.Code} */
private static final Map<Status.Code, StatusCodeMapping> GRPC_CODE_INDEX =
STATUS_CODE_MAPPINGS.stream()
.collect(
ImmutableMap.toImmutableMap(StatusCodeMapping::getGrpcCode, Function.identity()));

static int grpcCodeToDatastoreStatusCode(Status.Code code) {
StatusCodeMapping found = GRPC_CODE_INDEX.get(code);
// theoretically it's possible for gRPC to add a new code we haven't mapped here, if this
// happens fall through to our default of 0
if (found != null) {
return found.getDatastoreCode();
} else {
return 0;
}
}

/**
* Simple tuple class to bind together our corresponding http status code and {@link Status.Code}
* while providing easy access to the correct {@link GrpcStatusCode} where necessary.
*/
private static final class StatusCodeMapping {

private final int datastoreCode;

private final Status.Code grpcCode;

private StatusCodeMapping(int datastoreCode, Status.Code grpcCode) {
this.datastoreCode = datastoreCode;
this.grpcCode = grpcCode;
}

public int getDatastoreCode() {
return datastoreCode;
}

public Status.Code getGrpcCode() {
return grpcCode;
}

static StatusCodeMapping of(int datastoreCode, Status.Code grpcCode) {
return new StatusCodeMapping(datastoreCode, grpcCode);
}
}
}
Loading
Loading