Skip to content

Commit

Permalink
Add the dapr runtime returned error details to the Java DaprException (
Browse files Browse the repository at this point in the history
…#998)

* properly add the dapr runtime returned error details to the Java DaprException

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* add error handling to sdk docs

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* add tests for the dapr exception changes

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* try verifyNoMoreInteractions w/ channel

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* verify channel close -> channel close explicitly

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* rm verifyNoMoreInteractions

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* rm test to see if that is the orphaned managed channel issue

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* re-add test since that doesnt seem to be the issue

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* channel.close(); -> verify(channel).close();

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* Rewrite and redesign of the DaprErrorDetail in DaprException.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Update daprdocs too for DaprErrorDetails.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Fix README.md mm string.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Fix exception example.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Use runtime 1.13.0-rc.2

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Fix exception example to match gRPC output.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Update error message in IT as per new Dapr runtime version.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Dapr 1.13 is less tolerant of app downtime to keep timers.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

---------

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>
Signed-off-by: Artur Souza <asouza.pro@gmail.com>
Co-authored-by: Artur Souza <artursouza.ms@outlook.com>
Co-authored-by: Artur Souza <asouza.pro@gmail.com>
  • Loading branch information
3 people committed Feb 13, 2024
1 parent cd81ee8 commit a3cc138
Show file tree
Hide file tree
Showing 16 changed files with 605 additions and 90 deletions.
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();
} 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();

/**
* 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

0 comments on commit a3cc138

Please sign in to comment.