Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions sdk-actors/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@
<version>2.3.5.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-testing</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private static ManagedChannel buildManagedChannel(DaprApiProtocol apiProtocol) {
*/
private static DaprClient buildDaprClient(DaprApiProtocol apiProtocol, Channel grpcManagedChannel) {
switch (apiProtocol) {
case GRPC: return new DaprGrpcClient(DaprGrpc.newFutureStub(grpcManagedChannel));
case GRPC: return new DaprGrpcClient(DaprGrpc.newStub(grpcManagedChannel));
case HTTP: return new DaprHttpClient(new DaprHttpBuilder().build());
default: throw new IllegalStateException("Unsupported protocol: " + apiProtocol.name());
}
Expand Down
117 changes: 92 additions & 25 deletions sdk-actors/src/main/java/io/dapr/actors/client/DaprGrpcClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,126 @@

package io.dapr.actors.client;

import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import io.dapr.config.Properties;
import io.dapr.exceptions.DaprException;
import io.dapr.internal.opencensus.GrpcWrapper;
import io.dapr.v1.DaprGrpc;
import io.dapr.v1.DaprProtos;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.stub.StreamObserver;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import reactor.util.context.Context;

import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;

/**
* A DaprClient over GRPC for Actor.
*/
class DaprGrpcClient implements DaprClient {

/**
* The GRPC client to be used.
*
* @see DaprGrpc.DaprFutureStub
* The async gRPC stub.
*/
private DaprGrpc.DaprFutureStub client;
private DaprGrpc.DaprStub client;

/**
* Internal constructor.
*
* @param grpcClient Dapr's GRPC client.
*/
DaprGrpcClient(DaprGrpc.DaprFutureStub grpcClient) {
this.client = grpcClient;
DaprGrpcClient(DaprGrpc.DaprStub grpcClient) {
this.client = intercept(grpcClient);
}

/**
* {@inheritDoc}
*/
@Override
public Mono<byte[]> invoke(String actorType, String actorId, String methodName, byte[] jsonPayload) {
return Mono.fromCallable(DaprException.wrap(() -> {
DaprProtos.InvokeActorRequest req =
DaprProtos.InvokeActorRequest.newBuilder()
.setActorType(actorType)
.setActorId(actorId)
.setMethod(methodName)
.setData(jsonPayload == null ? ByteString.EMPTY : ByteString.copyFrom(jsonPayload))
.build();

return get(client.invokeActor(req));
})).map(r -> r.getData().toByteArray());
DaprProtos.InvokeActorRequest req =
DaprProtos.InvokeActorRequest.newBuilder()
.setActorType(actorType)
.setActorId(actorId)
.setMethod(methodName)
.setData(jsonPayload == null ? ByteString.EMPTY : ByteString.copyFrom(jsonPayload))
.build();
return Mono.subscriberContext().flatMap(
context -> this.<DaprProtos.InvokeActorResponse>createMono(
it -> intercept(context, client).invokeActor(req, it)
)
).map(r -> r.getData().toByteArray());
}

/**
* Populates GRPC client with interceptors.
*
* @param client GRPC client for Dapr.
* @return Client after adding interceptors.
*/
private static DaprGrpc.DaprStub intercept(DaprGrpc.DaprStub client) {
ClientInterceptor interceptor = new ClientInterceptor() {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> methodDescriptor,
CallOptions callOptions,
Channel channel) {
ClientCall<ReqT, RespT> clientCall = channel.newCall(methodDescriptor, callOptions);
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(clientCall) {
@Override
public void start(final Listener<RespT> responseListener, final Metadata metadata) {
String daprApiToken = Properties.API_TOKEN.get();
if (daprApiToken != null) {
metadata.put(Metadata.Key.of("dapr-api-token", Metadata.ASCII_STRING_MARSHALLER), daprApiToken);
Comment on lines +85 to +86

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a negative case if the token is empty? Or is this related to the feature that has to be enabled and if it's not we hit the null (expected) case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We hit null if not set. If it is set as empty, it is OK to pass it through.

}

super.start(responseListener, metadata);
}
};
}
};
return client.withInterceptors(interceptor);
}

private static <V> V get(ListenableFuture<V> future) {
try {
return future.get();
} catch (Exception e) {
DaprException.wrap(e);
}
/**
* Populates GRPC client with interceptors for telemetry.
*
* @param context Reactor's context.
* @param client GRPC client for Dapr.
* @return Client after adding interceptors.
*/
private static DaprGrpc.DaprStub intercept(Context context, DaprGrpc.DaprStub client) {
return GrpcWrapper.intercept(context, client);
}

private <T> Mono<T> createMono(Consumer<StreamObserver<T>> consumer) {
return Mono.create(sink -> DaprException.wrap(() -> consumer.accept(createStreamObserver(sink))).run());
}

private <T> StreamObserver<T> createStreamObserver(MonoSink<T> sink) {
return new StreamObserver<T>() {
@Override
public void onNext(T value) {
sink.success(value);
}

@Override
public void onError(Throwable t) {
sink.error(DaprException.propagate(new ExecutionException(t)));
}

return null;
@Override
public void onCompleted() {
sink.success();
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,114 +5,127 @@

package io.dapr.actors.client;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
import io.dapr.v1.DaprGrpc;
import io.dapr.v1.DaprProtos;
import io.grpc.ManagedChannel;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.stub.StreamObserver;
import io.grpc.testing.GrpcCleanupRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.util.concurrent.ExecutionException;

import static io.dapr.actors.TestUtils.assertThrowsDaprException;
import static org.junit.Assert.*;
import static org.mockito.AdditionalAnswers.delegatesTo;
import static org.mockito.Mockito.*;

public class DaprGrpcClientTest {

private static final String ACTOR_TYPE = "MyActorType";

private static final String ACTOR_ID = "1234567890";

private DaprGrpc.DaprFutureStub grpcStub;
private static final String ACTOR_ID_OK = "123-Ok";

private static final String ACTOR_ID_NULL_INPUT = "123-Null";

private static final String ACTOR_ID_EXCEPTION = "123-Exception";

private static final String METHOD_NAME = "myMethod";

private static final byte[] REQUEST_PAYLOAD = "{ \"id\": 123 }".getBytes();

private static final byte[] RESPONSE_PAYLOAD = "\"OK\"".getBytes();

@Rule
public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();

private final DaprGrpc.DaprImplBase serviceImpl =
mock(DaprGrpc.DaprImplBase.class, delegatesTo(
new DaprGrpc.DaprImplBase() {
@Override
public void invokeActor(DaprProtos.InvokeActorRequest request,
StreamObserver<DaprProtos.InvokeActorResponse> responseObserver) {
assertEquals(ACTOR_TYPE, request.getActorType());
assertEquals(METHOD_NAME, request.getMethod());
switch (request.getActorId()) {
case ACTOR_ID_OK:
assertArrayEquals(REQUEST_PAYLOAD, request.getData().toByteArray());
responseObserver.onNext(
DaprProtos.InvokeActorResponse.newBuilder().setData(ByteString.copyFrom(RESPONSE_PAYLOAD))
.build());
responseObserver.onCompleted();
return;
case ACTOR_ID_NULL_INPUT:
assertArrayEquals(new byte[0], request.getData().toByteArray());
responseObserver.onNext(
DaprProtos.InvokeActorResponse.newBuilder().setData(ByteString.copyFrom(RESPONSE_PAYLOAD))
.build());
responseObserver.onCompleted();
return;

case ACTOR_ID_EXCEPTION:
Throwable e = new ArithmeticException();
StatusException se = new StatusException(Status.UNKNOWN.withCause(e));
responseObserver.onError(se);
return;
}
super.invokeActor(request, responseObserver);
}
}));

private DaprGrpcClient client;

@Before
public void setup() {
grpcStub = mock(DaprGrpc.DaprFutureStub.class);
client = new DaprGrpcClient(grpcStub);
public void setup() throws IOException {
// Generate a unique in-process server name.
String serverName = InProcessServerBuilder.generateName();

// Create a server, add service, start, and register for automatic graceful shutdown.
grpcCleanup.register(InProcessServerBuilder
.forName(serverName).directExecutor().addService(serviceImpl).build().start());

// Create a client channel and register for automatic graceful shutdown.
ManagedChannel channel = grpcCleanup.register(
InProcessChannelBuilder.forName(serverName).directExecutor().build());

// Create a HelloWorldClient using the in-process channel;
client = new DaprGrpcClient(DaprGrpc.newStub(channel));
}

@Test
public void invoke() {
String methodName = "mymethod";
byte[] payload = "{ \"id\": 123 }".getBytes();
byte[] response = "\"OK\"".getBytes();

SettableFuture<DaprProtos.InvokeActorResponse> settableFuture = SettableFuture.create();
settableFuture.set(DaprProtos.InvokeActorResponse.newBuilder().setData(ByteString.copyFrom(response)).build());

when(grpcStub.invokeActor(argThat(argument -> {
assertEquals(ACTOR_TYPE, argument.getActorType());
assertEquals(ACTOR_ID, argument.getActorId());
assertEquals(methodName, argument.getMethod());
assertArrayEquals(payload, argument.getData().toByteArray());
return true;
}))).thenReturn(settableFuture);
Mono<byte[]> result = client.invoke(ACTOR_TYPE, ACTOR_ID, methodName, payload);
assertArrayEquals(response, result.block());
Mono<byte[]> result = client.invoke(ACTOR_TYPE, ACTOR_ID_OK, METHOD_NAME, REQUEST_PAYLOAD);
assertArrayEquals(RESPONSE_PAYLOAD, result.block());
}

@Test
public void invokeNullPayload() {
String methodName = "mymethod";
byte[] response = "\"OK\"".getBytes();

SettableFuture<DaprProtos.InvokeActorResponse> settableFuture = SettableFuture.create();
settableFuture.set(DaprProtos.InvokeActorResponse.newBuilder().setData(ByteString.copyFrom(response)).build());

when(grpcStub.invokeActor(argThat(argument -> {
assertEquals(ACTOR_TYPE, argument.getActorType());
assertEquals(ACTOR_ID, argument.getActorId());
assertEquals(methodName, argument.getMethod());
assertArrayEquals(new byte[0], argument.getData().toByteArray());
return true;
}))).thenReturn(settableFuture);
Mono<byte[]> result = client.invoke(ACTOR_TYPE, ACTOR_ID, methodName, null);
assertArrayEquals(response, result.block());
Mono<byte[]> result = client.invoke(ACTOR_TYPE, ACTOR_ID_NULL_INPUT, METHOD_NAME, null);
assertArrayEquals(RESPONSE_PAYLOAD, result.block());
}

@Test
public void invokeException() {
String methodName = "mymethod";

SettableFuture<DaprProtos.InvokeActorResponse> settableFuture = SettableFuture.create();
settableFuture.setException(new ArithmeticException());

when(grpcStub.invokeActor(argThat(argument -> {
assertEquals(ACTOR_TYPE, argument.getActorType());
assertEquals(ACTOR_ID, argument.getActorId());
assertEquals(methodName, argument.getMethod());
assertArrayEquals(new byte[0], argument.getData().toByteArray());
return true;
}))).thenReturn(settableFuture);
Mono<byte[]> result = client.invoke(ACTOR_TYPE, ACTOR_ID, methodName, null);
Mono<byte[]> result = client.invoke(ACTOR_TYPE, ACTOR_ID_EXCEPTION, METHOD_NAME, null);

assertThrowsDaprException(
ExecutionException.class,
"UNKNOWN",
"UNKNOWN: java.lang.ArithmeticException",
"UNKNOWN: ",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This has to do with how Exceptions are handled in the grpc library, it does not show the exception as it would in the blocking path.

() -> result.block());
}

@Test
public void invokeNotHotMono() {
String methodName = "mymethod";

SettableFuture<DaprProtos.InvokeActorResponse> settableFuture = SettableFuture.create();
settableFuture.setException(new ArithmeticException());

when(grpcStub.invokeActor(argThat(argument -> {
assertEquals(ACTOR_TYPE, argument.getActorType());
assertEquals(ACTOR_ID, argument.getActorId());
assertEquals(methodName, argument.getMethod());
assertArrayEquals(new byte[0], argument.getData().toByteArray());
return true;
}))).thenReturn(settableFuture);
client.invoke(ACTOR_TYPE, ACTOR_ID, methodName, null);
client.invoke(ACTOR_TYPE, ACTOR_ID_EXCEPTION, METHOD_NAME, null);
// No exception thrown because Mono is ignored here.
}

Expand Down
9 changes: 9 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 @@ -33,4 +33,13 @@ public static <T extends Throwable> void assertThrowsDaprException(
Assertions.assertEquals(expectedErrorCode, daprException.getErrorCode());
Assertions.assertEquals(expectedErrorMessage, daprException.getMessage());
}

public static <T extends Throwable> void assertThrowsDaprExceptionSubstring(
String expectedErrorCode,
String expectedErrorMessageSubstring,
Executable executable) {
DaprException daprException = Assertions.assertThrows(DaprException.class, executable);
Assertions.assertEquals(expectedErrorCode, daprException.getErrorCode());
Assertions.assertTrue(daprException.getMessage().contains(expectedErrorMessageSubstring));
}
}
Loading