diff --git a/sdk-actors/src/test/java/io/dapr/actors/client/ActorProxyImplTest.java b/sdk-actors/src/test/java/io/dapr/actors/client/ActorProxyImplTest.java index d603888581..3eb711bae8 100644 --- a/sdk-actors/src/test/java/io/dapr/actors/client/ActorProxyImplTest.java +++ b/sdk-actors/src/test/java/io/dapr/actors/client/ActorProxyImplTest.java @@ -23,6 +23,8 @@ import org.mockito.Mockito; import reactor.core.publisher.Mono; +import java.io.IOException; + import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -435,7 +437,60 @@ public void invokeActorMethodWithoutDataWithVoidReturnType() { Assertions.assertNull(emptyResponse); } + @Test() + public void invokeActorMethodWithRenamedMethodViaReflection() throws NoSuchMethodException { + final ActorClient daprClient = mock(ActorClient.class); + when(daprClient.invoke(anyString(), anyString(), Mockito.eq("actualName"), Mockito.isNull())) + .thenReturn(Mono.just("\"ok\"".getBytes())); + + final ActorProxyImpl actorProxy = new ActorProxyImpl( + "myActorType", + new ActorId("100"), + new DefaultObjectSerializer(), + daprClient); + + String res = (String) actorProxy.invoke(actorProxy, Actor.class.getMethod("renamedMethod"), null); + Assertions.assertEquals("ok", res); + Mockito.verify(daprClient).invoke( + Mockito.eq("myActorType"), Mockito.eq("100"), Mockito.eq("actualName"), Mockito.isNull()); + } + + @Test() + public void invokeActorMethodSerializationFails() throws IOException { + final ActorClient daprClient = mock(ActorClient.class); + final DaprObjectSerializer serializer = mock(DaprObjectSerializer.class); + when(serializer.serialize(Mockito.any())).thenThrow(new IOException("cannot serialize")); + + final ActorProxy actorProxy = new ActorProxyImpl( + "myActorType", + new ActorId("100"), + serializer, + daprClient); + + assertThrows(DaprException.class, () -> actorProxy.invokeMethod("mymethod", "hello").block()); + } + + @Test() + public void invokeActorMethodDeserializationFails() throws IOException { + final ActorClient daprClient = mock(ActorClient.class); + when(daprClient.invoke(anyString(), anyString(), anyString(), Mockito.isNull())) + .thenReturn(Mono.just("\"ok\"".getBytes())); + final DaprObjectSerializer serializer = mock(DaprObjectSerializer.class); + when(serializer.deserialize(Mockito.any(), Mockito.any())).thenThrow(new IOException("cannot deserialize")); + + final ActorProxy actorProxy = new ActorProxyImpl( + "myActorType", + new ActorId("100"), + serializer, + daprClient); + + assertThrows(DaprException.class, () -> actorProxy.invokeMethod("mymethod", String.class).block()); + } + interface Actor { + @ActorMethod(name = "actualName") + String renamedMethod(); + MyData getData(); String echo(String message); diff --git a/sdk-actors/src/test/java/io/dapr/actors/runtime/ActorRuntimeTest.java b/sdk-actors/src/test/java/io/dapr/actors/runtime/ActorRuntimeTest.java index 49167116fe..5254a68410 100644 --- a/sdk-actors/src/test/java/io/dapr/actors/runtime/ActorRuntimeTest.java +++ b/sdk-actors/src/test/java/io/dapr/actors/runtime/ActorRuntimeTest.java @@ -16,19 +16,29 @@ import io.dapr.actors.ActorId; import io.dapr.actors.ActorType; import io.dapr.serializer.DefaultObjectSerializer; +import io.dapr.utils.TypeRef; +import io.grpc.ManagedChannel; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; +import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.time.Duration; import java.util.Arrays; import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class ActorRuntimeTest { @@ -93,6 +103,40 @@ public int count() { } } + private static final String REMINDABLE_ACTOR_NAME = "MyRemindableActor"; + + private static final AtomicReference lastReminder = new AtomicReference<>(); + + private static final AtomicReference lastTimerData = new AtomicReference<>(); + + public interface MyRemindableActor { + void receiveTimer(String data); + } + + @ActorType(name = REMINDABLE_ACTOR_NAME) + public static class MyRemindableActorImpl extends AbstractActor + implements MyRemindableActor, Remindable { + + public MyRemindableActorImpl(ActorRuntimeContext runtimeContext, ActorId id) { + super(runtimeContext, id); + } + + @Override + public TypeRef getStateType() { + return TypeRef.STRING; + } + + @Override + public Mono receiveReminder(String reminderName, String state, Duration dueTime, Duration period) { + return Mono.fromRunnable(() -> lastReminder.set(reminderName + ":" + state)); + } + + @Override + public void receiveTimer(String data) { + lastTimerData.set(data); + } + } + private static final ActorObjectSerializer ACTOR_STATE_SERIALIZER = new ActorObjectSerializer(); private static Constructor constructor; @@ -260,6 +304,101 @@ public void lazyDeactivate() throws Exception { .doOnSuccess(s -> Assertions.fail()).onErrorReturn("".getBytes()).block(); } + @Test + public void invokeReminder() throws Exception { + lastReminder.set(null); + String actorId = UUID.randomUUID().toString(); + this.runtime.registerActor(MyRemindableActorImpl.class); + + this.runtime.invokeReminder( + REMINDABLE_ACTOR_NAME, actorId, "myReminder", createReminderParams("hello")).block(); + + Assertions.assertEquals("myReminder:hello", lastReminder.get()); + } + + @Test + public void invokeReminderUnknownActorType() throws Exception { + this.runtime.registerActor(MyRemindableActorImpl.class); + + assertThrows(IllegalArgumentException.class, () -> this.runtime.invokeReminder( + "UnknownActor", UUID.randomUUID().toString(), "myReminder", createReminderParams("hello")).block()); + } + + @Test + public void invokeTimer() throws Exception { + lastTimerData.set(null); + String actorId = UUID.randomUUID().toString(); + this.runtime.registerActor(MyRemindableActorImpl.class); + + this.runtime.invokeTimer( + REMINDABLE_ACTOR_NAME, actorId, "myTimer", createTimerParams("receiveTimer", "hello")).block(); + + Assertions.assertEquals("hello", lastTimerData.get()); + } + + @Test + public void invokeTimerUnknownActorType() throws Exception { + this.runtime.registerActor(MyRemindableActorImpl.class); + + assertThrows(IllegalArgumentException.class, () -> this.runtime.invokeTimer( + "UnknownActor", UUID.randomUUID().toString(), "myTimer", createTimerParams("receiveTimer", "hello")).block()); + } + + @Test + public void deactivateUnknownActorType() { + this.runtime.registerActor(MyActorImpl.class); + + assertThrows(IllegalArgumentException.class, + () -> this.runtime.deactivate("UnknownActor", UUID.randomUUID().toString()).block()); + } + + @Test + public void registerActorTwiceRegistersSingleEntity() throws Exception { + this.runtime.registerActor(MyActorImpl.class); + this.runtime.registerActor(MyActorImpl.class); + + Assertions.assertEquals("{\"entities\":[\"" + ACTOR_NAME + "\"]}", + new String(this.runtime.serializeConfig())); + } + + @Test + public void constructorShouldOnlyBeCalledOnce() throws Exception { + Field instanceField = ActorRuntime.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + instanceField.set(null, this.runtime); + try { + InvocationTargetException exception = assertThrows(InvocationTargetException.class, + () -> constructor.newInstance(null, this.mockDaprClient)); + Assertions.assertTrue(exception.getCause() instanceof IllegalStateException); + } finally { + instanceField.set(null, null); + } + } + + @Test + public void closeShutsDownChannel() throws Exception { + ManagedChannel channel = mock(ManagedChannel.class); + when(channel.isShutdown()).thenReturn(false); + + try (ActorRuntime runtimeWithChannel = constructor.newInstance(channel, this.mockDaprClient)) { + // Try-with-resources closes the runtime. + } + + verify(channel, times(1)).shutdown(); + } + + @Test + public void closeSkipsShutdownWhenChannelAlreadyShutdown() throws Exception { + ManagedChannel channel = mock(ManagedChannel.class); + when(channel.isShutdown()).thenReturn(true); + + try (ActorRuntime runtimeWithChannel = constructor.newInstance(channel, this.mockDaprClient)) { + // Try-with-resources closes the runtime. + } + + verify(channel, never()).shutdown(); + } + @Test public void lazyInvoke() throws Exception { String actorId = UUID.randomUUID().toString(); @@ -278,4 +417,18 @@ public void lazyInvoke() throws Exception { Assertions.assertEquals(1, count); } + private static byte[] createReminderParams(String data) throws IOException { + byte[] serializedData = new DefaultObjectSerializer().serialize(data); + ActorReminderParams params = + new ActorReminderParams(serializedData, Duration.ofSeconds(1), Duration.ofSeconds(1)); + return ACTOR_STATE_SERIALIZER.serialize(params); + } + + private static byte[] createTimerParams(String callback, String data) throws IOException { + byte[] serializedData = new DefaultObjectSerializer().serialize(data); + ActorTimerParams params = + new ActorTimerParams(callback, serializedData, Duration.ofSeconds(1), Duration.ofSeconds(1)); + return ACTOR_STATE_SERIALIZER.serialize(params); + } + }