diff --git a/Makefile b/Makefile index 6e2d3c0..c0ec348 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Variables -VERSION := 1.0-SNAPSHOT +VERSION := 1.3.0 PACKAGE := target/spawn-java-demo-${VERSION}-shaded.jar clean: diff --git a/README.md b/README.md index 42be8ec..d703f62 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ The second thing we have to do is add the spawn dependency to the project. com.github.eigr spawn-java-std-sdk - v1.2.7 + v1.3.0 ``` We're also going to configure a few things for our application build to work, including compiling the protobuf files. @@ -128,7 +128,7 @@ See below a full example of the pom.xml file: com.github.eigr spawn-java-std-sdk - v1.2.7 + v1.3.0 ch.qos.logback @@ -254,19 +254,22 @@ And let's populate this file with the following content: syntax = "proto3"; package domain; - option java_package = "io.eigr.spawn.java.demo.domain"; -message JoeState { - repeated string languages = 1; +message State { + repeated string languages = 1; } message Request { - string language = 1; + string language = 1; } message Reply { - string response = 1; + string response = 1; +} + +service JoeActor { + rpc SetLanguage(Request) returns (Reply); } ``` @@ -287,35 +290,45 @@ Populate this file with the following content: ```Java package io.eigr.spawn.java.demo; -import io.eigr.spawn.api.actors.Value; import io.eigr.spawn.api.actors.ActorContext; -import io.eigr.spawn.api.actors.annotations.Action; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.java.demo.domain.Domain; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.demo.domain.Actor.Reply; +import io.eigr.spawn.java.demo.domain.Actor.Request; +import io.eigr.spawn.java.demo.domain.Actor.State; -@StatefulNamedActor(name = "joe", stateType = Domain.JoeState.class) -public class Joe { - private static final Logger log = LoggerFactory.getLogger(Joe.class); +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.*; - @Action(name = "hi", inputType = Domain.Request.class) - public Value hi(Domain.Request msg, ActorContext context) { - log.info("Received invocation. Message: {}. Context: {}", msg, context); +public final class JoeActor extends StatefulActor { + + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new NamedActorBehavior( + name("JoeActor"), + channel("test.channel"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } + + private Value setLanguage(ActorContext context, Request msg) { if (context.getState().isPresent()) { - log.info("State is present and value is {}", context.getState().get()); + //Do something with previous state } return Value.at() - .response(Domain.Reply.newBuilder() - .setResponse("Hello From Java") + .response(Reply.newBuilder() + .setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage())) .build()) - .state(updateState("erlang")) + .state(updateState(msg.getLanguage())) .reply(); } - private Domain.JoeState updateState(String language) { - return Domain.JoeState.newBuilder() + private State updateState(String language) { + return State.newBuilder() .addLanguages(language) .build(); } @@ -434,37 +447,37 @@ In this case, it is enough to declare a constructor that receives a single argum ```java package io.eigr.spawn.java.demo; -import io.eigr.spawn.api.actors.Value; import io.eigr.spawn.api.actors.ActorContext; -import io.eigr.spawn.api.actors.annotations.Action; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.java.demo.domain.Domain; - -import java.util.Map; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.demo.domain.Actor.Reply; +import io.eigr.spawn.java.demo.domain.Actor.Request; +import io.eigr.spawn.java.demo.domain.Actor.State; -@StatefulNamedActor(name = "joe", stateful = true, stateType = Domain.JoeState.class, channel = "test") -public final class Joe { - private final String someValue; +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.action; +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.name; +public final class JoeActor extends StatefulActor { - public Joe(Map args) { - this.someValue = args.get("someKey"); - } + private String defaultMessage; - @Action(inputType = Domain.Request.class) - public Value setLanguage(Domain.Request msg, ActorContext context) { - return Value.at() - .response(Domain.Reply.newBuilder() - .setResponse("Hello From Java") - .build()) - .state(updateState("java")) - .reply(); + @Override + public ActorBehavior configure(BehaviorCtx context) { + defaultMessage = context.getInjector().getInstance(String.class); + return new NamedActorBehavior( + name("JoeActor"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); } // ... } ``` -Then you also need to register your Actor passing arguments like as follows: +Then you also need to register your Actor using injector : ```java package io.eigr.spawn.java.demo; @@ -476,12 +489,12 @@ import java.util.Map; public class App { public static void main(String[] args) { - Map actorConstructorArgs = new HashMap<>(); - actorConstructorArgs.put("someKey", "someValue"); + DependencyInjector injector = SimpleDependencyInjector.createInjector(); + injector.bind(String.class, "Hello with Constructor"); Spawn spawnSystem = new Spawn.SpawnSystem() - .create("spawn-system") - .withActor(Joe.class, actorConstructorArgs, arg -> new Joe((Map) arg)) + .create("spawn-system", injector) + .withActor(Joe.class) .build(); spawnSystem.start(); @@ -543,45 +556,52 @@ public class MessageServiceImpl implements MessageService { } ``` -2. Second, let's define an actor so that it receives an instance of the DependencyInjector class through the class constructor: +2. Second, let's define an actor so that it receives an instance of the DependencyInjector class through the context of configure method: ```java -package io.eigr.spawn.test.actors; +package io.eigr.spawn.java.demo; import io.eigr.spawn.api.actors.ActorContext; +import io.eigr.spawn.api.actors.StatefulActor; import io.eigr.spawn.api.actors.Value; -import io.eigr.spawn.api.actors.annotations.Action; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.api.extensions.DependencyInjector; -import io.eigr.spawn.java.test.domain.Actor; - -@StatefulNamedActor(name = "test_actor_constructor", stateType = Actor.State.class) -public final class Joe { - - private final MessageService messageService; - - public Joe(DependencyInjector injector) { - // Note how to use dependency injection here to get a concrete class of MessageService. - this.messageService = injector.getInstance(MessageService.class); - } - - @Action(inputType = Actor.Request.class) - public Value setLanguage(Actor.Request msg, ActorContext context) { - return Value.at() - .response(Actor.Reply.newBuilder() - .setResponse(messageService.getDefaultMessage()) - .build()) - .state(updateState("java")) - .reply(); - } - - private Actor.State updateState(String language) { - return Actor.State.newBuilder() - .addLanguages(language) - .build(); - } -} +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.demo.domain.Actor.Reply; +import io.eigr.spawn.java.demo.domain.Actor.Request; +import io.eigr.spawn.java.demo.domain.Actor.State; + +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.action; +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.name; +public final class JoeActor extends StatefulActor { + + private String defaultMessage; + + @Override + public ActorBehavior configure(BehaviorCtx context) { + defaultMessage = context.getInjector().getInstance(String.class); + return new NamedActorBehavior( + name("JoeActor"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } + + private Value setLanguage(ActorContext context, Request msg) { + return Value.at() + .response(Reply.newBuilder() + .setResponse(defaultMessage) + .build()) + .state(updateState("java")) + .reply(); + } + private State updateState(String language) { + return State.newBuilder() + .addLanguages(language) + .build(); + } +} ``` 3. Then you can pass your dependent classes this way to your Actor: @@ -600,10 +620,13 @@ public class App { If you try to add different instances of the same type you will receive an error. */ injector.bind(MessageService.class, new MessageServiceImpl()); + + // or using alias for put different values of same key types + injector.bind(MessageService.class, "myMessageService", new MessageServiceImpl()); Spawn spawnSystem = new Spawn.SpawnSystem() - .create("spawn-system") - .withActor(Joe.class, injector -> new Joe((DependencyInjector) injector)) + .create("spawn-system", injector) + .withActor(Joe.class) .build(); spawnSystem.start(); @@ -614,7 +637,7 @@ public class App { It is important to note that this helper mechanism does not currently implement any type of complex dependency graph. Therefore, it will not build objects based on complex dependencies nor take care of the object lifecycle for you. In other words, all instances added through the bind method of the SimpleDependencyInjector class will be singletons. -This mechanism works much more like a bucket of objects that will be forwarded via your actor's constructor. +This mechanism works much more like a bucket of objects that will be forwarded via your actor's context. > **_NOTE:_** **Why not use the java cdi 2.0 spec?** Our goals are to keep the SDK for standalone Java applications very simple. We consider that implementing the entire specification would not be viable for us at the moment. It would be a lot of effort and energy expenditure that we consider spending on other parts of the ecosystem that we think will guarantee us more benefits. @@ -644,28 +667,47 @@ or Stateless, those who do not need to maintain the state. For this the developer just needs to make use of the correct annotation. For example, I could declare a Serverless Actor using the following code: ```java -package io.eigr.spawn.test.actors; +package io.eigr.spawn.java.demo.actors; -import io.eigr.spawn.api.actors.Value; import io.eigr.spawn.api.actors.ActorContext; -import io.eigr.spawn.api.actors.annotations.Action; -import io.eigr.spawn.api.actors.annotations.stateless.StatelessNamedActor; -import io.eigr.spawn.java.test.domain.Actor; - -@StatelessNamedActor(name = "test_joe") -public class JoeActor { - @Action - public Value hi(Actor.Request msg, ActorContext context) { +import io.eigr.spawn.api.actors.StatelessActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.demo.domain.Actor.Reply; +import io.eigr.spawn.java.demo.domain.Actor.Request; +import io.eigr.spawn.java.demo.domain.Actor.State; + +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.action; +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.name; + +public final class StatelessNamedActor extends StatelessActor { + + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new NamedActorBehavior( + name("StatelessNamedActor"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } + + private Value setLanguage(ActorContext context, Request msg) { + if (context.getState().isPresent()) { + } + return Value.at() - .response(Actor.Reply.newBuilder() - .setResponse("Hello From Java") + .response(Reply.newBuilder() + .setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage())) .build()) .reply(); } } + ``` -Other than that the same Named, UnNamed types are supported. Just use the StatelessNamed or StatelessUnNamed annotations. +Other than that the same Named, UnNamed types are supported. Just use the StatelessNamedActor or StatelessUnNamedActor super class. ### Considerations about Spawn actors @@ -683,33 +725,51 @@ Whereas unnamed and pooled actors are instantiated the first time only when they Actors in Spawn can subscribe to a thread and receive, as well as broadcast, events for a given thread. -To consume from a topic, you just need to configure the Actor annotation using the channel option as follows: +To consume from a topic, you just need to configure the Actor using the channel option as follows: -```Java -@StatefulNamedActor(name = "joe", stateful = true, stateType = Domain.JoeState.class, channel = "test") ``` -In the case above, the Actor `joe` was configured to receive events that are forwarded to the topic called `test`. +return new NamedActorBehavior( + name("JoeActor"), + channel("test.channel"), +); +``` +In the case above, the Actor `JoeActor` was configured to receive events that are forwarded to the topic called `test.channel`. To produce events in a topic, just use the Broadcast Workflow. The example below demonstrates a complete example of producing and consuming events. In this case, the same actor is the event consumer and producer, but in a more realistic scenario, different actors would be involved in these processes. ```Java -package io.eigr.spawn.java.demo; +package io.eigr.spawn.java.demo.actors; +import io.eigr.spawn.api.actors.ActorContext; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; import io.eigr.spawn.api.actors.workflows.Broadcast; -// some imports omitted for brevity +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.demo.domain.Actor.Reply; +import io.eigr.spawn.java.demo.domain.Actor.Request; +import io.eigr.spawn.java.demo.domain.Actor.State; -@StatefulNamedActor(name = "joe", stateType = Domain.JoeState.class, channel = "test") -public class Joe { - @TimerAction(name = "hi", period = 60000) - public Value hi(ActorContext context) { - Domain.Request msg = Domain.Request.newBuilder() - .setLanguage("erlang") - .build(); +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.*; + +public final class LoopActor extends StatefulActor { + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new NamedActorBehavior( + name("LoopActor"), + channel("test.channel"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } + + private Value setLanguage(ActorContext context, Request msg) { return Value.at() - .flow(Broadcast.to("test", "setLanguage", msg)) + .flow(Broadcast.to("test.channel", "setLanguage", msg)) .response(Domain.Reply.newBuilder() .setResponse("Hello From Erlang") .build()) @@ -717,16 +777,7 @@ public class Joe { .reply(); } - @Action(inputType = Domain.Request.class) - public Value setLanguage(Domain.Request msg, ActorContext context) { - return Value.at() - .response(Domain.Reply.newBuilder() - .setResponse("Hello From Java") - .build()) - .state(updateState("java")) - .reply(); - } - // .... + // ... } ``` @@ -736,36 +787,45 @@ Actors can also emit side effects to other Actors as part of their response. See an example: ```Java -package io.eigr.spawn.java.demo; +package io.eigr.spawn.java.demo.actors; -import io.eigr.spawn.api.ActorIdentity; -import io.eigr.spawn.api.ActorRef; -import io.eigr.spawn.api.actors.Value; import io.eigr.spawn.api.actors.ActorContext; -import io.eigr.spawn.api.actors.annotations.Action; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.api.actors.workflows.SideEffect; -import io.eigr.spawn.java.demo.domain.Domain; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.demo.domain.Actor.Reply; +import io.eigr.spawn.java.demo.domain.Actor.Request; +import io.eigr.spawn.java.demo.domain.Actor.State; + +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.*; + +public final class JoeActor extends StatefulActor { + + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new NamedActorBehavior( + name("JoeActor"), + channel("test.channel"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } -@StatefulNamedActor(name = "side_effect_actor", stateType = Domain.State.class) -public class SideEffectActorExample { - @Action - public Value setLanguage(Domain.Request msg, ActorContext ctx) throws Exception { - // Create a ActorReference to send side effect message + private Value setLanguage(ActorContext context, Request msg) { ActorRef sideEffectReceiverActor = ctx.getSpawnSystem() - .createActorRef(ActorIdentity.of("spawn-system", "mike", "abs_actor")); + .createActorRef(ActorIdentity.of("spawn-system", "MikeFriendActor", "MikeParentActor")); return Value.at() - .response(Domain.Reply.newBuilder() - .setResponse("Hello From Java") - .build()) - .state(updateState("java")) .flow(SideEffect.to(sideEffectReceiverActor, "setLanguage", msg)) - //.flow(SideEffect.to(emailSenderReceiverActor, "sendEmail", emailMessage)) - //.flow(SideEffect.to(otherReceiverActor, "otherAction", otherMessage)) + .response(Reply.newBuilder() + .setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage())) + .build()) + .state(updateState(msg.getLanguage())) .noReply(); } - + // .... } ``` @@ -783,31 +843,34 @@ an action in another Actor. See an example: ```Java -package io.eigr.spawn.java.demo; +package io.eigr.spawn.java.demo.actors; -import io.eigr.spawn.api.actors.Value; import io.eigr.spawn.api.actors.ActorContext; -import io.eigr.spawn.api.ActorIdentity; -import io.eigr.spawn.api.ActorRef; -import io.eigr.spawn.api.actors.annotations.Action; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.api.actors.workflows.Forward; -import io.eigr.spawn.java.demo.domain.Domain; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@StatefulNamedActor(name = "routing_actor", stateType = Domain.State.class) -public class ForwardExample { - private static final Logger log = LoggerFactory.getLogger(ForwardExample.class); - - @Action - public Value setLanguage(Domain.Request msg, ActorContext ctx) throws Exception { - log.info("Received invocation. Message: {}. Context: {}", msg, ctx); - if (ctx.getState().isPresent()) { - log.info("State is present and value is {}", ctx.getState().get()); - } +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.demo.domain.Actor.Reply; +import io.eigr.spawn.java.demo.domain.Actor.Request; +import io.eigr.spawn.java.demo.domain.Actor.State; + +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.*; + +public final class RoutingActor extends StatefulActor { + + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new NamedActorBehavior( + name("RoutingActor"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } + + private Value setLanguage(ActorContext context, Request msg) { ActorRef forwardedActor = ctx.getSpawnSystem() - .createActorRef(ActorIdentity.of("spawn-system", "mike", "abs_actor")); + .createActorRef(ActorIdentity.of("spawn-system", "MikeFriendActor", "MikeActor")); return Value.at() .flow(Forward.to(forwardedActor, "setLanguage")) @@ -826,39 +889,45 @@ In the end, just like in a Forward, it is the response of the last Actor in the Example: ```Java -package io.eigr.spawn.java.demo; +package io.eigr.spawn.java.demo.actors; -import io.eigr.spawn.api.actors.Value; import io.eigr.spawn.api.actors.ActorContext; -import io.eigr.spawn.api.ActorIdentity; -import io.eigr.spawn.api.ActorRef; -import io.eigr.spawn.api.actors.annotations.Action; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.api.actors.workflows.Pipe; -import io.eigr.spawn.java.demo.domain.Domain; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.demo.domain.Actor.Reply; +import io.eigr.spawn.java.demo.domain.Actor.Request; +import io.eigr.spawn.java.demo.domain.Actor.State; + +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.*; + +public final class PipeActor extends StatefulActor { -@StatefulNamedActor(name = "pipe_actor", stateType = Domain.State.class) -public class PipeActorExample { + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new NamedActorBehavior( + name("PipeActor"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } - @Action - public Value setLanguage(Domain.Request msg, ActorContext ctx) throws Exception { + private Value setLanguage(ActorContext context, Request msg) { ActorRef pipeReceiverActor = ctx.getSpawnSystem() - .createActorRef(ActorIdentity.of("spawn-system", "joe")); + .createActorRef(ActorIdentity.of("spawn-system", "JoeActor")); return Value.at() - .response(Domain.Reply.newBuilder() + .response(Reply.newBuilder() .setResponse("Hello From Java") .build()) .flow(Pipe.to(pipeReceiverActor, "someAction")) .state(updateState("java")) .noReply(); } - - private Domain.State updateState(String language) { - return Domain.State.newBuilder() - .addLanguages(language) - .build(); - } + + // ... } ``` @@ -875,32 +944,51 @@ during the moment of the Actor's deactivation. That is, data is saved at regular intervals asynchronously while the Actor is active and once synchronously when the Actor suffers a deactivation, when it is turned off. -These snapshots happen from time to time. And this time is configurable through the ***snapshotTimeout*** property of -the ***StatefulNamedActor*** or ***UnStatefulNamedActor*** annotation. +These snapshots happen from time to time. And this time is configurable through the ***snapshotTimeout*** method of +the ***NamedActorBehavior*** or ***UnNamedActorBehavior*** class. However, you can tell the Spawn runtime that you want it to persist the data immediately synchronously after executing an Action. And this can be done in the following way: Example: ```Java -import io.eigr.spawn.api.actors.Value; +package io.eigr.spawn.test.actors; + import io.eigr.spawn.api.actors.ActorContext; -import io.eigr.spawn.api.actors.annotations.Action; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.java.demo.domain.Domain; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.demo.domain.Actor.Reply; +import io.eigr.spawn.java.demo.domain.Actor.Request; +import io.eigr.spawn.java.demo.domain.Actor.State; + +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.*; -@StatefulNamedActor(name = "joe", stateType = Domain.JoeState.class) -public final class Joe { - @Action(inputType = Domain.Request.class) - public Value setLanguage(Domain.Request msg, ActorContext context) { +public final class JoeActor extends StatefulActor { + + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new NamedActorBehavior( + name("JoeActor"), + snapshot(1000), + deactivated(60000), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } + + private Value setLanguage(ActorContext context, Request msg) { return Value.at() - .response(Domain.Reply.newBuilder() - .setResponse("Hello From Java") + .response(Reply.newBuilder() + .setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage())) .build()) - .state(updateState("java"), true) + .state(updateState(msg.getLanguage()), true) .reply(); } - // ... + + // ... } ``` @@ -938,13 +1026,13 @@ In the sections below we will give some examples of how to invoke different type To invoke an actor named like the one we defined in section [Getting Started](#getting-started) we could do as follows: ```Java -ActorRef joeActor = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "joe")); +ActorRef joeActor = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "JoeActor")); -Domain.Request msg = Domain.Request.newBuilder() +Request msg = Request.newBuilder() .setLanguage("erlang") .build(); -Optional maybeResponse = joeActor.invoke("setLanguage", msg, Domain.Reply.class); +Optional maybeResponse = joeActor.invoke("setLanguage", msg, Reply.class); Domain.Reply reply = maybeResponse.get(); ``` @@ -976,7 +1064,7 @@ public class App { spawnSystem.start(); - ActorRef joeActor = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "joe")); + ActorRef joeActor = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "JoeActor")); Domain.Request msg = Domain.Request.newBuilder() .setLanguage("erlang") @@ -997,50 +1085,67 @@ To better exemplify, let's first show the Actor's definition code and later how name at runtime: ```java -package io.eigr.spawn.java.demo; -// omitted imports for brevity... - -@UnStatefulNamedActor(name = "abs_actor", stateful = true, stateType = Domain.State.class) -public class AbstractActor { - @Action(inputType = Domain.Request.class) - public Value setLanguage(Domain.Request msg, ActorContext context) { - return Value.at() - .response(Domain.Reply.newBuilder() - .setResponse("Hello From Java") - .build()) - .state(updateState("java")) - .reply(); - } - - private Domain.State updateState(String language) { - return Domain.State.newBuilder() - .addLanguages(language) - .build(); - } +package io.eigr.spawn.test.actors; + +import io.eigr.spawn.api.actors.ActorContext; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.UnNamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.demo.domain.Actor.Reply; +import io.eigr.spawn.java.demo.domain.Actor.Request; +import io.eigr.spawn.java.demo.domain.Actor.State; + +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.*; + +public final class MikeActor extends StatefulActor { + + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new UnNamedActorBehavior( + name("MikeActor"), + snapshot(1000), + deactivated(60000), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } + + private Value setLanguage(ActorContext context, Request msg) { + return Value.at() + .response(Reply.newBuilder() + .setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage())) + .build()) + .state(updateState(msg.getLanguage()), true) + .reply(); + } + + // ... } ``` So you could define and call this actor at runtime like this: ```Java -ActorRef mike = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "mike", "abs_actor")); +ActorRef mike = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "MikeInstanceActor", "MikeActor")); -Domain.Request msg = Domain.Request.newBuilder() +Request msg = Request.newBuilder() .setLanguage("erlang") .build(); -Optional maybeResponse = mike.invoke("setLanguage", msg, Domain.Reply.class); +Optional maybeResponse = mike.invoke("setLanguage", msg, Reply.class); Domain.Reply reply = maybeResponse.get(); ``` The important part of the code above is the following snippet: ```Java -ActorRef mike = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "mike", "abs_actor")); +ActorRef mike = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "MikeInstanceActor", "MikeActor")); ``` -These tells Spawn that this actor will actually be named at runtime. The name parameter with value "mike" -in this case is just a reference to "abs_actor" Actor that will be used later +These tells Spawn that this actor will actually be named at runtime. The name parameter with value "MikeInstanceActor" +in this case is just a reference to "MikeActor" Actor that will be used later so that we can actually create an instance of the real Actor. ### Async @@ -1063,17 +1168,12 @@ It is possible to change the request waiting timeout using the invocation option ```Java package io.eigr.spawn.java.demo; -import io.eigr.spawn.api.ActorIdentity; -import io.eigr.spawn.api.ActorRef; -import io.eigr.spawn.api.InvocationOpts; -import io.eigr.spawn.api.Spawn; -import io.eigr.spawn.api.TransportOpts; -import io.eigr.spawn.java.demo.domain.Domain; +// omitted for brevity import java.util.Optional; public class App { - public static void main(String[] args) throws Exception { + public static void main(String[] args) { Spawn spawnSystem = new Spawn.SpawnSystem() .create("spawn-system") .withActor(Joe.class) @@ -1081,9 +1181,9 @@ public class App { spawnSystem.start(); - ActorRef joeActor = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "joe")); + ActorRef joeActor = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "JoeActor")); - Domain.Request msg = Domain.Request.newBuilder() + Request msg = Request.newBuilder() .setLanguage("erlang") .build(); @@ -1091,7 +1191,7 @@ public class App { .timeoutSeconds(Duration.ofSeconds(30)) .build(); - Optional maybeResponse = joeActor.invoke("setLanguage", msg, Domain.Reply.class, opts); + Optional maybeResponse = joeActor.invoke("setLanguage", msg, Reply.class, opts); } } ``` diff --git a/pom.xml b/pom.xml index c665d14..4e43e8b 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.eigr.spawn spawn-java-std-sdk jar - 1.2.7 + 1.3.0 spawn-java-std-sdk http://maven.apache.org @@ -59,6 +59,13 @@ test + + ch.qos.logback + logback-classic + 1.4.7 + test + + org.testcontainers mysql @@ -110,7 +117,8 @@ protobuf-maven-plugin 0.6.1 - com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + grpc-java io.grpc:protoc-gen-grpc-java:1.65.0:exe:${os.detected.classifier} diff --git a/src/main/java/io/eigr/spawn/api/ActorRef.java b/src/main/java/io/eigr/spawn/api/ActorRef.java index 4a3a703..4561847 100644 --- a/src/main/java/io/eigr/spawn/api/ActorRef.java +++ b/src/main/java/io/eigr/spawn/api/ActorRef.java @@ -24,9 +24,12 @@ public final class ActorRef { private final SpawnClient client; - private ActorRef(ActorOuterClass.ActorId actorId, SpawnClient client) { + private final Class type; + + private ActorRef(ActorOuterClass.ActorId actorId, SpawnClient client, Class type) { this.client = client; this.actorId = actorId; + this.type = type; } /** @@ -39,7 +42,7 @@ private ActorRef(ActorOuterClass.ActorId actorId, SpawnClient client) { * @return the ActorRef instance * @since 0.0.1 */ - protected static ActorRef of(SpawnClient client, Cache cache, ActorIdentity identity) throws ActorCreationException { + protected static ActorRef of(SpawnClient client, Cache cache, ActorIdentity identity, Class actorType) throws ActorCreationException { ActorOuterClass.ActorId actorId; if (identity.isParent()) { @@ -59,7 +62,7 @@ protected static ActorRef of(SpawnClient client, Cache actorIds, Spa client.spawn(req); } + public Class getType() { + return this.type; + } + /** *

This method synchronously invokes an action on the actor that this ActorRef instance represents through the Spawn Proxy. * Used when it is not necessary to send parameters to the Action. diff --git a/src/main/java/io/eigr/spawn/api/Spawn.java b/src/main/java/io/eigr/spawn/api/Spawn.java index 47d2e41..6d414ee 100644 --- a/src/main/java/io/eigr/spawn/api/Spawn.java +++ b/src/main/java/io/eigr/spawn/api/Spawn.java @@ -5,17 +5,15 @@ import com.sun.net.httpserver.HttpServer; import io.eigr.functions.protocol.Protocol; import io.eigr.functions.protocol.actors.ActorOuterClass; -import io.eigr.spawn.api.actors.ActorFactory; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulPooledActor; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulUnNamedActor; -import io.eigr.spawn.api.actors.annotations.stateless.StatelessNamedActor; -import io.eigr.spawn.api.actors.annotations.stateless.StatelessPooledActor; -import io.eigr.spawn.api.actors.annotations.stateless.StatelessUnNamedActor; +import io.eigr.spawn.api.actors.BaseActor; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.StatelessActor; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; import io.eigr.spawn.api.exceptions.ActorCreationException; import io.eigr.spawn.api.exceptions.ActorRegistrationException; import io.eigr.spawn.api.exceptions.SpawnException; import io.eigr.spawn.api.exceptions.SpawnFailureException; +import io.eigr.spawn.api.extensions.DependencyInjector; import io.eigr.spawn.internal.Entity; import io.eigr.spawn.internal.transport.client.OkHttpSpawnClient; import io.eigr.spawn.internal.transport.client.SpawnClient; @@ -48,6 +46,8 @@ public final class Spawn { private final String proxyHost; private final int proxyPort; private final String system; + + private BehaviorCtx ctx; private final List entities; private final String host; private final Executor executor; @@ -55,6 +55,7 @@ public final class Spawn { private Spawn(SpawnSystem builder) { this.system = builder.system; + this.ctx = builder.ctx; this.entities = builder.entities; this.port = builder.transportOpts.getPort(); this.host = builder.transportOpts.getHost(); @@ -82,6 +83,10 @@ public String getSystem() { return system; } + public BehaviorCtx getBehaviorCtx() { + return ctx; + } + public int getTerminationGracePeriodSeconds() { return terminationGracePeriodSeconds; } @@ -96,7 +101,18 @@ public int getTerminationGracePeriodSeconds() { * @since 0.0.1 */ public ActorRef createActorRef(ActorIdentity identity) throws ActorCreationException { - return ActorRef.of(this.client, this.actorIdCache, identity); + Class actorType; + if (identity.isParent()) { + actorType = this.entities.stream().filter(e -> e.getActorName().equalsIgnoreCase(identity.getParent())) + .map(e -> e.getActorType()) + .findFirst().get(); + } else { + actorType = this.entities.stream().filter(e -> e.getActorName().equalsIgnoreCase(identity.getName())) + .map(e -> e.getActorType()) + .findFirst().get(); + } + + return ActorRef.of(this.client, this.actorIdCache, identity, actorType); } /** @@ -122,14 +138,25 @@ public Stream createMultiActorRefs(List identities) thr return identities.stream().map(identity -> { try { + Class actorType; + if (identity.isParent()) { + actorType = this.entities.stream().filter(e -> e.getActorName().equalsIgnoreCase(identity.getParent())) + .map(e -> e.getActorType()) + .findFirst().get(); + } else { + actorType = this.entities.stream().filter(e -> e.getActorName().equalsIgnoreCase(identity.getName())) + .map(e -> e.getActorType()) + .findFirst().get(); + } + if (identity.isParent()) { return ActorRef.of( this.client, this.actorIdCache, - ActorIdentity.of(identity.getSystem(), identity.getName(), identity.getParent(), false)); + ActorIdentity.of(identity.getSystem(), identity.getName(), identity.getParent(), false), actorType); } - return ActorRef.of(this.client, this.actorIdCache, identity); + return ActorRef.of(this.client, this.actorIdCache, identity, actorType); } catch (ActorCreationException e) { throw new SpawnFailureException(e); } @@ -164,7 +191,9 @@ private void startServer() throws SpawnException { } private void registerActorSystem() throws ActorRegistrationException { - ActorOuterClass.Registry registry = ActorOuterClass.Registry.newBuilder().putAllActors(getActors(this.entities)).build(); + ActorOuterClass.Registry registry = ActorOuterClass.Registry.newBuilder() + .putAllActors(getActors(this.entities)) + .build(); ActorOuterClass.ActorSystem actorSystem = ActorOuterClass.ActorSystem.newBuilder() .setName(this.system) @@ -217,13 +246,17 @@ private Map getActors(List entities) { .setMaxPoolSize(actorEntity.getMaxPoolSize()) .build(); - Map tags = new HashMap<>(); - ActorOuterClass.Metadata metadata = ActorOuterClass.Metadata.newBuilder() - .addChannelGroup(ActorOuterClass.Channel.newBuilder() - .setTopic(actorEntity.getChannel()) - .build()) - .putAllTags(tags) - .build(); + final Map tags = new HashMap<>(); + final ActorOuterClass.Metadata.Builder metadataBuilder = ActorOuterClass.Metadata.newBuilder(); + + if (Objects.nonNull(actorEntity.getChannel())){ + metadataBuilder.addChannelGroup(ActorOuterClass.Channel.newBuilder() + .setTopic(actorEntity.getChannel()) + .build()); + } + + metadataBuilder.putAllTags(tags); + final ActorOuterClass.Metadata metadata = metadataBuilder.build(); return ActorOuterClass.Actor.newBuilder() .setId(ActorOuterClass.ActorId.newBuilder() @@ -241,19 +274,23 @@ private Map getActors(List entities) { } private List getActions(Entity actorEntity) { - return actorEntity.getActions().values().stream() - .filter(v -> Entity.EntityMethodType.DIRECT.equals(v.getType())) - .map(action -> ActorOuterClass.Action.newBuilder().setName(action.getName()).build()) + return (List) actorEntity.getActions().values().stream() + .filter(v -> Entity.EntityMethodType.DIRECT.equals(((Entity.EntityMethod)v).getType())) + .map(action -> ActorOuterClass.Action.newBuilder() + .setName(((Entity.EntityMethod)action).getName()) + .build() + ) .collect(Collectors.toList()); } private List getTimerActions(Entity actorEntity) { - List timerActions = actorEntity.getTimerActions().values() - .stream().filter(v -> Entity.EntityMethodType.TIMER.equals(v.getType())) + List timerActions = + (List) actorEntity.getTimerActions().values() + .stream().filter(v -> Entity.EntityMethodType.TIMER.equals(((Entity.EntityMethod)v).getType())) .map(action -> ActorOuterClass.FixedTimerAction.newBuilder() .setAction(ActorOuterClass.Action.newBuilder() - .setName(action.getName()).build()) - .setSeconds(action.getFixedPeriod()).build()) + .setName(((Entity.EntityMethod)action).getName()).build()) + .setSeconds(((Entity.EntityMethod)action).getFixedPeriod()).build()) .collect(Collectors.toList()); log.debug("Actor have TimeActions: {}", timerActions); @@ -262,7 +299,13 @@ private List getTimerActions(Entity actorEntit @Override public String toString() { - return new StringJoiner(", ", Spawn.class.getSimpleName() + "[", "]").add("system='" + system + "'").add("port=" + port).add("host='" + host + "'").add("proxyHost='" + proxyHost + "'").add("proxyPort=" + proxyPort).toString(); + return new StringJoiner(", ", Spawn.class.getSimpleName() + "[", "]") + .add("system='" + system + "'") + .add("port=" + port) + .add("host='" + host + "'") + .add("proxyHost='" + proxyHost + "'") + .add("proxyPort=" + proxyPort) + .toString(); } public static final class SpawnSystem { @@ -270,7 +313,9 @@ public static final class SpawnSystem { private Cache actorIdCache; private SpawnClient client; - private String system = "spawn-system"; + private String system; + + private BehaviorCtx ctx; private int terminationGracePeriodSeconds = 30; @@ -286,6 +331,13 @@ public static final class SpawnSystem { */ public SpawnSystem create(String system) { this.system = system; + this.ctx = BehaviorCtx.create(); + return this; + } + + public SpawnSystem create(String system, DependencyInjector injector) { + this.system = system; + this.ctx = BehaviorCtx.create(injector); return this; } @@ -301,36 +353,28 @@ public SpawnSystem createFromEnv() { String system = System.getenv("PROXY_ACTOR_SYSTEM_NAME"); Objects.requireNonNull(system, "To use createFromEnv method it is necessary to have defined the environment variable PROXY_ACTOR_SYSTEM_NAME"); this.system = system; + this.ctx = BehaviorCtx.create(); return this; } - /** - *

Constructor method that adds a new Actor to the Spawn proxy. - *

- * - * @param actorKlass the actor definition class - * @return the SpawnSystem instance - * @since 0.0.1 - */ - public SpawnSystem withActor(Class actorKlass) { - Optional maybeEntity = getEntity(actorKlass); - maybeEntity.ifPresent(this.entities::add); + public SpawnSystem createFromEnv(DependencyInjector injector) { + String system = System.getenv("PROXY_ACTOR_SYSTEM_NAME"); + Objects.requireNonNull(system, "To use createFromEnv method it is necessary to have defined the environment variable PROXY_ACTOR_SYSTEM_NAME"); + this.system = system; + this.ctx = BehaviorCtx.create(injector); return this; } /** *

Constructor method that adds a new Actor to the Spawn proxy. - * Allows options to be passed to the class constructor. The constructor must consist of only one argument *

* * @param actorKlass the actor definition class - * @param arg the object that will be passed as an argument to the constructor via the lambda fabric - * @param factory a lambda that constructs the instance of the Actor object * @return the SpawnSystem instance * @since 0.0.1 */ - public SpawnSystem withActor(Class actorKlass, Object arg, ActorFactory factory) { - Optional maybeEntity = getEntity(actorKlass, arg, factory); + public SpawnSystem withActor(Class actorKlass) throws ActorCreationException { + Optional maybeEntity = getEntity(actorKlass); maybeEntity.ifPresent(this.entities::add); return this; } @@ -362,14 +406,9 @@ public Spawn build() { return new Spawn(this); } - private Optional getEntity(Class actorKlass) { - Optional maybeEntity = getStatefulEntity(actorKlass, null, null); - - if (maybeEntity.isPresent()) { - return maybeEntity; - } + private Optional getEntity(Class actorKlass) throws ActorCreationException { + Optional maybeEntity = mapEntity(actorKlass); - maybeEntity = getStatelessEntity(actorKlass, null, null); if (maybeEntity.isPresent()) { return maybeEntity; } @@ -377,48 +416,13 @@ private Optional getEntity(Class actorKlass) { return Optional.empty(); } - private Optional getEntity(Class actorKlass, Object arg, ActorFactory factory) { - Optional maybeEntity = getStatefulEntity(actorKlass, arg, factory); - - if (maybeEntity.isPresent()) { - return maybeEntity; - } - - maybeEntity = getStatelessEntity(actorKlass, arg, factory); - if (maybeEntity.isPresent()) { - return maybeEntity; - } - - return Optional.empty(); - } - - private Optional getStatefulEntity(Class actorKlass, Object arg, ActorFactory factory) { - if (Objects.nonNull(actorKlass.getAnnotation(StatefulNamedActor.class))) { - return Optional.of(Entity.fromAnnotationToEntity(actorKlass, actorKlass.getAnnotation(StatefulNamedActor.class), arg, factory)); - } - - if (Objects.nonNull(actorKlass.getAnnotation(StatefulUnNamedActor.class))) { - return Optional.of(Entity.fromAnnotationToEntity(actorKlass, actorKlass.getAnnotation(StatefulUnNamedActor.class), arg, factory)); - } - - if (Objects.nonNull(actorKlass.getAnnotation(StatefulPooledActor.class))) { - return Optional.of(Entity.fromAnnotationToEntity(actorKlass, actorKlass.getAnnotation(StatefulPooledActor.class), arg, factory)); - } - - return Optional.empty(); - } - - private Optional getStatelessEntity(Class actorKlass, Object arg, ActorFactory factory) { - if (Objects.nonNull(actorKlass.getAnnotation(StatelessNamedActor.class))) { - return Optional.of(Entity.fromAnnotationToEntity(actorKlass, actorKlass.getAnnotation(StatelessNamedActor.class), arg, factory)); - } - - if (Objects.nonNull(actorKlass.getAnnotation(StatelessUnNamedActor.class))) { - return Optional.of(Entity.fromAnnotationToEntity(actorKlass, actorKlass.getAnnotation(StatelessUnNamedActor.class), arg, factory)); + private Optional mapEntity(Class actorKlass) throws ActorCreationException { + if (StatefulActor.class.isAssignableFrom(actorKlass)) { + return Optional.of(Entity.fromStatefulActorToEntity(ctx, actorKlass)); } - if (Objects.nonNull(actorKlass.getAnnotation(StatelessPooledActor.class))) { - return Optional.of(Entity.fromAnnotationToEntity(actorKlass, actorKlass.getAnnotation(StatelessPooledActor.class), arg, factory)); + if (StatelessActor.class.isAssignableFrom(actorKlass)) { + return Optional.of(Entity.fromStatelessActorToEntity(ctx, actorKlass)); } return Optional.empty(); diff --git a/src/main/java/io/eigr/spawn/api/actors/ActorFactory.java b/src/main/java/io/eigr/spawn/api/actors/ActorFactory.java deleted file mode 100644 index e208167..0000000 --- a/src/main/java/io/eigr/spawn/api/actors/ActorFactory.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.eigr.spawn.api.actors; - -@FunctionalInterface -public interface ActorFactory { - Object newInstance(Object arg); -} diff --git a/src/main/java/io/eigr/spawn/api/actors/BaseActor.java b/src/main/java/io/eigr/spawn/api/actors/BaseActor.java new file mode 100644 index 0000000..477c8f8 --- /dev/null +++ b/src/main/java/io/eigr/spawn/api/actors/BaseActor.java @@ -0,0 +1,13 @@ +package io.eigr.spawn.api.actors; + +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; + +public abstract class BaseActor { + + public abstract ActorBehavior configure(BehaviorCtx ctx); + + public abstract Boolean isStateful(); + + +} diff --git a/src/main/java/io/eigr/spawn/api/actors/StatefulActor.java b/src/main/java/io/eigr/spawn/api/actors/StatefulActor.java new file mode 100644 index 0000000..333440d --- /dev/null +++ b/src/main/java/io/eigr/spawn/api/actors/StatefulActor.java @@ -0,0 +1,16 @@ +package io.eigr.spawn.api.actors; + +import java.lang.reflect.ParameterizedType; + +public abstract class StatefulActor extends BaseActor { + public Class getStateType() { + return (Class) ((ParameterizedType) + getClass().getGenericSuperclass()) + .getActualTypeArguments()[0]; + } + + @Override + public Boolean isStateful() { + return true; + } +} diff --git a/src/main/java/io/eigr/spawn/api/actors/StatelessActor.java b/src/main/java/io/eigr/spawn/api/actors/StatelessActor.java new file mode 100644 index 0000000..2ef0272 --- /dev/null +++ b/src/main/java/io/eigr/spawn/api/actors/StatelessActor.java @@ -0,0 +1,8 @@ +package io.eigr.spawn.api.actors; + +public abstract class StatelessActor extends BaseActor { + @Override + public Boolean isStateful() { + return false; + } +} diff --git a/src/main/java/io/eigr/spawn/api/actors/annotations/Action.java b/src/main/java/io/eigr/spawn/api/actors/annotations/Action.java deleted file mode 100644 index 063b8a5..0000000 --- a/src/main/java/io/eigr/spawn/api/actors/annotations/Action.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.eigr.spawn.api.actors.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@SpawnAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface Action { - - /** - * The name of the command to handle. - * - *

If not specified, the name of the method will be used as the command name. - * - * @return The command name. - */ - String name() default ""; - - /** - * * The input type. - * - * The type class of input method parameter. Generally, this will be determined by looking at the parameter of the input type - * handler method, however if the event doesn't need to be passed to the method (for example, - * perhaps it contains no data), then this can be used to indicate which event this handler - * handles. - */ - Class inputType() default Default.class; - - Class outputType() default Default.class; - - class Default {} -} \ No newline at end of file diff --git a/src/main/java/io/eigr/spawn/api/actors/annotations/SpawnAnnotation.java b/src/main/java/io/eigr/spawn/api/actors/annotations/SpawnAnnotation.java deleted file mode 100644 index 00eff94..0000000 --- a/src/main/java/io/eigr/spawn/api/actors/annotations/SpawnAnnotation.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.eigr.spawn.api.actors.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -@Target(ElementType.ANNOTATION_TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface SpawnAnnotation { -} diff --git a/src/main/java/io/eigr/spawn/api/actors/annotations/TimerAction.java b/src/main/java/io/eigr/spawn/api/actors/annotations/TimerAction.java deleted file mode 100644 index b437ca3..0000000 --- a/src/main/java/io/eigr/spawn/api/actors/annotations/TimerAction.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.eigr.spawn.api.actors.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@SpawnAnnotation -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface TimerAction { - - /** - * The name of the action to handle. - * - *

If not specified, the name of the method will be used as the command name. - * - * @return The action name. - */ - String name() default ""; - - int period() default 0; - - /** - * * The input type. - * - * The type class of input method parameter. Generally, this will be determined by looking at the parameter of the input type - * handler method, however if the event doesn't need to be passed to the method (for example, - * perhaps it contains no data), then this can be used to indicate which event this handler - * handles. - */ - Class inputType() default Default.class; - - Class outputType() default Default.class; - - class Default {} -} \ No newline at end of file diff --git a/src/main/java/io/eigr/spawn/api/actors/annotations/stateful/StatefulNamedActor.java b/src/main/java/io/eigr/spawn/api/actors/annotations/stateful/StatefulNamedActor.java deleted file mode 100644 index 20fe253..0000000 --- a/src/main/java/io/eigr/spawn/api/actors/annotations/stateful/StatefulNamedActor.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.eigr.spawn.api.actors.annotations.stateful; - -import com.google.protobuf.GeneratedMessage; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface StatefulNamedActor { - String value() default ""; - - String name() default ""; - - Class stateType(); - - long deactivatedTimeout() default 60000; - - long snapshotTimeout() default 50000; - - String channel() default ""; -} diff --git a/src/main/java/io/eigr/spawn/api/actors/annotations/stateful/StatefulPooledActor.java b/src/main/java/io/eigr/spawn/api/actors/annotations/stateful/StatefulPooledActor.java deleted file mode 100644 index 017ede0..0000000 --- a/src/main/java/io/eigr/spawn/api/actors/annotations/stateful/StatefulPooledActor.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.eigr.spawn.api.actors.annotations.stateful; - -import com.google.protobuf.GeneratedMessage; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface StatefulPooledActor { - String value() default ""; - - String name() default ""; - - Class stateType(); - - long deactivatedTimeout() default 60000; - - long snapshotTimeout() default 50000; - - String channel() default ""; - - int minPoolSize() default 1; - - int maxPoolSize() default 0; -} diff --git a/src/main/java/io/eigr/spawn/api/actors/annotations/stateful/StatefulUnNamedActor.java b/src/main/java/io/eigr/spawn/api/actors/annotations/stateful/StatefulUnNamedActor.java deleted file mode 100644 index 681d35f..0000000 --- a/src/main/java/io/eigr/spawn/api/actors/annotations/stateful/StatefulUnNamedActor.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.eigr.spawn.api.actors.annotations.stateful; - -import com.google.protobuf.GeneratedMessage; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface StatefulUnNamedActor { - String value() default ""; - - String name() default ""; - - Class stateType(); - - long deactivatedTimeout() default 60000; - - long snapshotTimeout() default 50000; - - String channel() default ""; -} diff --git a/src/main/java/io/eigr/spawn/api/actors/annotations/stateless/StatelessNamedActor.java b/src/main/java/io/eigr/spawn/api/actors/annotations/stateless/StatelessNamedActor.java deleted file mode 100644 index 3cf9a10..0000000 --- a/src/main/java/io/eigr/spawn/api/actors/annotations/stateless/StatelessNamedActor.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.eigr.spawn.api.actors.annotations.stateless; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface StatelessNamedActor { - String value() default ""; - - String name() default ""; - - long deactivatedTimeout() default 10000; - - String channel() default ""; - -} diff --git a/src/main/java/io/eigr/spawn/api/actors/annotations/stateless/StatelessPooledActor.java b/src/main/java/io/eigr/spawn/api/actors/annotations/stateless/StatelessPooledActor.java deleted file mode 100644 index 1426519..0000000 --- a/src/main/java/io/eigr/spawn/api/actors/annotations/stateless/StatelessPooledActor.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.eigr.spawn.api.actors.annotations.stateless; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface StatelessPooledActor { - String value() default ""; - - String name() default ""; - - long deactivatedTimeout() default 10000; - - String channel() default ""; - - int minPoolSize() default 1; - - int maxPoolSize() default 0; -} diff --git a/src/main/java/io/eigr/spawn/api/actors/annotations/stateless/StatelessUnNamedActor.java b/src/main/java/io/eigr/spawn/api/actors/annotations/stateless/StatelessUnNamedActor.java deleted file mode 100644 index a5a0f3d..0000000 --- a/src/main/java/io/eigr/spawn/api/actors/annotations/stateless/StatelessUnNamedActor.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.eigr.spawn.api.actors.annotations.stateless; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface StatelessUnNamedActor { - String value() default ""; - - String name() default ""; - - long deactivatedTimeout() default 10000; - - String channel() default ""; -} diff --git a/src/main/java/io/eigr/spawn/api/actors/behaviors/ActorBehavior.java b/src/main/java/io/eigr/spawn/api/actors/behaviors/ActorBehavior.java new file mode 100644 index 0000000..7d995bd --- /dev/null +++ b/src/main/java/io/eigr/spawn/api/actors/behaviors/ActorBehavior.java @@ -0,0 +1,144 @@ +package io.eigr.spawn.api.actors.behaviors; + +import com.google.protobuf.GeneratedMessage; +import com.google.protobuf.Message; +import io.eigr.spawn.api.actors.ActorContext; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.exceptions.ActorInvocationException; +import io.eigr.spawn.internal.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; + +public abstract class ActorBehavior { + Class stateType; + private String name; + + private String channel; + private long deactivatedTimeout = 60000; + private long snapshotTimeout = 50000; + private final Map actions = new HashMap<>(); + + public ActorBehavior(ActorOption... options) { + Optional.ofNullable(options) + .stream() + .flatMap(Stream::of) + .forEach(option -> option.accept(this)); + } + + public static ActorOption name(String actorName) { + return instance -> instance.name = actorName; + } + + public static ActorOption channel(String channel) { + return instance -> instance.channel = channel; + } + + public static ActorOption deactivated(long timeout) { + return instance -> instance.deactivatedTimeout = timeout; + } + + public static ActorOption snapshot(long timeout) { + return instance -> instance.snapshotTimeout = timeout; + } + + public static ActorOption init(ActionNoBindings action) { + return instance -> instance.actions.put( + "Init", + new ActionEnvelope( + action, + new ActionConfiguration(ActionKind.NORMAL_DISPATCH, 0, null, null))); + } + + public static ActorOption action(String name, ActionNoBindings action) { + final Class outputType = Value.class; + return instance -> instance.actions.put( + name, + new ActionEnvelope( + action, + new ActionConfiguration(ActionKind.NORMAL_DISPATCH, 0, null, outputType))); + } + + public static ActorOption action(String name, ActionBindings action) { + final Class inputType = action.getArgumentType(); + final Class outputType = Value.class; + + return instance -> instance.actions.put( + name, + new ActionEnvelope( + action, + new ActionConfiguration(ActionKind.NORMAL_DISPATCH, 1, inputType, outputType))); + } + + public static ActorOption timerAction(String name, int timer, ActionNoBindings action) { + final Class outputType = Value.class; + + return instance -> instance.actions.put( + name, new ActionEnvelope( + action, + new ActionConfiguration(ActionKind.TIMER_DISPATCH, timer, 0, null, outputType))); + } + + public static ActorOption timerAction(String name, int timer, ActionBindings action) { + final Class inputType = action.getArgumentType(); + final Class outputType = Value.class; + + return instance -> instance.actions.put( + name, + new ActionEnvelope( + action, + new ActionConfiguration(ActionKind.TIMER_DISPATCH, timer, 1, inputType, outputType))); + } + + public abstract ActorKind getActorType(); + + public String getName() { + return name; + } + + public String getChannel() { + return channel; + } + + public long getDeactivatedTimeout() { + return deactivatedTimeout; + } + + public long getSnapshotTimeout() { + return snapshotTimeout; + } + + public Class getStateType() { + return this.stateType; + } + + public void setStateType(Class stateType) { + this.stateType = stateType; + } + + public Map getActions() { + return actions; + } + + public Value call(String action, ActorContext context) throws ActorInvocationException { + if (this.actions.containsKey(action)) { + return ((ActionNoBindings) this.actions.get(action).getFunction()).handle(context); + } + + throw new ActorInvocationException(String.format("Action [%s] not found for Actor [%s]", action, name)); + } + + public Value call(String action, ActorContext context, A argument) throws ActorInvocationException { + if (this.actions.containsKey(action)) { + return ((ActionBindings) this.actions.get(action).getFunction()).handle(context, argument); + } + + throw new ActorInvocationException(String.format("Action [%s] not found for Actor [%s]", action, name)); + } + + public interface ActorOption extends Consumer { + } +} diff --git a/src/main/java/io/eigr/spawn/api/actors/behaviors/BehaviorCtx.java b/src/main/java/io/eigr/spawn/api/actors/behaviors/BehaviorCtx.java new file mode 100644 index 0000000..fd0ccf3 --- /dev/null +++ b/src/main/java/io/eigr/spawn/api/actors/behaviors/BehaviorCtx.java @@ -0,0 +1,28 @@ +package io.eigr.spawn.api.actors.behaviors; + +import io.eigr.spawn.api.extensions.DependencyInjector; +import io.eigr.spawn.api.extensions.SimpleDependencyInjector; + +public final class BehaviorCtx { + private DependencyInjector injector; + + private BehaviorCtx() { + this.injector = SimpleDependencyInjector.createInjector(); + } + + private BehaviorCtx(DependencyInjector injector) { + this.injector = injector; + } + + public static BehaviorCtx create() { + return new BehaviorCtx(); + } + + public static BehaviorCtx create(DependencyInjector injector) { + return new BehaviorCtx(injector); + } + + public DependencyInjector getInjector() { + return injector; + } +} diff --git a/src/main/java/io/eigr/spawn/api/actors/behaviors/NamedActorBehavior.java b/src/main/java/io/eigr/spawn/api/actors/behaviors/NamedActorBehavior.java new file mode 100644 index 0000000..77fa69c --- /dev/null +++ b/src/main/java/io/eigr/spawn/api/actors/behaviors/NamedActorBehavior.java @@ -0,0 +1,15 @@ +package io.eigr.spawn.api.actors.behaviors; + +import io.eigr.spawn.internal.ActorKind; + +public final class NamedActorBehavior extends ActorBehavior { + + public NamedActorBehavior(ActorOption... options) { + super(options); + } + + @Override + public ActorKind getActorType() { + return ActorKind.NAMED; + } +} diff --git a/src/main/java/io/eigr/spawn/api/actors/behaviors/UnNamedActorBehavior.java b/src/main/java/io/eigr/spawn/api/actors/behaviors/UnNamedActorBehavior.java new file mode 100644 index 0000000..d872486 --- /dev/null +++ b/src/main/java/io/eigr/spawn/api/actors/behaviors/UnNamedActorBehavior.java @@ -0,0 +1,15 @@ +package io.eigr.spawn.api.actors.behaviors; + +import io.eigr.spawn.internal.ActorKind; + +public final class UnNamedActorBehavior extends ActorBehavior { + + public UnNamedActorBehavior(ActorOption... options) { + super(options); + } + + @Override + public ActorKind getActorType() { + return ActorKind.UNNAMED; + } +} diff --git a/src/main/java/io/eigr/spawn/api/actors/workflows/Broadcast.java b/src/main/java/io/eigr/spawn/api/actors/workflows/Broadcast.java index 69ee689..580b320 100644 --- a/src/main/java/io/eigr/spawn/api/actors/workflows/Broadcast.java +++ b/src/main/java/io/eigr/spawn/api/actors/workflows/Broadcast.java @@ -44,8 +44,9 @@ public T getPayload() { public Protocol.Broadcast build() { Protocol.Broadcast.Builder builder = Protocol.Broadcast.newBuilder(); + //TODO use Channel object instead of string if (this.action.isPresent()) { - builder.setActionName(this.action.get()); + builder.setChannelGroup(this.action.get()); } if (this.channel.isPresent()) { diff --git a/src/main/java/io/eigr/spawn/api/extensions/DependencyInjector.java b/src/main/java/io/eigr/spawn/api/extensions/DependencyInjector.java index 667fcf1..3333c46 100644 --- a/src/main/java/io/eigr/spawn/api/extensions/DependencyInjector.java +++ b/src/main/java/io/eigr/spawn/api/extensions/DependencyInjector.java @@ -4,5 +4,9 @@ public interface DependencyInjector { void bind(Class type, Object instance); + void bind(Class type, String alias, Object instance); + T getInstance(Class type); + + T getInstanceByAlias(String alias); } diff --git a/src/main/java/io/eigr/spawn/api/extensions/SimpleDependencyInjector.java b/src/main/java/io/eigr/spawn/api/extensions/SimpleDependencyInjector.java index 4b08754..a8ca0be 100644 --- a/src/main/java/io/eigr/spawn/api/extensions/SimpleDependencyInjector.java +++ b/src/main/java/io/eigr/spawn/api/extensions/SimpleDependencyInjector.java @@ -6,9 +6,11 @@ public final class SimpleDependencyInjector implements DependencyInjector { private final Map, Object> bucket; + private final Map aliasBucket; private SimpleDependencyInjector() { this.bucket = new HashMap<>(); + this.aliasBucket = new HashMap<>(); } private static class SimpleDependencyInjectorHelper{ @@ -28,8 +30,23 @@ public void bind(Class type, Object instance) { this.bucket.put(type, instance); } + @Override + public void bind(Class type, String alias, Object instance) { + if (this.aliasBucket.containsKey(type)) { + throw new IllegalArgumentException("There is already an instance associated with this type in the bucket"); + } + + this.aliasBucket.put(alias, instance); + } + @Override public T getInstance(Class type) { return type.cast(this.bucket.get(type)); } + + @Override + public Object getInstanceByAlias(String alias) { + var typeClass = this.aliasBucket.get(alias).getClass(); + return typeClass.cast(this.aliasBucket.get(alias)); + } } diff --git a/src/main/java/io/eigr/spawn/internal/ActionBindings.java b/src/main/java/io/eigr/spawn/internal/ActionBindings.java new file mode 100644 index 0000000..b2da5c1 --- /dev/null +++ b/src/main/java/io/eigr/spawn/internal/ActionBindings.java @@ -0,0 +1,37 @@ +package io.eigr.spawn.internal; + +import com.google.protobuf.Message; +import io.eigr.spawn.api.actors.ActorContext; +import io.eigr.spawn.api.actors.Value; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +@FunctionalInterface +public interface ActionBindings extends ActionEmptyFunction { + + Value handle(ActorContext context, A argument); + + default Class getArgumentType() { + Type type = ((ParameterizedType) getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0]; + if (type instanceof Class) { + return (Class) type; + } else { + throw new IllegalStateException("Unable to determine generic type A."); + } + } + + static ActionBindings of(Class type, ActionBindings function) { + return new ActionBindings() { + @Override + public Value handle(ActorContext context, A argument) { + return function.handle(context, argument); + } + + @Override + public Class getArgumentType() { + return type; + } + }; + } +} diff --git a/src/main/java/io/eigr/spawn/internal/ActionConfiguration.java b/src/main/java/io/eigr/spawn/internal/ActionConfiguration.java new file mode 100644 index 0000000..7666d0d --- /dev/null +++ b/src/main/java/io/eigr/spawn/internal/ActionConfiguration.java @@ -0,0 +1,57 @@ +package io.eigr.spawn.internal; + +import java.util.StringJoiner; + +public final class ActionConfiguration { + private final ActionKind kind; + private int timer; + + private int arity; + + private Class inputType; + + private Class outputType; + + public ActionConfiguration(ActionKind kind, int arity, Class inputType, Class outputType) { + this.kind = kind; + this.arity = arity; + this.inputType = inputType; + this.outputType = outputType; + } + + public ActionConfiguration(ActionKind kind, int timer, int arity, Class inputType, Class outputType) { + this.kind = kind; + this.timer = timer; + this.arity = arity; + this.inputType = inputType; + this.outputType = outputType; + } + + public ActionKind getKind() { + return kind; + } + + public int getTimer() { + return timer; + } + + public Class getInputType() { + return inputType; + } + + public Class getOutputType() { + return outputType; + } + + @Override + public String toString() { + return new StringJoiner(", ", ActionConfiguration.class.getSimpleName() + "[", "]") + .add("kind=" + kind) + .add("timer=" + timer) + .toString(); + } + + public int getArity() { + return this.arity; + } +} diff --git a/src/main/java/io/eigr/spawn/internal/ActionEmptyFunction.java b/src/main/java/io/eigr/spawn/internal/ActionEmptyFunction.java new file mode 100644 index 0000000..a41522e --- /dev/null +++ b/src/main/java/io/eigr/spawn/internal/ActionEmptyFunction.java @@ -0,0 +1,3 @@ +package io.eigr.spawn.internal; + +public interface ActionEmptyFunction {} diff --git a/src/main/java/io/eigr/spawn/internal/ActionEnvelope.java b/src/main/java/io/eigr/spawn/internal/ActionEnvelope.java new file mode 100644 index 0000000..476cb84 --- /dev/null +++ b/src/main/java/io/eigr/spawn/internal/ActionEnvelope.java @@ -0,0 +1,30 @@ +package io.eigr.spawn.internal; + +import java.util.StringJoiner; + +public final class ActionEnvelope { + + private final F function; + private final ActionConfiguration config; + + public ActionEnvelope(F function, ActionConfiguration config) { + this.function = function; + this.config = config; + } + + public F getFunction() { + return function; + } + + public ActionConfiguration getConfig() { + return config; + } + + @Override + public String toString() { + return new StringJoiner(", ", ActionEnvelope.class.getSimpleName() + "[", "]") + .add("function=" + function) + .add("config=" + config) + .toString(); + } +} diff --git a/src/main/java/io/eigr/spawn/internal/ActionKind.java b/src/main/java/io/eigr/spawn/internal/ActionKind.java new file mode 100644 index 0000000..aeef34c --- /dev/null +++ b/src/main/java/io/eigr/spawn/internal/ActionKind.java @@ -0,0 +1,6 @@ +package io.eigr.spawn.internal; + +public enum ActionKind { + NORMAL_DISPATCH, + TIMER_DISPATCH +} diff --git a/src/main/java/io/eigr/spawn/internal/ActionNoBindings.java b/src/main/java/io/eigr/spawn/internal/ActionNoBindings.java new file mode 100644 index 0000000..3afcbea --- /dev/null +++ b/src/main/java/io/eigr/spawn/internal/ActionNoBindings.java @@ -0,0 +1,10 @@ +package io.eigr.spawn.internal; + +import io.eigr.spawn.api.actors.ActorContext; +import io.eigr.spawn.api.actors.Value; + +@FunctionalInterface +public interface ActionNoBindings extends ActionEmptyFunction { + + Value handle(ActorContext context); +} diff --git a/src/main/java/io/eigr/spawn/internal/Entity.java b/src/main/java/io/eigr/spawn/internal/Entity.java index 6c1d191..dfba127 100644 --- a/src/main/java/io/eigr/spawn/internal/Entity.java +++ b/src/main/java/io/eigr/spawn/internal/Entity.java @@ -1,31 +1,33 @@ package io.eigr.spawn.internal; import io.eigr.functions.protocol.actors.ActorOuterClass; -import io.eigr.spawn.api.actors.ActorContext; -import io.eigr.spawn.api.actors.ActorFactory; -import io.eigr.spawn.api.actors.annotations.*; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulPooledActor; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulUnNamedActor; -import io.eigr.spawn.api.actors.annotations.stateless.StatelessNamedActor; -import io.eigr.spawn.api.actors.annotations.stateless.StatelessPooledActor; -import io.eigr.spawn.api.actors.annotations.stateless.StatelessUnNamedActor; +import io.eigr.spawn.api.actors.BaseActor; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.StatelessActor; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.api.actors.behaviors.UnNamedActorBehavior; +import io.eigr.spawn.api.exceptions.ActorCreationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.AbstractMap; +import java.util.Map; import java.util.stream.Collectors; -public final class Entity { +public final class Entity { private static final Logger log = LoggerFactory.getLogger(Entity.class); - private String actorName; - private Class actorType; - private Optional actorArg; + private A actor; + + private B behavior; - private Optional actorFactory; + private BehaviorCtx ctx; + private String actorName; + private Class actorType; private ActorOuterClass.Kind kind; @@ -46,6 +48,9 @@ public final class Entity { private String channel; public Entity( + BehaviorCtx ctx, + A actor, + B behavior, String actorName, Class actorType, ActorOuterClass.Kind kind, @@ -58,9 +63,10 @@ public Entity( Map timerActions, int minPoolSize, int maxPoolSize, - String channel, - Optional actorArg, - Optional actorFactory) { + String channel) { + this.ctx = ctx; + this.actor = actor; + this.behavior = behavior; this.actorName = actorName; this.actorType = actorType; this.kind = kind; @@ -74,11 +80,244 @@ public Entity( this.minPoolSize = minPoolSize; this.maxPoolSize = maxPoolSize; this.channel = channel; - this.actorArg = actorArg; - this.actorFactory = actorFactory; } + public static Entity fromStatelessActorToEntity(BehaviorCtx ctx, Class actor) throws ActorCreationException { + try { + Constructor constructor = actor.getConstructor(); + StatelessActor stActor = (StatelessActor) constructor.newInstance(); + ActorBehavior behavior = stActor.configure(ctx); + + if (behavior.getClass().isAssignableFrom(NamedActorBehavior.class)) { + Entity entity = buildNamedActor(null, stActor, (NamedActorBehavior) behavior, ctx); + return entity; + } + + if (behavior.getClass().isAssignableFrom(UnNamedActorBehavior.class)) { + return buildUnNamedActor(null, stActor, (UnNamedActorBehavior) behavior, ctx); + } + + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | + IllegalAccessException e) { + throw new ActorCreationException(); + } + + throw new ActorCreationException(); + } + + public static Entity fromStatefulActorToEntity(BehaviorCtx ctx, Class actor) throws ActorCreationException { + try { + Constructor constructor = actor.getConstructor(); + StatefulActor stActor = (StatefulActor) constructor.newInstance(); + Class stateType = stActor.getStateType(); + ActorBehavior behavior = stActor.configure(ctx); + + if (behavior.getClass().isAssignableFrom(NamedActorBehavior.class)) { + Entity entity = buildNamedActor(stateType, stActor, (NamedActorBehavior) behavior, ctx); + return entity; + } + + if (behavior.getClass().isAssignableFrom(UnNamedActorBehavior.class)) { + return buildUnNamedActor(stateType, stActor, (UnNamedActorBehavior) behavior, ctx); + } + + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | + IllegalAccessException e) { + throw new ActorCreationException(); + } + + throw new ActorCreationException(); + } + + private static Map getActions(Map actions) { + return actions + .entrySet() + .stream().filter(entry -> entry.getValue().getConfig().getKind().equals(ActionKind.NORMAL_DISPATCH)) + .map(entry -> { + final String actionName = entry.getKey(); + final ActionEnvelope envelope = entry.getValue(); + final ActionConfiguration config = envelope.getConfig(); + + return new AbstractMap.SimpleEntry<>( + actionName, + new Entity.EntityMethod( + actionName, + EntityMethodType.DIRECT, + config.getArity(), + 0, + config.getInputType(), + config.getOutputType())); + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static Map getTimerActions(Map actions) { + return actions + .entrySet() + .stream().filter(entry -> entry.getValue().getConfig().getKind().equals(ActionKind.TIMER_DISPATCH)) + .map(entry -> { + final String actionName = entry.getKey(); + final ActionEnvelope envelope = entry.getValue(); + final ActionConfiguration config = envelope.getConfig(); + final int arity = config.getArity(); + final int timer = config.getTimer(); + + return new AbstractMap.SimpleEntry<>( + actionName, + new Entity.EntityMethod( + actionName, + EntityMethodType.DIRECT, + arity, + timer, + config.getInputType(), + config.getOutputType())); + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static Entity buildNamedActor(Class stateType, StatefulActor actor, NamedActorBehavior behavior, BehaviorCtx ctx) { + final String actorName = behavior.getName(); + final ActorKind kind = behavior.getActorType(); + final String channel = behavior.getChannel(); + final long deactivateTimeout = behavior.getDeactivatedTimeout(); + final long snapshotTimeout = behavior.getSnapshotTimeout(); + final Map envelopeActions = behavior.getActions(); + final Map actions = getActions(envelopeActions); + final Map timerActions = getTimerActions(envelopeActions); + + Entity entityType = new Entity( + ctx, + actor, + behavior, + actorName, + actor.getClass(), + getKind(kind), + stateType, + actorName, + actor.isStateful(), + deactivateTimeout, + snapshotTimeout, + actions, + timerActions, + 0, + 0, + channel); + + return entityType; + } + + private static Entity buildNamedActor(Class stateType, StatelessActor actor, NamedActorBehavior behavior, BehaviorCtx ctx) { + final String actorName = behavior.getName(); + final ActorKind kind = behavior.getActorType(); + final String channel = behavior.getChannel(); + final long deactivateTimeout = behavior.getDeactivatedTimeout(); + final long snapshotTimeout = behavior.getSnapshotTimeout(); + final Map envelopeActions = behavior.getActions(); + final Map actions = getActions(envelopeActions); + final Map timerActions = getTimerActions(envelopeActions); + + Entity entityType = new Entity( + ctx, actor, behavior, actorName, + actor.getClass(), + getKind(kind), + stateType, + actorName, + actor.isStateful(), + deactivateTimeout, + snapshotTimeout, + actions, + timerActions, + 0, + 0, + channel); + + return entityType; + } + + private static Entity buildUnNamedActor(Class stateType, StatefulActor actor, UnNamedActorBehavior behavior, BehaviorCtx ctx) { + final String actorName = behavior.getName(); + final ActorKind kind = behavior.getActorType(); + final String channel = behavior.getChannel(); + long deactivateTimeout = behavior.getDeactivatedTimeout(); + long snapshotTimeout = behavior.getSnapshotTimeout(); + final Map envelopeActions = behavior.getActions(); + final Map actions = getActions(envelopeActions); + final Map timerActions = getTimerActions(envelopeActions); + + Entity entityType = new Entity( + ctx, + actor, + behavior, + actorName, + actor.getClass(), + getKind(kind), + stateType, + actorName, + actor.isStateful(), + deactivateTimeout, + snapshotTimeout, + actions, + timerActions, + 0, + 0, + channel); + + return entityType; + } + private static Entity buildUnNamedActor(Class stateType, StatelessActor actor, UnNamedActorBehavior behavior, BehaviorCtx ctx) { + final String actorName = behavior.getName(); + final ActorKind kind = behavior.getActorType(); + final String channel = behavior.getChannel(); + long deactivateTimeout = behavior.getDeactivatedTimeout(); + long snapshotTimeout = behavior.getSnapshotTimeout(); + final Map envelopeActions = behavior.getActions(); + final Map actions = getActions(envelopeActions); + final Map timerActions = getTimerActions(envelopeActions); + + Entity entityType = new Entity( + ctx, + actor, + behavior, + actorName, + actor.getClass(), + getKind(kind), + stateType, + actorName, + actor.isStateful(), + deactivateTimeout, + snapshotTimeout, + actions, + timerActions, + 0, + 0, + channel); + + return entityType; + } + + private static ActorOuterClass.Kind getKind(ActorKind kind) { + switch (kind) { + case UNNAMED: + return ActorOuterClass.Kind.UNNAMED; + case POOLED: + return ActorOuterClass.Kind.POOLED; + case PROXY: + return ActorOuterClass.Kind.PROXY; + default: + return ActorOuterClass.Kind.NAMED; + } + } + + public A getActor() { + return (A) this.actor; + } + + public B getBehavior() { + return (B) this.behavior; + } + + public BehaviorCtx getCtx() { + return this.ctx; + } public String getActorName() { return actorName; @@ -132,18 +371,32 @@ public int getMaxPoolSize() { return maxPoolSize; } - public String getChannel(){ return channel; } - - public enum EntityMethodType { - DIRECT, TIMER + public String getChannel() { + return channel; } - public Optional getActorArg() { - return actorArg; + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Entity{"); + sb.append("actorName='").append(actorName).append('\''); + sb.append(", actorType=").append(actorType); + sb.append(", kind=").append(kind); + sb.append(", stateType=").append(stateType); + sb.append(", actorBeanName='").append(actorBeanName).append('\''); + sb.append(", stateful=").append(stateful); + sb.append(", deactivateTimeout=").append(deactivateTimeout); + sb.append(", snapshotTimeout=").append(snapshotTimeout); + sb.append(", actions=").append(actions); + sb.append(", timerActions=").append(timerActions); + sb.append(", minPoolSize=").append(minPoolSize); + sb.append(", maxPoolSize=").append(maxPoolSize); + sb.append(", channel=").append(channel); + sb.append('}'); + return sb.toString(); } - public Optional getActorFactory() { - return actorFactory; + public enum EntityMethodType { + DIRECT, TIMER } public static final class EntityMethod { @@ -151,18 +404,19 @@ public static final class EntityMethod { private EntityMethodType type; + private int arity; + private int fixedPeriod; - private Method method; private Class inputType; private Class outputType; public EntityMethod( - String name, EntityMethodType type, int fixedPeriod, Method method, Class inputType, Class outputType) { + String name, EntityMethodType type, int arity, int fixedPeriod, Class inputType, Class outputType) { this.name = name; this.type = type; + this.arity = arity; this.fixedPeriod = fixedPeriod; - this.method = method; this.inputType = inputType; this.outputType = outputType; } @@ -175,24 +429,15 @@ public EntityMethodType getType() { return type; } - public int getFixedPeriod() { - return fixedPeriod; + public int getArity() { + return arity; } - public Method getMethod() { - return method; + public int getFixedPeriod() { + return fixedPeriod; } - public Class getInputType() { - int arity = method.getParameterTypes().length; - - if (arity == 2 && Objects.isNull(inputType)) { - for (Class parameterType : method.getParameterTypes()) { - if (!inputType.isAssignableFrom(ActorContext.class)) { - return parameterType; - } - } - } + public Class getInputType() { return inputType; } @@ -205,394 +450,12 @@ public String toString() { final StringBuilder sb = new StringBuilder("EntityMethod{"); sb.append("name='").append(name).append('\''); sb.append(", type=").append(type); + sb.append(", arity=").append(arity); sb.append(", fixedPeriod=").append(fixedPeriod); - sb.append(", method=").append(method); sb.append(", inputType=").append(inputType); sb.append(", outputType=").append(outputType); sb.append('}'); return sb.toString(); } } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("Entity{"); - sb.append("actorName='").append(actorName).append('\''); - sb.append(", actorType=").append(actorType); - sb.append(", kind=").append(kind); - sb.append(", stateType=").append(stateType); - sb.append(", actorBeanName='").append(actorBeanName).append('\''); - sb.append(", stateful=").append(stateful); - sb.append(", deactivateTimeout=").append(deactivateTimeout); - sb.append(", snapshotTimeout=").append(snapshotTimeout); - sb.append(", actions=").append(actions); - sb.append(", timerActions=").append(timerActions); - sb.append(", minPoolSize=").append(minPoolSize); - sb.append(", maxPoolSize=").append(maxPoolSize); - sb.append(", channel=").append(channel); - sb.append('}'); - return sb.toString(); - } - - public static Entity fromAnnotationToEntity(Class entity, StatefulNamedActor actor, Object arg, ActorFactory factory) { - String actorBeanName = entity.getSimpleName(); - String actorName; - if ((Objects.isNull(actor.name()) || actor.name().isEmpty())) { - actorName = actorBeanName; - } else { - actorName = actor.name(); - } - - final ActorKind kind = ActorKind.NAMED; - final long deactivateTimeout = actor.deactivatedTimeout(); - final long snapshotTimeout = actor.snapshotTimeout(); - final boolean isStateful = true; - final Class stateType = actor.stateType(); - final String channel = actor.channel(); - - final Map actions = buildActions(entity, Action.class); - final Map timerActions = buildActions(entity, TimerAction.class); - - Entity entityType = new Entity( - actorName, - entity, - getKind(kind), - stateType, - actorBeanName, - isStateful, - deactivateTimeout, - snapshotTimeout, - actions, - timerActions, - 0, - 0, - channel, - Optional.ofNullable(arg), - Optional.ofNullable(factory)); - - log.info("Registering NamedActor: {}", actorName); - log.debug("Registering Entity -> {}", entityType); - return entityType; - } - - public static Entity fromAnnotationToEntity(Class entity, StatefulUnNamedActor actor, Object arg, ActorFactory factory) { - String actorBeanName = entity.getSimpleName(); - String actorName; - if ((Objects.isNull(actor.name()) || actor.name().isEmpty())) { - actorName = actorBeanName; - } else { - actorName = actor.name(); - } - - final ActorKind kind = ActorKind.UNNAMED; - final long deactivateTimeout = actor.deactivatedTimeout(); - final long snapshotTimeout = actor.snapshotTimeout(); - final boolean isStateful = true; - final Class stateType = actor.stateType(); - final String channel = actor.channel(); - - final Map actions = buildActions(entity, Action.class); - final Map timerActions = buildActions(entity, TimerAction.class); - - Entity entityType = new Entity( - actorName, - entity, - getKind(kind), - stateType, - actorBeanName, - isStateful, - deactivateTimeout, - snapshotTimeout, - actions, - timerActions, - 0, - 0, - channel, - Optional.ofNullable(arg), - Optional.ofNullable(factory)); - - log.info("Registering UnNamedActor: {}", actorName); - log.debug("Registering Entity -> {}", entityType); - return entityType; - } - - public static Entity fromAnnotationToEntity(Class entity, StatefulPooledActor actor, Object arg, ActorFactory factory) { - - String actorBeanName = entity.getSimpleName(); - String actorName; - if ((Objects.isNull(actor.name()) || actor.name().isEmpty())) { - actorName = actorBeanName; - } else { - actorName = actor.name(); - } - - final ActorKind kind = ActorKind.POOLED; - final long deactivateTimeout = actor.deactivatedTimeout(); - final long snapshotTimeout = actor.snapshotTimeout(); - final boolean isStateful = true; - final Class stateType = actor.stateType(); - final int minPoolSize = actor.minPoolSize(); - final int maxPoolSize = actor.maxPoolSize(); - final String channel = actor.channel(); - - final Map actions = buildActions(entity, Action.class); - final Map timerActions = buildActions(entity, TimerAction.class); - - Entity entityType = new Entity( - actorName, - entity, - getKind(kind), - stateType, - actorBeanName, - isStateful, - deactivateTimeout, - snapshotTimeout, - actions, - timerActions, - minPoolSize, - maxPoolSize, - channel, - Optional.ofNullable(arg), - Optional.ofNullable(factory)); - - log.info("Registering PooledActor: {}", actorName); - log.debug("Registering Entity -> {}", entityType); - return entityType; - } - - public static Entity fromAnnotationToEntity(Class entity, StatelessNamedActor actor, Object arg, ActorFactory factory) { - String actorBeanName = entity.getSimpleName(); - String actorName; - if ((Objects.isNull(actor.name()) || actor.name().isEmpty())) { - actorName = actorBeanName; - } else { - actorName = actor.name(); - } - - final ActorKind kind = ActorKind.NAMED; - final long deactivateTimeout = actor.deactivatedTimeout(); - final boolean isStateful = false; - final String channel = actor.channel(); - - final Map actions = buildActions(entity, Action.class); - final Map timerActions = buildActions(entity, TimerAction.class); - - Entity entityType = new Entity( - actorName, - entity, - getKind(kind), - null, - actorBeanName, - isStateful, - deactivateTimeout, - 0, - actions, - timerActions, - 0, - 0, - channel, - Optional.ofNullable(arg), - Optional.ofNullable(factory)); - - log.info("Registering NamedActor: {}", actorName); - log.debug("Registering Entity -> {}", entityType); - return entityType; - } - - public static Entity fromAnnotationToEntity(Class entity, StatelessUnNamedActor actor, Object arg, ActorFactory factory) { - String actorBeanName = entity.getSimpleName(); - String actorName; - if ((Objects.isNull(actor.name()) || actor.name().isEmpty())) { - actorName = actorBeanName; - } else { - actorName = actor.name(); - } - - final ActorKind kind = ActorKind.UNNAMED; - final long deactivateTimeout = actor.deactivatedTimeout(); - final boolean isStateful = false; - final String channel = actor.channel(); - - final Map actions = buildActions(entity, Action.class); - final Map timerActions = buildActions(entity, TimerAction.class); - - Entity entityType = new Entity( - actorName, - entity, - getKind(kind), - null, - actorBeanName, - isStateful, - deactivateTimeout, - 0, - actions, - timerActions, - 0, - 0, - channel, - Optional.ofNullable(arg), - Optional.ofNullable(factory)); - - log.info("Registering UnNamedActor: {}", actorName); - log.debug("Registering Entity -> {}", entityType); - return entityType; - } - - public static Entity fromAnnotationToEntity(Class entity, StatelessPooledActor actor, Object arg, ActorFactory factory) { - - String actorBeanName = entity.getSimpleName(); - String actorName; - if ((Objects.isNull(actor.name()) || actor.name().isEmpty())) { - actorName = actorBeanName; - } else { - actorName = actor.name(); - } - - final ActorKind kind = ActorKind.POOLED; - final long deactivateTimeout = actor.deactivatedTimeout(); - final boolean isStateful = false; - final int minPoolSize = actor.minPoolSize(); - final int maxPoolSize = actor.maxPoolSize(); - final String channel = actor.channel(); - - final Map actions = buildActions(entity, Action.class); - final Map timerActions = buildActions(entity, TimerAction.class); - - Entity entityType = new Entity( - actorName, - entity, - getKind(kind), - null, - actorBeanName, - isStateful, - deactivateTimeout, - 0, - actions, - timerActions, - minPoolSize, - maxPoolSize, - channel, - Optional.ofNullable(arg), - Optional.ofNullable(factory)); - - log.info("Registering PooledActor: {}", actorName); - log.debug("Registering Entity -> {}", entityType); - return entityType; - } - - private static Map buildActions(Class entity, Class annotationType) { - final Map actions = new HashMap<>(); - - List methods = Arrays.stream(entity.getDeclaredMethods()) - .filter(method -> method.isAnnotationPresent(annotationType)) - .collect(Collectors.toList()); - - for (Method method : methods) { - try { - method.setAccessible(true); - String commandName = getActionName(method, annotationType); - Class inputType = getInputType(method, annotationType); - Class outputType = getOutputType(method, annotationType); - - Entity.EntityMethod action = new Entity.EntityMethod( - commandName, - getEntityMethodType(method, annotationType), - getPeriod(method, annotationType), - method, - inputType, - outputType); - - actions.put(commandName, action); - } catch (SecurityException e) { - log.error("Failure on load Actor Action", e); - } - } - return actions; - } - - private static int getPeriod(Method method, Class type) { - int period = 0; - - if (type.isAssignableFrom(TimerAction.class)) { - TimerAction act = method.getAnnotation(TimerAction.class); - period = act.period(); - } - - return period; - } - - private static Entity.EntityMethodType getEntityMethodType(Method method, Class type) { - Entity.EntityMethodType entityMethodType = null; - - if (type.isAssignableFrom(Action.class)) { - entityMethodType = Entity.EntityMethodType.DIRECT; - } - - if (type.isAssignableFrom(TimerAction.class)) { - entityMethodType = Entity.EntityMethodType.TIMER; - } - - return entityMethodType; - } - - private static String getActionName(Method method, Class type) { - String commandName = ""; - - if (type.isAssignableFrom(Action.class)) { - Action act = method.getAnnotation(Action.class); - commandName = ((!act.name().equalsIgnoreCase("")) ? act.name() : method.getName()); - } - - if (type.isAssignableFrom(TimerAction.class)) { - TimerAction act = method.getAnnotation(TimerAction.class); - commandName = ((!act.name().equalsIgnoreCase("")) ? act.name() : method.getName()); - } - - return commandName; - } - - private static Class getInputType(Method method, Class type) { - Class inputType = null; - - if (type.isAssignableFrom(Action.class)) { - Action act = method.getAnnotation(Action.class); - inputType = (!act.inputType().isAssignableFrom(Action.Default.class) ? act.inputType() : method.getParameterTypes()[0]); - } - - if (type.isAssignableFrom(TimerAction.class)) { - TimerAction act = method.getAnnotation(TimerAction.class); - inputType = (!act.inputType().isAssignableFrom(TimerAction.Default.class) ? act.inputType() : method.getParameterTypes()[0]); - } - - return inputType; - } - - private static Class getOutputType(Method method, Class type) { - Class outputType = null; - - if (type.isAssignableFrom(Action.class)) { - Action act = method.getAnnotation(Action.class); - outputType = (!act.outputType().isAssignableFrom(Action.Default.class) ? act.outputType() : method.getReturnType()); - } - - if (type.isAssignableFrom(TimerAction.class)) { - TimerAction act = method.getAnnotation(TimerAction.class); - outputType = (!act.outputType().isAssignableFrom(TimerAction.Default.class) ? act.outputType() : method.getReturnType()); - } - - return outputType; - } - - private static ActorOuterClass.Kind getKind(ActorKind kind) { - switch (kind) { - case UNNAMED: - return ActorOuterClass.Kind.UNAMED; - case POOLED: - return ActorOuterClass.Kind.POOLED; - case PROXY: - return ActorOuterClass.Kind.PROXY; - default: - return ActorOuterClass.Kind.NAMED; - } - } } \ No newline at end of file diff --git a/src/main/java/io/eigr/spawn/internal/transport/client/OkHttpSpawnClient.java b/src/main/java/io/eigr/spawn/internal/transport/client/OkHttpSpawnClient.java index b987faa..0ad1779 100644 --- a/src/main/java/io/eigr/spawn/internal/transport/client/OkHttpSpawnClient.java +++ b/src/main/java/io/eigr/spawn/internal/transport/client/OkHttpSpawnClient.java @@ -95,6 +95,7 @@ public Protocol.InvocationResponse invoke(Protocol.InvocationRequest request) th return Protocol.InvocationResponse .parseFrom(Objects.requireNonNull(callInvocationResponse.body()).bytes()); } catch (Exception e) { + e.printStackTrace(); throw new ActorInvocationException(e); } } diff --git a/src/main/java/io/eigr/spawn/internal/transport/server/ActorServiceHandler.java b/src/main/java/io/eigr/spawn/internal/transport/server/ActorServiceHandler.java index a58ab67..1907a60 100644 --- a/src/main/java/io/eigr/spawn/internal/transport/server/ActorServiceHandler.java +++ b/src/main/java/io/eigr/spawn/internal/transport/server/ActorServiceHandler.java @@ -3,6 +3,7 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.google.protobuf.Any; +import com.google.protobuf.GeneratedMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; @@ -10,8 +11,10 @@ import io.eigr.functions.protocol.actors.ActorOuterClass.ActorId; import io.eigr.spawn.api.Spawn; import io.eigr.spawn.api.actors.ActorContext; -import io.eigr.spawn.api.actors.ActorFactory; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.StatelessActor; import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; import io.eigr.spawn.api.actors.workflows.SideEffect; import io.eigr.spawn.api.exceptions.ActorInvocationException; import io.eigr.spawn.internal.Entity; @@ -22,15 +25,19 @@ import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.time.Duration; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; -public final class ActorServiceHandler implements HttpHandler { +/** + * Handles HTTP requests for actor actions in an actor-based system. + * Implements {@link HttpHandler} to process incoming HTTP POST requests and invoke actions on actors. + * + * @param the type of {@link ActorBehavior} managed by this handler + */ +public final class ActorServiceHandler implements HttpHandler { private static final Logger log = LoggerFactory.getLogger(ActorServiceHandler.class); private static final int CACHE_MAXIMUM_SIZE = 10_000; private static final int CACHE_EXPIRE_AFTER_WRITE_SECONDS = 60; @@ -38,26 +45,34 @@ public final class ActorServiceHandler implements HttpHandler { private final Spawn spawn; private final String system; - private final List entities; - - private final Cache cache; - - - public ActorServiceHandler(final Spawn spawn, final List actors) { + private final Cache cache; + + /** + * Constructs an {@link ActorServiceHandler} instance. + * + * @param spawn the {@link Spawn} instance representing the actor system + * @param entities the list of {@link Entity} objects representing the actors + */ + public ActorServiceHandler(final Spawn spawn, final List entities) { this.spawn = spawn; this.system = spawn.getSystem(); - this.entities = actors; + this.entities = entities; this.cache = Caffeine.newBuilder() .maximumSize(CACHE_MAXIMUM_SIZE) .expireAfterWrite(Duration.ofSeconds(CACHE_EXPIRE_AFTER_WRITE_SECONDS)) .build(); } + /** + * Handles incoming HTTP requests. + * + * @param exchange the {@link HttpExchange} representing the HTTP request-response exchange + * @throws IOException if an I/O error occurs + */ @Override public void handle(HttpExchange exchange) throws IOException { log.debug("Received Actor Action Request."); - if ("POST".equals(exchange.getRequestMethod())) { try (OutputStream os = exchange.getResponseBody()) { Protocol.ActorInvocationResponse response = handleRequest(exchange); @@ -69,6 +84,13 @@ public void handle(HttpExchange exchange) throws IOException { } } + /** + * Processes the actor invocation request and returns the response. + * + * @param exchange the {@link HttpExchange} representing the HTTP request-response exchange + * @return the {@link Protocol.ActorInvocationResponse} representing the response to the actor invocation + * @throws IOException if an I/O error occurs + */ private Protocol.ActorInvocationResponse handleRequest(HttpExchange exchange) throws IOException { try (InputStream in = exchange.getRequestBody()) { Protocol.ActorInvocation actorInvocationRequest = Protocol.ActorInvocation.parseFrom(in); @@ -87,126 +109,179 @@ private Protocol.ActorInvocationResponse handleRequest(HttpExchange exchange) th actor, commandName, maybeValueResponse); if (maybeValueResponse.isPresent()) { - Value valueResponse = maybeValueResponse.get(); - - Protocol.Context.Builder updatedContextBuilder = Protocol.Context.newBuilder(); - if (Objects.nonNull(valueResponse.getState())) { - Any encodedState = Any.pack(valueResponse.getState()); - updatedContextBuilder.setState(encodedState); - } - - Any encodedValue; - if (Objects.isNull(valueResponse.getResponse())) { - encodedValue = Any.pack(Protocol.Noop.getDefaultInstance()); - } else { - encodedValue = Any.pack(valueResponse.getResponse()); - } - - return Protocol.ActorInvocationResponse.newBuilder() - .setActorName(actor) - .setActorSystem(system) - .setValue(encodedValue) - .setWorkflow(buildWorkflow(valueResponse)) - .setUpdatedContext(updatedContextBuilder.build()) - .setCheckpoint(valueResponse.getCheckpoint()) - .build(); + return buildResponse(maybeValueResponse.get(), actor, system); } - } catch (Exception e) { + e.printStackTrace(); log.error("Error during handle request.", e); + throw new IOException("Action result is null", e); } throw new IOException("Action result is null"); } - private Optional callAction(String system, String actor, String parent, String commandName, Any value, Protocol.Context context) throws ActorInvocationException { + /** + * Invokes the specified action on the actor and returns the result. + * + * @param system the actor system name + * @param actor the actor name + * @param parent the parent actor name + * @param commandName the action name + * @param value the action input value + * @param context the actor context + * @return an {@link Optional} containing the result of the action invocation + * @throws ActorInvocationException if an error occurs during action invocation + */ + private Optional callAction( + String system, String actor, String parent, String commandName, Any value, Protocol.Context context) + throws ActorInvocationException { Optional optionalEntity = getEntityByActor(actor, parent); if (optionalEntity.isPresent()) { Entity entity = optionalEntity.get(); - try { - String actorKey = String.format("%s:%s", system, actor); - Object actorRef = this.cache.getIfPresent(actorKey); - if (Objects.isNull(actorRef)) { - actorRef = buildInstance(entity); - this.cache.put(actorKey, actorRef); - } - - Entity.EntityMethod entityMethod; - - if (entity.getActions().containsKey(commandName)) { - entityMethod = entity.getActions().get(commandName); - } else if (entity.getTimerActions().containsKey(commandName)) { - entityMethod = entity.getTimerActions().get(commandName); - } else { - throw new ActorInvocationException( - String.format("The Actor does not have the desired action: %s", commandName)); - } - - final Method actorMethod = entityMethod.getMethod(); - Class inputType = entityMethod.getInputType(); - log.debug("Action input type is: {}. Deserialize with value {}", inputType, value.getTypeUrl()); - - ActorContext actorContext; - if (context.hasState()) { - Any anyCtxState = context.getState(); - log.debug("[{}] trying to get the state of the Actor {}. Parse Any type {} from State type {}", - system, actor, anyCtxState, entity.getStateType().getSimpleName()); - - Object state = anyCtxState.unpack(entity.getStateType()); - actorContext = new ActorContext(this.spawn, state); - } else { - actorContext = new ActorContext(this.spawn); - } - - if (inputType.isAssignableFrom(ActorContext.class) && actorMethod.getParameterTypes().length == 1) { - return Optional.of((Value) actorMethod.invoke(actorRef, actorContext)); - } else { - final Object unpack = value.unpack(inputType); - return Optional.of((Value) actorMethod.invoke(actorRef, unpack, actorContext)); - } - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw new ActorInvocationException(e); - } catch (InvalidProtocolBufferException e) { - throw new ActorInvocationException(e); - } catch (NoSuchMethodException | InstantiationException e) { + ActorBehavior actorRef = getOrCreateActor(system, actor, entity); + Entity.EntityMethod entityMethod = getEntityMethod(commandName, entity); + ActorContext actorContext = createActorContext(context, entity); + + return invokeAction(actorRef, commandName, value, entityMethod, actorContext); + } catch (ReflectiveOperationException | InvalidProtocolBufferException e) { throw new ActorInvocationException(e); } } - return Optional.empty(); } - private Object buildInstance(Entity entity) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { - return buildInstanceByArg(entity); + private Optional invokeAction( + ActorBehavior actorRef, String commandName, Any value, Entity.EntityMethod entityMethod, ActorContext actorContext) + throws ReflectiveOperationException, InvalidProtocolBufferException, ActorInvocationException { + if (entityMethod.getArity() == 0) { + return Optional.of((Value) actorRef.call(commandName, actorContext)); + } else { + Class inputType = entityMethod.getInputType(); + final var unpackedValue = value.unpack(inputType); + return Optional.of((Value) actorRef.call(commandName, actorContext, unpackedValue)); + } } - private Object buildInstanceByArg(Entity entity) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { - if (entity.getActorFactory().isPresent() && entity.getActorArg().isPresent()) { - ActorFactory factory = entity.getActorFactory().get(); - Object arg = entity.getActorArg().get(); - return factory.newInstance(arg); + /** + * Builds or retrieves an actor behavior instance from the cache. + * + * @param system the actor system name + * @param actor the actor name + * @param entity the {@link Entity} object representing the actor + * @return the {@link ActorBehavior} instance + * @throws ReflectiveOperationException if a reflection operation fails + */ + private ActorBehavior getOrCreateActor(String system, String actor, Entity entity) throws ReflectiveOperationException { + String actorKey = String.format("%s:%s", system, actor); + ActorBehavior actorRef = cache.getIfPresent(actorKey); + if (Objects.isNull(actorRef)) { + actorRef = buildInstance(entity); + cache.put(actorKey, (B) actorRef); } + return actorRef; + } - Class klass = entity.getActorType(); - Constructor constructor = klass.getConstructor(); - return constructor.newInstance(); + /** + * Retrieves the method corresponding to the specified command name from the entity. + * + * @param commandName the action name + * @param entity the {@link Entity} object representing the actor + * @return the {@link Entity.EntityMethod} representing the method for the command + * @throws ActorInvocationException if the entity does not contain the desired action + */ + private Entity.EntityMethod getEntityMethod(String commandName, Entity entity) throws ActorInvocationException { + if (entity.getActions().containsKey(commandName)) { + return (Entity.EntityMethod) entity.getActions().get(commandName); + } else if (entity.getTimerActions().containsKey(commandName)) { + return (Entity.EntityMethod) entity.getTimerActions().get(commandName); + } else { + throw new ActorInvocationException( + String.format("The Actor does not have the desired action: %s", commandName)); + } } + /** + * Creates an {@link ActorContext} instance using the provided context and entity state. + * + * @param context the {@link Protocol.Context} representing the actor context + * @param entity the {@link Entity} object representing the actor + * @return the {@link ActorContext} instance + * @throws InvalidProtocolBufferException if the state cannot be unpacked + */ + private ActorContext createActorContext(Protocol.Context context, Entity entity) throws InvalidProtocolBufferException { + if (context.hasState()) { + Any anyCtxState = context.getState(); + log.debug("[{}] trying to get the state of the Actor {}. Parse Any type {} from State type {}", + system, entity.getActorName(), anyCtxState, entity.getStateType().getSimpleName()); + + Object state = anyCtxState.unpack(entity.getStateType()); + return new ActorContext(spawn, state); + } else { + return new ActorContext(spawn); + } + } + + /** + * Builds an {@link ActorBehavior} instance from the provided entity. + * + * @param entity the {@link Entity} object representing the actor + * @return the {@link ActorBehavior} instance + * @throws ReflectiveOperationException if a reflection operation fails + */ + private B buildInstance(Entity entity) throws ReflectiveOperationException { + Constructor constructor = entity.getActor().getClass().getConstructor(); + if (entity.isStateful()) { + StatefulActor stActor = (StatefulActor) constructor.newInstance(); + return (B) stActor.configure(entity.getCtx()); + } + + StatelessActor stsActor = (StatelessActor) constructor.newInstance(); + return (B) stsActor.configure(entity.getCtx()); + } + + /** + * Retrieves an {@link Entity} based on the actor or parent actor name. + * + * @param actor the actor name + * @param parent the parent actor name + * @return an {@link Optional} containing the {@link Entity} if found + */ private Optional getEntityByActor(String actor, String parent) { - Optional entity = this.entities.stream() + return entities.stream() .filter(e -> e.getActorName().equalsIgnoreCase(actor)) - .findFirst(); + .findFirst() + .or(() -> entities.stream() + .filter(e -> e.getActorName().equalsIgnoreCase(parent)) + .findFirst()); + } - if (entity.isPresent()) { - return entity; - } + private Protocol.ActorInvocationResponse buildResponse(Value valueResponse, String actor, String system) { + Protocol.Context.Builder updatedContextBuilder = Protocol.Context.newBuilder(); + + Optional.ofNullable(valueResponse.getState()) + .ifPresent(state -> updatedContextBuilder.setState(Any.pack(state))); - return this.entities.stream() - .filter(e -> e.getActorName().equalsIgnoreCase(parent)) - .findFirst(); + Any encodedValue = Optional.ofNullable(valueResponse.getResponse()) + .map(value -> Any.pack(value)) + .orElse(Any.pack(Protocol.Noop.getDefaultInstance())); + + return Protocol.ActorInvocationResponse.newBuilder() + .setActorName(actor) + .setActorSystem(system) + .setValue(encodedValue) + .setWorkflow(buildWorkflow(valueResponse)) + .setUpdatedContext(updatedContextBuilder.build()) + .setCheckpoint(valueResponse.getCheckpoint()) + .build(); } + /** + * Builds a {@link Protocol.Workflow} from the provided value response. + * + * @param valueResponse the {@link Value} representing the response value + * @return the {@link Protocol.Workflow} instance + */ private Protocol.Workflow buildWorkflow(Value valueResponse) { Protocol.Workflow.Builder workflowBuilder = Protocol.Workflow.newBuilder(); @@ -218,10 +293,15 @@ private Protocol.Workflow buildWorkflow(Value valueResponse) { return workflowBuilder.build(); } + /** + * Converts a list of {@link SideEffect} to a list of {@link Protocol.SideEffect}. + * + * @param effects the list of {@link SideEffect} objects + * @return the list of {@link Protocol.SideEffect} objects + */ private List getProtocolEffects(List> effects) { return effects.stream() .map(SideEffect::build) .collect(Collectors.toList()); } - } diff --git a/src/main/proto/eigr/functions/protocol/actors/actor.proto b/src/main/proto/eigr/functions/protocol/actors/actor.proto index 5e3aa71..e4c2157 100644 --- a/src/main/proto/eigr/functions/protocol/actors/actor.proto +++ b/src/main/proto/eigr/functions/protocol/actors/actor.proto @@ -66,10 +66,11 @@ message ActorState { google.protobuf.Any state = 2; } -// TODO doc here +// Metadata represents a set of key-value pairs that can be used to +// provide additional information about an Actor. message Metadata { - // A channel group represents a way to send actions to various actors - // that belong to a certain semantic group. + // A channel group represents a way to send actions to various actors + // that belong to a certain semantic group. Following the Pub-Sub pattern. repeated Channel channel_group = 1; map tags = 2; @@ -84,32 +85,32 @@ message Channel { } // The type that defines the runtime characteristics of the Actor. -// Regardless of the type of actor it is important that +// Regardless of the type of actor it is important that // all actors are registered during the proxy and host initialization phase. enum Kind { // When no type is informed, the default to be assumed will be the Named pattern. UNKNOW_KIND = 0; - // NAMED actors are used to create children of this based actor at runtime + // NAMED actors as the name suggests have only one real instance of themselves running + // during their entire lifecycle. That is, they are the opposite of the UNNAMED type Actors. NAMED = 1; - // UNAMED actors as the name suggests have only one real instance of themselves running - // during their entire lifecycle. That is, they are the opposite of the NAMED type Actors. - UNAMED = 2; - - // Pooled Actors are similar to Unamed actors, but unlike them, - // their identifying name will always be the one registered at the system initialization stage. - // The great advantage of Pooled actors is that they have multiple instances of themselves + // UNNAMED actors are used to create children of this based actor at runtime + UNNAMED = 2; + + // Pooled Actors are similar to Unnamed actors, but unlike them, + // their identifying name will always be the one registered at the system initialization stage. + // The great advantage of Pooled actors is that they have multiple instances of themselves // acting as a request service pool. - // Pooled actors are also stateless actors, that is, they will not have their - // in-memory state persisted via Statesstore. This is done to avoid problems + // Pooled actors are also stateless actors, that is, they will not have their + // in-memory state persisted via Statesstore. This is done to avoid problems // with the correctness of the stored state. - // Pooled Actors are generally used for tasks where the Actor Model would perform worse - // than other concurrency models and for tasks that do not require state concerns. - // Integration flows, data caching, proxies are good examples of use cases + // Pooled Actors are generally used for tasks where the Actor Model would perform worse + // than other concurrency models and for tasks that do not require state concerns. + // Integration flows, data caching, proxies are good examples of use cases // for this type of Actor. POOLED = 3; - + // Reserved for future use PROXY = 4; } @@ -142,7 +143,7 @@ message ActorId { // Name of a ActorSystem string system = 2; - // When the Actor is of the Unamed type, + // When the Actor is of the Unnamed type, // the name of the parent Actor must be informed here. string parent = 3; } diff --git a/src/main/proto/eigr/functions/protocol/actors/protocol.proto b/src/main/proto/eigr/functions/protocol/actors/protocol.proto index 28ec777..fec7e03 100644 --- a/src/main/proto/eigr/functions/protocol/actors/protocol.proto +++ b/src/main/proto/eigr/functions/protocol/actors/protocol.proto @@ -253,7 +253,7 @@ message ProxyInfo { int32 protocol_minor_version = 2; string proxy_name = 3; - + string proxy_version = 4; } @@ -274,12 +274,9 @@ message SideEffect { // https://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html // https://www.enterpriseintegrationpatterns.com/patterns/messaging/BroadcastAggregate.html message Broadcast { - // Channel of target Actors + // Target topic or channel string channel_group = 1; - // Action. Only Actors that have this action will run successfully - string action_name = 2; - // Payload oneof payload { google.protobuf.Any value = 3; @@ -291,27 +288,27 @@ message Broadcast { // Useful for handle `pipes` pattern: // https://www.enterpriseintegrationpatterns.com/patterns/messaging/PipesAndFilters.html message Pipe { - // Target Actor - string actor = 1; + // Target Actor + string actor = 1; - // Action. - string action_name = 2; + // Action. + string action_name = 2; } // Sends the input of a action of an Actor to the input of another action of an Actor // Useful for handle `content-basead router` pattern // https://www.enterpriseintegrationpatterns.com/patterns/messaging/ContentBasedRouter.html message Forward { - // Target Actor - string actor = 1; + // Target Actor + string actor = 1; - // Action. - string action_name = 2; + // Action. + string action_name = 2; } -// Container for archicetural message patterns +// Container for archicetural message patterns message Workflow { - + Broadcast broadcast = 2; repeated SideEffect effects = 1; @@ -328,12 +325,13 @@ message Workflow { // * system: See ActorSystem message. // * actor: The target Actor, i.e. the one that the user function is calling to perform some computation. // * caller: The caller Actor -// * action_name: The function or method on the target Actor that will receive this request +// * action_name: The function or method on the target Actor that will receive this request // and perform some useful computation with the sent data. // * value: This is the value sent by the user function to be computed by the request's target Actor action. -// * async: Indicates whether the action should be processed synchronously, where a response should be sent back to the user function, +// * async: Indicates whether the action should be processed synchronously, where a response should be sent back to the user function, // or whether the action should be processed asynchronously, i.e. no response sent to the caller and no waiting. // * metadata: Meta information or headers +// * register_ref: If the invocation should register the specific actor with the given name without having to call register before message InvocationRequest { eigr.functions.protocol.actors.ActorSystem system = 1; diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..b1f9bfe --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/src/test/java/io/eigr/spawn/AbstractContainerBaseTest.java b/src/test/java/io/eigr/spawn/AbstractContainerBaseTest.java index e6d33b6..6835f6e 100644 --- a/src/test/java/io/eigr/spawn/AbstractContainerBaseTest.java +++ b/src/test/java/io/eigr/spawn/AbstractContainerBaseTest.java @@ -9,6 +9,8 @@ import io.eigr.spawn.api.extensions.SimpleDependencyInjector; import io.eigr.spawn.test.actors.ActorWithConstructor; import io.eigr.spawn.test.actors.JoeActor; +import io.eigr.spawn.test.actors.StatelessNamedActor; +import io.eigr.spawn.test.actors.UnNamedActor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.Testcontainers; @@ -31,7 +33,6 @@ abstract class AbstractContainerBaseTest { SPAWN_CONTAINER = new GenericContainer<>(DockerImageName.parse(spawnProxyImage)) .withCreateContainerCmdModifier(e -> e.withHostConfig(HostConfig.newHostConfig() .withPortBindings(PortBinding.parse("9004:9004")))) - // .withEnv("TZ", "America/Fortaleza") .withEnv("SPAWN_PROXY_LOGGER_LEVEL", "DEBUG") .withEnv("SPAWN_STATESTORE_KEY", "3Jnb0hZiHIzHTOih7t2cTEPEpY98Tu1wvQkPfq/XwqE=") .withEnv("PROXY_ACTOR_SYSTEM_NAME", spawnSystemName) @@ -52,23 +53,25 @@ abstract class AbstractContainerBaseTest { DependencyInjector injector = SimpleDependencyInjector.createInjector(); injector.bind(String.class, "Hello with Constructor"); - spawnSystem = new Spawn.SpawnSystem() - .create(spawnSystemName) - .withActor(JoeActor.class) - .withActor(ActorWithConstructor.class, injector, arg -> new ActorWithConstructor((DependencyInjector) arg)) - .withTerminationGracePeriodSeconds(5) - .withTransportOptions(TransportOpts.builder() - .port(8091) - .proxyPort(9004) - .build()) - .build(); - try { + spawnSystem = new Spawn.SpawnSystem() + .create(spawnSystemName, injector) + .withActor(JoeActor.class) + .withActor(UnNamedActor.class) + .withActor(ActorWithConstructor.class) + .withActor(StatelessNamedActor.class) + .withTerminationGracePeriodSeconds(5) + .withTransportOptions(TransportOpts.builder() + .port(8091) + .proxyPort(9004) + .build()) + .build(); + spawnSystem.start(); + log.info(String.format("%s started", spawnSystemName)); } catch (SpawnException e) { throw new RuntimeException(e); } - log.info(String.format("%s started", spawnSystemName)); } } diff --git a/src/test/java/io/eigr/spawn/ContainerTest.java b/src/test/java/io/eigr/spawn/ContainerTest.java deleted file mode 100644 index 6440e8a..0000000 --- a/src/test/java/io/eigr/spawn/ContainerTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.eigr.spawn; - -import io.eigr.spawn.api.ActorIdentity; -import io.eigr.spawn.api.ActorRef; -import io.eigr.spawn.api.exceptions.SpawnException; -import io.eigr.spawn.java.test.domain.Actor; -import org.junit.Test; - -import java.util.Optional; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -public final class ContainerTest extends AbstractContainerBaseTest { - - @Test - public void testApp() throws SpawnException { - ActorRef joeActor = spawnSystem.createActorRef( - ActorIdentity.of(spawnSystemName, "test_joe")); - - Actor.Request msg = Actor.Request.newBuilder() - .setLanguage("erlang") - .build(); - - Optional maybeReply = - joeActor.invoke("setLanguage", msg, Actor.Reply.class); - - if (maybeReply.isPresent()) { - Actor.Reply reply = maybeReply.get(); - assertNotNull(reply); - assertEquals("Hello From Java", reply.getResponse()); - } else { - throw new RuntimeException("Error"); - } - - } -} diff --git a/src/test/java/io/eigr/spawn/EntityTest.java b/src/test/java/io/eigr/spawn/EntityTest.java deleted file mode 100644 index d54490f..0000000 --- a/src/test/java/io/eigr/spawn/EntityTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.eigr.spawn; - -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.internal.Entity; -import io.eigr.spawn.test.actors.JoeActor; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class EntityTest { - - @Test - public void testEntityBuilder() { - StatefulNamedActor annotation = JoeActor.class.getAnnotation(StatefulNamedActor.class); - final Entity entity = Entity.fromAnnotationToEntity(JoeActor.class, annotation, null, null); - assertEquals(1, entity.getActions().values().size()); - assertEquals(0, entity.getTimerActions().values().size()); - assertEquals("test.channel", entity.getChannel()); - } -} diff --git a/src/test/java/io/eigr/spawn/MyTestActorEntity.java b/src/test/java/io/eigr/spawn/MyTestActorEntity.java new file mode 100644 index 0000000..004366f --- /dev/null +++ b/src/test/java/io/eigr/spawn/MyTestActorEntity.java @@ -0,0 +1,45 @@ +package io.eigr.spawn; + +import io.eigr.spawn.api.actors.*; + +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.java.test.domain.Actor.*; + +import java.util.Optional; + +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.*; + +public class MyTestActorEntity extends StatefulActor { + + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new NamedActorBehavior( + name("test"), + deactivated(30000), + snapshot(10000), + init(this::handleInit), + action("Hi", actorCtx -> Value.at().noReply()), + action("SayHello", this::sayHello), + action("SayHelloTwo", (actorCtx, arg) -> Value.at().noReply()) + ); + } + + private Value handleInit(ActorContext ctx) { + Optional> maybeState = ctx.getState(); + if (maybeState.isPresent()) { + State state = maybeState.get().getState().get(); + return Value.at() + .state(state) + .noReply(); + } + return Value.at() + .state(State.newBuilder().addLanguages("Java").build()) + .noReply(); + } + + public Value sayHello(ActorContext ctx, Request req) { + return Value.at().response(Request.newBuilder().setLanguage("Java").build()).reply(); + } +} diff --git a/src/test/java/io/eigr/spawn/SpawnTest.java b/src/test/java/io/eigr/spawn/SpawnTest.java index 9b8805b..a7f50b1 100644 --- a/src/test/java/io/eigr/spawn/SpawnTest.java +++ b/src/test/java/io/eigr/spawn/SpawnTest.java @@ -2,16 +2,12 @@ import io.eigr.spawn.api.ActorIdentity; import io.eigr.spawn.api.ActorRef; -import io.eigr.spawn.api.Spawn; -import io.eigr.spawn.api.TransportOpts; import io.eigr.spawn.api.exceptions.ActorCreationException; import io.eigr.spawn.api.exceptions.ActorInvocationException; -import io.eigr.spawn.api.extensions.DependencyInjector; -import io.eigr.spawn.api.extensions.SimpleDependencyInjector; import io.eigr.spawn.java.test.domain.Actor; -import io.eigr.spawn.test.actors.ActorWithConstructor; import io.eigr.spawn.test.actors.JoeActor; -import org.junit.Before; +import io.eigr.spawn.test.actors.StatelessNamedActor; +import io.eigr.spawn.test.actors.UnNamedActor; import org.junit.Test; import java.util.Optional; @@ -19,48 +15,77 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -public class SpawnTest { - - private Spawn spawnSystem; - - @Before - public void before() throws Exception { - DependencyInjector injector = SimpleDependencyInjector.createInjector(); - injector.bind(String.class, "Hello with Constructor"); - - spawnSystem = new Spawn.SpawnSystem() - .create("spawn-system") - .withActor(JoeActor.class) - .withActor(ActorWithConstructor.class, injector, arg -> new ActorWithConstructor((DependencyInjector) arg)) - .withTransportOptions( - TransportOpts.builder() - .port(8091) - .proxyPort(9003) - .build() - ) +public class SpawnTest extends AbstractContainerBaseTest { + + @Test + public void testNamedInvocation() throws ActorCreationException, ActorInvocationException { + ActorRef joeActor = spawnSystem.createActorRef( + ActorIdentity.of(spawnSystemName, "JoeActor")); + + Class type = joeActor.getType(); + + assertEquals(type, JoeActor.class); + assertNotNull(joeActor); + + Actor.Request msg = Actor.Request.newBuilder() + .setLanguage("Erlang") .build(); - spawnSystem.start(); + Optional maybeReply = + joeActor.invoke("SetLanguage", msg, Actor.Reply.class); + + if (maybeReply.isPresent()) { + Actor.Reply reply = maybeReply.get(); + assertNotNull(reply); + assertEquals("Hi Erlang. Hello From Java", reply.getResponse()); + } } @Test - public void testApp() throws ActorCreationException, ActorInvocationException { - ActorRef joeActor = spawnSystem.createActorRef( - ActorIdentity.of("spawn-system", "test_joe")); + public void testUnNamedInvocation() throws ActorCreationException, ActorInvocationException { + ActorRef unNamedJoeActor = spawnSystem.createActorRef( + ActorIdentity.of(spawnSystemName, "UnNamedJoeActor", "UnNamedActor")); - assertNotNull(joeActor); + Class type = unNamedJoeActor.getType(); + + assertEquals(type, UnNamedActor.class); + assertNotNull(unNamedJoeActor); + + Actor.Request msg = Actor.Request.newBuilder() + .setLanguage("Erlang") + .build(); + + Optional maybeReply = + unNamedJoeActor.invoke("SetLanguage", msg, Actor.Reply.class); + + if (maybeReply.isPresent()) { + Actor.Reply reply = maybeReply.get(); + assertNotNull(reply); + assertEquals("Hi Erlang. Hello From Java", reply.getResponse()); + } + } + + @Test + public void testStatelessInvocation() throws ActorCreationException, ActorInvocationException { + ActorRef statelessNamedActor = spawnSystem.createActorRef( + ActorIdentity.of(spawnSystemName, "StatelessNamedActor")); + + Class type = statelessNamedActor.getType(); + + assertEquals(type, StatelessNamedActor.class); + assertNotNull(statelessNamedActor); Actor.Request msg = Actor.Request.newBuilder() - .setLanguage("erlang") + .setLanguage("Elixir") .build(); Optional maybeReply = - joeActor.invoke("setLanguage", msg, Actor.Reply.class); + statelessNamedActor.invoke("SetLanguage", msg, Actor.Reply.class); if (maybeReply.isPresent()) { Actor.Reply reply = maybeReply.get(); assertNotNull(reply); - assertEquals("Hello From Java", reply.getResponse()); + assertEquals("Hi Elixir. Hello From Java", reply.getResponse()); } } } diff --git a/src/test/java/io/eigr/spawn/WorkflowTest.java b/src/test/java/io/eigr/spawn/WorkflowTest.java index 213ddb4..8e66d56 100644 --- a/src/test/java/io/eigr/spawn/WorkflowTest.java +++ b/src/test/java/io/eigr/spawn/WorkflowTest.java @@ -3,46 +3,32 @@ import io.eigr.functions.protocol.Protocol; import io.eigr.spawn.api.ActorIdentity; import io.eigr.spawn.api.ActorRef; -import io.eigr.spawn.api.Spawn; -import io.eigr.spawn.api.TransportOpts; import io.eigr.spawn.api.actors.workflows.Broadcast; import io.eigr.spawn.api.actors.workflows.Forward; import io.eigr.spawn.api.actors.workflows.Pipe; import io.eigr.spawn.api.actors.workflows.SideEffect; import io.eigr.spawn.api.exceptions.SpawnException; import io.eigr.spawn.java.test.domain.Actor; -import io.eigr.spawn.test.actors.JoeActor; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; -public class WorkflowTest { +public class WorkflowTest extends AbstractContainerBaseTest { private ActorRef joeActorRef; - private Spawn spawnSystem; + @Before public void before() throws SpawnException { - spawnSystem = new Spawn.SpawnSystem() - .create("spawn-system") - .withActor(JoeActor.class) - .withTransportOptions( - TransportOpts.builder() - .port(8091) - .proxyPort(9003) - .build() - ) - .build(); - joeActorRef = spawnSystem.createActorRef( - ActorIdentity.of("spawn-system", "joe")); + ActorIdentity.of(spawnSystemName, "joe")); } @Test public void testBroadcastBuilder() { Broadcast broadcast = Broadcast.to("test.channel", "hi", Actor.Request.getDefaultInstance()); final Protocol.Broadcast protocolBroadcast = broadcast.build(); - assertEquals("hi", protocolBroadcast.getActionName()); + assertEquals("hi", protocolBroadcast.getChannelGroup()); assertEquals("test.channel", protocolBroadcast.getChannelGroup()); assertNotNull(protocolBroadcast.getValue()); } diff --git a/src/test/java/io/eigr/spawn/test/actors/ActorWithConstructor.java b/src/test/java/io/eigr/spawn/test/actors/ActorWithConstructor.java index 0cbdea8..4941b14 100644 --- a/src/test/java/io/eigr/spawn/test/actors/ActorWithConstructor.java +++ b/src/test/java/io/eigr/spawn/test/actors/ActorWithConstructor.java @@ -1,36 +1,42 @@ package io.eigr.spawn.test.actors; import io.eigr.spawn.api.actors.ActorContext; +import io.eigr.spawn.api.actors.StatefulActor; import io.eigr.spawn.api.actors.Value; -import io.eigr.spawn.api.actors.annotations.Action; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.api.extensions.DependencyInjector; -import io.eigr.spawn.java.test.domain.Actor; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.test.domain.Actor.Reply; +import io.eigr.spawn.java.test.domain.Actor.Request; +import io.eigr.spawn.java.test.domain.Actor.State; -@StatefulNamedActor(name = "test_actor_constructor", stateType = Actor.State.class) -public final class ActorWithConstructor { +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.action; +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.name; +public final class ActorWithConstructor extends StatefulActor { - private final String defaultMessage; + private String defaultMessage; - public ActorWithConstructor(DependencyInjector injector) { - this.defaultMessage = injector.getInstance(String.class); + @Override + public ActorBehavior configure(BehaviorCtx context) { + defaultMessage = context.getInjector().getInstance(String.class); + return new NamedActorBehavior( + name("TestActorConstructor"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); } - @Action(inputType = Actor.Request.class) - public Value setLanguage(Actor.Request msg, ActorContext context) { - if (context.getState().isPresent()) { - } - + private Value setLanguage(ActorContext context, Request msg) { return Value.at() - .response(Actor.Reply.newBuilder() + .response(Reply.newBuilder() .setResponse(defaultMessage) .build()) .state(updateState("java")) .reply(); } - private Actor.State updateState(String language) { - return Actor.State.newBuilder() + private State updateState(String language) { + return State.newBuilder() .addLanguages(language) .build(); } diff --git a/src/test/java/io/eigr/spawn/test/actors/JoeActor.java b/src/test/java/io/eigr/spawn/test/actors/JoeActor.java index 0f2c3f4..8549007 100644 --- a/src/test/java/io/eigr/spawn/test/actors/JoeActor.java +++ b/src/test/java/io/eigr/spawn/test/actors/JoeActor.java @@ -1,29 +1,44 @@ package io.eigr.spawn.test.actors; -import io.eigr.spawn.api.actors.Value; import io.eigr.spawn.api.actors.ActorContext; -import io.eigr.spawn.api.actors.annotations.Action; -import io.eigr.spawn.api.actors.annotations.stateful.StatefulNamedActor; -import io.eigr.spawn.api.actors.workflows.SideEffect; -import io.eigr.spawn.java.test.domain.Actor; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.test.domain.Actor.Reply; +import io.eigr.spawn.java.test.domain.Actor.Request; +import io.eigr.spawn.java.test.domain.Actor.State; + +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.*; + +public final class JoeActor extends StatefulActor { + + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new NamedActorBehavior( + name("JoeActor"), + channel("test.channel"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } -@StatefulNamedActor(name = "test_joe", stateType = Actor.State.class, channel = "test.channel") -public final class JoeActor { - @Action(inputType = Actor.Request.class) - public Value setLanguage(Actor.Request msg, ActorContext context) { + private Value setLanguage(ActorContext context, Request msg) { if (context.getState().isPresent()) { + //Do something with previous state } return Value.at() - .response(Actor.Reply.newBuilder() - .setResponse("Hello From Java") + .response(Reply.newBuilder() + .setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage())) .build()) - .state(updateState("java")) + .state(updateState(msg.getLanguage())) .reply(); } - private Actor.State updateState(String language) { - return Actor.State.newBuilder() + private State updateState(String language) { + return State.newBuilder() .addLanguages(language) .build(); } diff --git a/src/test/java/io/eigr/spawn/test/actors/StatelessNamedActor.java b/src/test/java/io/eigr/spawn/test/actors/StatelessNamedActor.java new file mode 100644 index 0000000..4bfbf67 --- /dev/null +++ b/src/test/java/io/eigr/spawn/test/actors/StatelessNamedActor.java @@ -0,0 +1,37 @@ +package io.eigr.spawn.test.actors; + +import io.eigr.spawn.api.actors.ActorContext; +import io.eigr.spawn.api.actors.StatelessActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.test.domain.Actor.Reply; +import io.eigr.spawn.java.test.domain.Actor.Request; +import io.eigr.spawn.java.test.domain.Actor.State; + +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.action; +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.name; + +public final class StatelessNamedActor extends StatelessActor { + + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new NamedActorBehavior( + name("StatelessNamedActor"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } + + private Value setLanguage(ActorContext context, Request msg) { + if (context.getState().isPresent()) { + } + + return Value.at() + .response(Reply.newBuilder() + .setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage())) + .build()) + .reply(); + } +} diff --git a/src/test/java/io/eigr/spawn/test/actors/UnNamedActor.java b/src/test/java/io/eigr/spawn/test/actors/UnNamedActor.java new file mode 100644 index 0000000..6b0a75b --- /dev/null +++ b/src/test/java/io/eigr/spawn/test/actors/UnNamedActor.java @@ -0,0 +1,44 @@ +package io.eigr.spawn.test.actors; + +import io.eigr.spawn.api.actors.ActorContext; +import io.eigr.spawn.api.actors.StatefulActor; +import io.eigr.spawn.api.actors.Value; +import io.eigr.spawn.api.actors.behaviors.ActorBehavior; +import io.eigr.spawn.api.actors.behaviors.BehaviorCtx; +import io.eigr.spawn.api.actors.behaviors.NamedActorBehavior; +import io.eigr.spawn.api.actors.behaviors.UnNamedActorBehavior; +import io.eigr.spawn.internal.ActionBindings; +import io.eigr.spawn.java.test.domain.Actor.Reply; +import io.eigr.spawn.java.test.domain.Actor.Request; +import io.eigr.spawn.java.test.domain.Actor.State; + +import static io.eigr.spawn.api.actors.behaviors.ActorBehavior.*; + +public final class UnNamedActor extends StatefulActor { + + @Override + public ActorBehavior configure(BehaviorCtx context) { + return new UnNamedActorBehavior( + name("UnNamedActor"), + action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage)) + ); + } + + private Value setLanguage(ActorContext context, Request msg) { + if (context.getState().isPresent()) { + } + + return Value.at() + .response(Reply.newBuilder() + .setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage())) + .build()) + .state(updateState("java")) + .reply(); + } + + private State updateState(String language) { + return State.newBuilder() + .addLanguages(language) + .build(); + } +} diff --git a/src/test/proto/eigr/functions/java/sdk/test/actor.proto b/src/test/proto/eigr/functions/java/sdk/test/actor.proto index 2ffea96..81b1951 100644 --- a/src/test/proto/eigr/functions/java/sdk/test/actor.proto +++ b/src/test/proto/eigr/functions/java/sdk/test/actor.proto @@ -12,4 +12,20 @@ message Request { message Reply { string response = 1; +} + +service JoeActor { + rpc SetLanguage(Request) returns (Reply); +} + +service TestActorConstructor { + rpc SetLanguage(Request) returns (Reply); +} + +service UnNamedActor { + rpc SetLanguage(Request) returns (Reply); +} + +service StatelessNamedActor { + rpc SetLanguage(Request) returns (Reply); } \ No newline at end of file diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 0000000..b1f9bfe --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + +