Skip to content
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

Add the dapr runtime returned error details to the Java DaprException #998

Merged
merged 19 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
GOPROXY: https://proxy.golang.org
JDK_VER: ${{ matrix.java }}
DAPR_CLI_VER: 1.12.0
DAPR_RUNTIME_VER: 1.12.4
DAPR_RUNTIME_VER: 1.13.0-rc.2
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.12.0/install/install.sh
DAPR_CLI_REF:
DAPR_REF:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
GOPROXY: https://proxy.golang.org
JDK_VER: ${{ matrix.java }}
DAPR_CLI_VER: 1.12.0
DAPR_RUNTIME_VER: 1.12.4
DAPR_RUNTIME_VER: 1.13.0-rc.2
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.12.0/install/install.sh
DAPR_CLI_REF:
DAPR_REF:
Expand Down
22 changes: 22 additions & 0 deletions daprdocs/content/en/java-sdk-docs/java-client/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,29 @@ If your Dapr instance is configured to require the `DAPR_API_TOKEN` environment
set it in the environment and the client will use it automatically.
You can read more about Dapr API token authentication [here](https://docs.dapr.io/operations/security/api-token/).

#### Error Handling

Initially, errors in Dapr followed the Standard gRPC error model. However, to provide more detailed and informative error
messages, in version 1.13 an enhanced error model has been introduced which aligns with the gRPC Richer error model. In
response, the Java SDK extended the DaprException to include the error details that were added in Dapr.

Example of handling the DaprException and consuming the error details when using the Dapr Java SDK:

```java
...
try {
client.publishEvent("unknown_pubsub", "mytopic", "mydata").block();
} catch (DaprException exception) {
System.out.println("Dapr exception's error code: " + exception.getErrorCode());
System.out.println("Dapr exception's message: " + exception.getMessage());
// DaprException now contains `getStatusDetails()` to include more details about the error from Dapr runtime.
System.out.println("Dapr exception's reason: " + exception.getStatusDetails().get(
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
"reason",
TypeRef.STRING));
}
...
```

## Building blocks

Expand Down
19 changes: 13 additions & 6 deletions examples/src/main/java/io/dapr/examples/exception/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@

import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.exceptions.DaprErrorDetails;
import io.dapr.exceptions.DaprException;
import io.dapr.utils.TypeRef;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* 1. Build and install jars:
Expand All @@ -33,17 +40,17 @@ public class Client {
*/
public static void main(String[] args) throws Exception {
try (DaprClient client = new DaprClientBuilder().build()) {

try {
client.getState("Unknown state store", "myKey", String.class).block();
client.publishEvent("unknown_pubsub", "mytopic", "mydata").block();
Copy link
Member

Choose a reason for hiding this comment

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

Clarifying my change: keeping this example simple.

} catch (DaprException exception) {
System.out.println("Error code: " + exception.getErrorCode());
System.out.println("Error message: " + exception.getMessage());

exception.printStackTrace();
System.out.println("Reason: " + exception.getStatusDetails().get(
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
"reason",
TypeRef.STRING));
}

System.out.println("Done");
}
System.out.println("Done");
}
}
74 changes: 30 additions & 44 deletions examples/src/main/java/io/dapr/examples/exception/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,52 +23,49 @@ cd java-sdk
Then build the Maven project:

```sh
# make sure you are in the `java-sdk` directory.
mvn install
# make sure you are in the `java-sdk` (root) directory.
./mvnw clean install
```

Then get into the examples directory:
```sh
cd examples
```

### Running the StateClient
This example uses the Java SDK Dapr client in order to perform an invalid operation, causing Dapr runtime to return an error. See the code snippet below:
### Understanding the code

This example uses the Java SDK Dapr client in order to perform an invalid operation, causing Dapr runtime to return an error. See the code snippet below, from `Client.java`:

```java
public class Client {

public static void main(String[] args) throws Exception {
try (DaprClient client = new DaprClientBuilder().build()) {

try {
client.getState("Unknown state store", "myKey", String.class).block();
client.publishEvent("unknown_pubsub", "mytopic", "mydata").block();
} catch (DaprException exception) {
System.out.println("Error code: " + exception.getErrorCode());
System.out.println("Error message: " + exception.getMessage());

exception.printStackTrace();
System.out.println("Dapr exception's error code: " + exception.getErrorCode());
System.out.println("Dapr exception's message: " + exception.getMessage());
System.out.println("Dapr exception's reason: " + exception.getStatusDetails().get(
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
"reason",
TypeRef.STRING));
}

System.out.println("Done");
}
System.out.println("Done");
}

}
```
The code uses the `DaprClient` created by the `DaprClientBuilder`. It tries to get a state from state store, but provides an unknown state store. It causes the Dapr sidecar to return an error, which is converted to a `DaprException` to the application. To be compatible with Project Reactor, `DaprException` extends from `RuntimeException` - making it an unchecked exception. Applications might also get an `IllegalArgumentException` when invoking methods with invalid input parameters that are validated at the client side.

The Dapr client is also within a try-with-resource block to properly close the client at the end.

### Running the example

Run this example with the following command:

<!-- STEP
name: Run exception example
expected_stdout_lines:
- '== APP == Error code: INVALID_ARGUMENT'
- '== APP == Error message: INVALID_ARGUMENT: state store Unknown state store is not found'
- '== APP == Error message: INVALID_ARGUMENT: pubsub unknown_pubsub is not found'
- '== APP == Reason: DAPR_PUBSUB_NOT_FOUND'
background: true
sleep: 5
-->
Expand All @@ -79,41 +76,30 @@ dapr run --app-id exception-example -- java -jar target/dapr-java-sdk-examples-e

<!-- END_STEP -->

Once running, the OutputBindingExample should print the output as follows:
Once running, the State Client Example should print the output as follows:

```txt
== APP == Error code: INVALID_ARGUMENT

== APP == Error message: INVALID_ARGUMENT: state store Unknown state store is not found

== APP == io.dapr.exceptions.DaprException: INVALID_ARGUMENT: state store Unknown state store is not found

== APP == at io.dapr.exceptions.DaprException.propagate(DaprException.java:168)

== APP == at io.dapr.client.DaprClientGrpc$2.onError(DaprClientGrpc.java:716)
== APP == Error code: ERR_PUBSUB_NOT_FOUND

== APP == at io.grpc.stub.ClientCalls$StreamObserverToCallListenerAdapter.onClose(ClientCalls.java:478)
== APP == Error message: ERR_PUBSUB_NOT_FOUND: pubsub unknown_pubsub is not found

== APP == at io.grpc.internal.DelayedClientCall$DelayedListener$3.run(DelayedClientCall.java:464)

== APP == at io.grpc.internal.DelayedClientCall$DelayedListener.delayOrExecute(DelayedClientCall.java:428)

== APP == at io.grpc.internal.DelayedClientCall$DelayedListener.onClose(DelayedClientCall.java:461)

== APP == at io.grpc.internal.ClientCallImpl.closeObserver(ClientCallImpl.java:617)

== APP == at io.grpc.internal.ClientCallImpl.access$300(ClientCallImpl.java:70)

== APP == at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInternal(ClientCallImpl.java:803)
== APP == Reason: DAPR_PUBSUB_NOT_FOUND
...

== APP == at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInContext(ClientCallImpl.java:782)
```

== APP == at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37)
### Debug

== APP == at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:123)
...
You can further explore all the error details returned in the `DaprException` class.
Before running it in your favorite IDE (like IntelliJ), compile and run the Dapr sidecar first.

1. Pre-req:
```sh
# make sure you are in the `java-sdk` (root) directory.
./mvnw clean install
```
2. From the examples directory, run: `dapr run --app-id exception-example --dapr-grpc-port=50001 --dapr-http-port=3500`
3. From your IDE click the play button on the client code and put break points where desired.

### Cleanup

Expand Down
20 changes: 20 additions & 0 deletions sdk-tests/src/test/java/io/dapr/it/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

package io.dapr.it;

import io.dapr.exceptions.DaprErrorDetails;
import io.dapr.exceptions.DaprException;
import io.dapr.utils.TypeRef;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.function.Executable;

Expand Down Expand Up @@ -42,6 +44,24 @@ public static <T extends Throwable> void assertThrowsDaprException(
Assertions.assertEquals(expectedErrorMessage, daprException.getMessage());
}

public static <T extends Throwable> void assertThrowsDaprExceptionWithReason(
String expectedErrorCode,
String expectedErrorMessage,
String expectedReason,
Executable executable) {
DaprException daprException = Assertions.assertThrows(DaprException.class, executable);
Assertions.assertEquals(expectedErrorCode, daprException.getErrorCode());
Assertions.assertEquals(expectedErrorMessage, daprException.getMessage());
Assertions.assertNotNull(daprException.getStatusDetails());
Assertions.assertEquals(
expectedReason,
daprException.getStatusDetails().get(
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
"reason",
TypeRef.STRING
));
}

public static <T extends Throwable> void assertThrowsDaprExceptionSubstring(
String expectedErrorCode,
String expectedErrorMessageSubstring,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import static io.dapr.it.Retry.callWithRetry;
import static io.dapr.it.actors.MyActorTestUtils.fetchMethodCallLogs;
import static io.dapr.it.actors.MyActorTestUtils.validateMethodCalls;
import static io.dapr.it.actors.MyActorTestUtils.validateMessageContent;
import static io.dapr.it.actors.MyActorTestUtils.validateMethodCalls;
import static org.junit.jupiter.api.Assertions.assertNotEquals;

public class ActorTimerRecoveryIT extends BaseIT {
Expand Down Expand Up @@ -82,21 +81,17 @@ public void timerRecoveryTest() throws Exception {

// Restarts app only.
runs.left.stop();

// Pause a bit to let placements settle.
logger.info("Pausing 12 seconds to let placements settle.");
Thread.sleep(Duration.ofSeconds(12).toMillis());

// Cannot sleep between app's stop and start since it can trigger unhealthy actor in runtime and lose timers.
// Timers will survive only if the restart is "quick" and survives the runtime's actor health check.
// Starting in 1.13, sidecar is more sensitive to an app restart and will not keep actors active for "too long".
runs.left.start();

logger.debug("Pausing 10 seconds to allow timer to fire");
Thread.sleep(10000);
final List<MethodEntryTracker> newLogs = new ArrayList<>();
callWithRetry(() -> {
newLogs.clear();
newLogs.addAll(fetchMethodCallLogs(proxy));
validateMethodCalls(newLogs, METHOD_NAME, 3);
}, 5000);
}, 10000);

// Check that the restart actually happened by confirming the old logs are not in the new logs.
for (MethodEntryTracker oldLog: logs) {
Expand Down
19 changes: 8 additions & 11 deletions sdk-tests/src/test/java/io/dapr/it/pubsub/http/PubSubIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,13 @@
import io.dapr.it.DaprRun;
import io.dapr.serializer.DaprObjectSerializer;
import io.dapr.utils.TypeRef;
import org.junit.After;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
Expand All @@ -56,6 +50,7 @@

import static io.dapr.it.Retry.callWithRetry;
import static io.dapr.it.TestUtils.assertThrowsDaprException;
import static io.dapr.it.TestUtils.assertThrowsDaprExceptionWithReason;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
Expand Down Expand Up @@ -119,14 +114,16 @@ public void publishPubSubNotFound(boolean useGrpc) throws Exception {
try (DaprClient client = new DaprClientBuilder().build()) {

if (useGrpc) {
assertThrowsDaprException(
assertThrowsDaprExceptionWithReason(
"INVALID_ARGUMENT",
"INVALID_ARGUMENT: pubsub unknown pubsub not found",
"INVALID_ARGUMENT: pubsub unknown pubsub is not found",
"DAPR_PUBSUB_NOT_FOUND",
() -> client.publishEvent("unknown pubsub", "mytopic", "payload").block());
} else {
assertThrowsDaprException(
assertThrowsDaprExceptionWithReason(
"ERR_PUBSUB_NOT_FOUND",
"ERR_PUBSUB_NOT_FOUND: pubsub unknown pubsub not found",
"ERR_PUBSUB_NOT_FOUND: pubsub unknown pubsub is not found",
"DAPR_PUBSUB_NOT_FOUND",
() -> client.publishEvent("unknown pubsub", "mytopic", "payload").block());
}
}
Expand All @@ -149,7 +146,7 @@ public void testBulkPublishPubSubNotFound(boolean useGrpc) throws Exception {
try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) {
assertThrowsDaprException(
"INVALID_ARGUMENT",
"INVALID_ARGUMENT: pubsub unknown pubsub not found",
"INVALID_ARGUMENT: pubsub unknown pubsub is not found",
() -> client.publishEvents("unknown pubsub", "mytopic","text/plain", "message").block());
}
}
Expand Down
14 changes: 8 additions & 6 deletions sdk/src/main/java/io/dapr/client/DaprHttp.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import io.dapr.client.domain.Metadata;
import io.dapr.config.Properties;
import io.dapr.exceptions.DaprError;
import io.dapr.exceptions.DaprErrorDetails;
import io.dapr.exceptions.DaprException;
import io.dapr.utils.Version;
import okhttp3.Call;
Expand Down Expand Up @@ -73,6 +74,11 @@ public class DaprHttp implements AutoCloseable {
private static final Set<String> ALLOWED_CONTEXT_IN_HEADERS =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("grpc-trace-bin", "traceparent", "tracestate")));

/**
* Object mapper to parse DaprError with or without details.
*/
private static final ObjectMapper DAPR_ERROR_DETAILS_OBJECT_MAPPER = new ObjectMapper();
Copy link
Member

@artursouza artursouza Feb 13, 2024

Choose a reason for hiding this comment

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

Clarifying my change: no need for a custom ObjectMapper. The stock one is enough now.


/**
* HTTP Methods supported.
*/
Expand Down Expand Up @@ -136,11 +142,6 @@ public int getStatusCode() {
*/
private static final byte[] EMPTY_BYTES = new byte[0];

/**
* JSON Object Mapper.
*/
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

/**
* Endpoint used to communicate to Dapr's HTTP endpoint.
*/
Expand Down Expand Up @@ -347,12 +348,13 @@ private static DaprError parseDaprError(byte[] json) {
}

try {
return OBJECT_MAPPER.readValue(json, DaprError.class);
return DAPR_ERROR_DETAILS_OBJECT_MAPPER.readValue(json, DaprError.class);
} catch (IOException e) {
throw new DaprException("UNKNOWN", new String(json, StandardCharsets.UTF_8));
}
}


private static byte[] getBodyBytesOrEmptyArray(okhttp3.Response response) throws IOException {
ResponseBody body = response.body();
if (body != null) {
Expand Down
Loading
Loading