JVM User Language Support for Spawn.
Spawn is a Stateful Serverless Runtime and Framework based on the Actor Model and operates as a Service Mesh.
Spawn's main goal is to remove the complexity in developing services or microservices, providing simple and intuitive APIs, as well as a declarative deployment and configuration model and based on a Serverless architecture and Actor Model. This leaves the developer to focus on developing the business domain while the platform deals with the complexities and infrastructure needed to support the scalable, resilient, distributed, and event-driven architecture that modern systems requires.
Spawn is based on the sidecar proxy pattern to provide a polyglot Actor Model framework and platform. Spawn's technology stack, built on the BEAM VM (Erlang's virtual machine) and OTP, provides support for different languages from its native Actor model.
For more information consult the main repository documentation.
First we must create a new Java project. In this example we will use Maven as our package manager.
mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4Now you will need to fill in the data for groupId, artifactId, version, and package. Let's call our maven artifact spawn-java-demo. The output of this command will be similar to the output below
$ mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4
[INFO] Scanning for projects...
[INFO] Generating project in Interactive mode
[INFO] Archetype repository not defined. Using the one from [org.apache.maven.archetypes:maven-archetype-quickstart:1.4] found in catalog remote
Define value for property 'groupId': io.eigr.spawn
Define value for property 'artifactId': spawn-java-demo
Define value for property 'version' 1.0-SNAPSHOT: :
Define value for property 'package' io.eigr.spawn: : io.eigr.spawn.java.demo
Confirm properties configuration:
groupId: io.eigr.spawn
artifactId: spawn-java-demo
version: 1.0-SNAPSHOT
package: io.eigr.spawn.java.demo
Y: : y
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: maven-archetype-quickstart:1.4
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: io.eigr.spawn
[INFO] Parameter: artifactId, Value: spawn-java-demo
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: io.eigr.spawn.java.demo
[INFO] Parameter: packageInPathFormat, Value: io/eigr/spawn/java/demo
[INFO] Parameter: package, Value: io.eigr.spawn.java.demo
[INFO] Parameter: groupId, Value: io.eigr.spawn
[INFO] Parameter: artifactId, Value: spawn-java-demo
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Project created from Archetype in dir: /home/sleipnir/workspaces/eigr/spawn-java-demo
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:39 min
[INFO] Finished at: 2023-08-28T11:37:57-03:00
[INFO] ------------------------------------------------------------------------The second thing we have to do is add the spawn dependency to the project.
<dependency>
<groupId>com.github.eigr</groupId>
<artifactId>spawn-java-std-sdk</artifactId>
<version>v1.3.0</version>
</dependency>We're also going to configure a few things for our application build to work, including compiling the protobuf files. See below a full example of the pom.xml file:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.eigr.spawn</groupId>
<artifactId>spawn-java-demo</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>spawn-java-demo</name>
<url>https://eigr.io</url>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.encoding>UTF-8</project.encoding>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.eigr</groupId>
<artifactId>spawn-java-std-sdk</artifactId>
<version>v1.3.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</version>
</extension>
</extensions>
<!-- make jar runnable -->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedArtifactAttached>true</shadedArtifactAttached>
<transformers>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>io.eigr.spawn.java.demo.App</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.19.2:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.47.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>add-test-sources</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-test-sources/protobuf</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>Now it is necessary to download the dependencies via Maven:
cd spawn-java-demo && mvn installSo far it's all pretty boring and not really Spawn related, so it's time to start playing for real. The first thing we're going to do is define a place to put our protobuf files. In the root of the project we will create a folder called protobuf and some sub folders
mkdir -p src/main/proto/domainThat done, let's create our protobuf file inside the example folder.
touch src/main/proto/domain/domain.protoAnd let's populate this file with the following content:
syntax = "proto3";
package domain;
option java_package = "io.eigr.spawn.java.demo.domain";
message State {
repeated string languages = 1;
}
message Request {
string language = 1;
}
message Reply {
string response = 1;
}
service JoeActor {
rpc SetLanguage(Request) returns (Reply);
}We must compile this file using the protoc utility. In the root of the project type the following command:
mvn protobuf:compileNow in the spawn-java-demo folder we will create our first Java file containing the code of our Actor.
touch src/main/java/io/eigr/spawn/java/demo/Joe.javaPopulate this file with the following content:
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.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<State> {
@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<State> context, Request msg) {
if (context.getState().isPresent()) {
//Do something with previous state
}
return Value.at()
.response(Reply.newBuilder()
.setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage()))
.build())
.state(updateState(msg.getLanguage()))
.reply();
}
private State updateState(String language) {
return State.newBuilder()
.addLanguages(language)
.build();
}
}Now with our Actor properly defined, we just need to start the SDK correctly. Create another file called App.java to serve as your application's entrypoint and fill it with the following content:
package io.eigr.spawn.java.demo;
import io.eigr.spawn.api.Spawn;
public class App {
public static void main(String[] args) throws Exception {
Spawn spawnSystem = new SpawnSystem()
.create("spawn-system")
.withActor(Joe.class)
.build();
spawnSystem.start();
}
}Or passing transport options like:
package io.eigr.spawn.java.demo;
import io.eigr.spawn.api.Spawn;
import io.eigr.spawn.api.TransportOpts;
public class App {
public static void main(String[] args) throws Exception {
TransportOpts opts = TransportOpts.builder()
.port(8091)
.proxyPort(9003)
.executor(Executors.newVirtualThreadPerTaskExecutor()) // If you use java above 19 and use the --enable-preview flag when running the jvm
.build();
Spawn spawnSystem = new SpawnSystem()
.create("spawn-system")
.withActor(Joe.class)
.withTransportOptions(opts)
.build();
spawnSystem.start();
}
}Then:
mvn compile && mvn package && java -jar target/spawn-java-demo-1.0-SNAPSHOT.jar But of course you will need to locally run the Elixir proxy which will actually provide all the functionality for your Java application. One way to do this is to create a docker-compose file containing all the services that your application depends on, in this case, in addition to the Spawn proxy, it also has a database and possibly a nats broker if you want access to more advanced Spawn features.
version: "3.8"
services:
mariadb:
image: mariadb
environment:
MYSQL_ROOT_PASSWORD: admin
MYSQL_DATABASE: eigr-functions-db
MYSQL_USER: admin
MYSQL_PASSWORD: admin
volumes:
- mariadb:/var/lib/mysql
ports:
- "3307:3306"
nats:
image: nats:0.8.0
entrypoint: "/gnatsd -DV"
ports:
- "8222:8222"
- "4222:4222"
spawn-proxy:
build:
context: https://github.com/eigr/spawn.git#main
dockerfile: ./Dockerfile-proxy
restart: always
network_mode: "host"
environment:
SPAWN_USE_INTERNAL_NATS: "true"
SPAWN_PUBSUB_ADAPTER: nats
SPAWN_STATESTORE_KEY: 3Jnb0hZiHIzHTOih7t2cTEPEpY98Tu1wvQkPfq/XwqE=
PROXY_APP_NAME: spawn
PROXY_CLUSTER_STRATEGY: gossip
PROXY_DATABASE_PORT: 3307
PROXY_DATABASE_TYPE: mariadb
PROXY_HTTP_PORT: 9003
USER_FUNCTION_PORT: 8091
depends_on:
- mariadb
- nats
networks:
mysql-compose-network:
driver: bridge
volumes:
mariadb:
You may also want your Actors to be initialized with some dependent objects similarly to how you would use the dependency injection pattern. In this case, it is enough to declare a constructor that receives a single argument for its actor.
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.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<State> {
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))
);
}
// ...
}Then you also need to register your Actor using injector :
package io.eigr.spawn.java.demo;
import io.eigr.spawn.api.Spawn;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main(String[] args) {
DependencyInjector injector = SimpleDependencyInjector.createInjector();
injector.bind(String.class, "Hello with Constructor");
Spawn spawnSystem = new Spawn.SpawnSystem()
.create("spawn-system", injector)
.withActor(Joe.class)
.build();
spawnSystem.start();
}
}Spawn is based on kubernetes and containers, so you will need to generate a docker container for your application. There are many ways to do this, one of them is by adding Maven's jib plugin. Add the following lines to your plugin's section in pom.xml file:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<to>
<image>your-repo-here/spawn-java-demo</image>
</to>
</configuration>
</plugin>finally you will be able to create your container by running the following command in the root of your project:
mvn compile jib:buildAnd this is it to start! Now that you know the basics of local development, we can go a little further.
Spawn Actors abstract a huge amount of developer infrastructure and can be used for many types of jobs. In the sections below we will demonstrate some features available in Spawn that contribute to the development of complex applications in a simplified way.
Sometimes we need to pass many arguments as dependencies to the Actor class. In this case, it is more convenient to use your own dependency injection mechanism. However, the Spawn SDK already comes with an auxiliary class to make this easier for the developer. Let's look at an example:
- First let's take a look at some example dependency classes:
// We will have an interface that represents any type of service.
public interface MessageService {
String getDefaultMessage();
}
// and concrete implementation here
public class MessageServiceImpl implements MessageService {
@Override
public String getDefaultMessage() {
return "Hello Spawn in English";
}
}- Second, let's define an actor so that it receives an instance of the DependencyInjector class through the context of configure method:
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.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<State> {
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<State> 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();
}
}- Then you can pass your dependent classes this way to your Actor:
package io.eigr.spawn.java.demo;
import io.eigr.spawn.api.Spawn;
import io.eigr.spawn.api.extensions.DependencyInjector;
import io.eigr.spawn.api.extensions.SimpleDependencyInjector;
public class App {
public static void main(String[] args) {
DependencyInjector injector = SimpleDependencyInjector.createInjector();
/*
You can bind as many objects as you want. As long as they are of unique types.
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", injector)
.withActor(Joe.class)
.build();
spawnSystem.start();
}
}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 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. However, as an open source project we will be happy if anyone wants to contribute in this regard.
First we need to understand how the various types of actors available in Spawn behave. Spawn defines the following types of Actors:
-
Named Actors: Named actors are actors whose name is defined at compile time. They also behave slightly differently Then unnamed actors and pooled actors. Named actors when they are defined with the stateful parameter equal to True are immediately instantiated when they are registered at the beginning of the program, they can also only be referenced by the name given to them in their definition.
-
Unnamed Actors: Unlike named actors, unnamed actors are only created when they are named at runtime, that is, during program execution. Otherwise, they behave like named actors.
-
Pooled Actors: Pooled Actors, as the name suggests, are a collection of actors that are grouped under the same name assigned to them at compile time. Pooled actors are generally used when higher performance is needed and are also recommended for handling serverless loads.
In addition to these types, Spawn also allows the developer to choose Stateful actors, who need to maintain the state, 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:
package io.eigr.spawn.java.demo.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.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<State> 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();
}
}Other than that the same Named, UnNamed types are supported. Just use the StatelessNamedActor or StatelessUnNamedActor super class.
Another important feature of Spawn Actors is that the lifecycle of each Actor is managed by the platform itself. This means that an Actor will exist when it is invoked and that it will be deactivated after an idle time in its execution. This pattern is known as Virtual Actors but Spawn's implementation differs from some other known frameworks like Orleans or Dapr by defining a specific behavior depending on the type of Actor (named, unnamed, pooled, and etc...).
For example, named actors are instantiated the first time as soon as the host application registers them with the Spawn proxy. Whereas unnamed and pooled actors are instantiated the first time only when they receive their first invocation call.
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 using the channel option as follows:
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.
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;
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 LoopActor extends StatefulActor<State> {
@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<State> context, Request msg) {
return Value.at()
.flow(Broadcast.to("test.channel", "setLanguage", msg))
.response(Domain.Reply.newBuilder()
.setResponse("Hello From Erlang")
.build())
.state(updateState("erlang"))
.reply();
}
// ...
}Actors can also emit side effects to other Actors as part of their response. See an example:
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.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<State> {
@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<State> context, Request msg) {
ActorRef sideEffectReceiverActor = ctx.getSpawnSystem()
.createActorRef(ActorIdentity.of("spawn-system", "MikeFriendActor", "MikeParentActor"));
return Value.at()
.flow(SideEffect.to(sideEffectReceiverActor, "setLanguage", msg))
.response(Reply.newBuilder()
.setResponse(String.format("Hi %s. Hello From Java", msg.getLanguage()))
.build())
.state(updateState(msg.getLanguage()))
.noReply();
}
// ....
}Side effects such as broadcast are not part of the response flow to the caller. They are request-asynchronous events that are emitted after the Actor's state has been saved in memory.
Actors can route some actions to other actors as part of their response. For example, sometimes you may want another Actor to be responsible for processing a message that another Actor has received. We call this forwarding, and it occurs when we want to forward the input argument of a request that a specific Actor has received to the input of an action in another Actor.
See an example:
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.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<State> {
@Override
public ActorBehavior configure(BehaviorCtx context) {
return new NamedActorBehavior(
name("RoutingActor"),
action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage))
);
}
private Value setLanguage(ActorContext<State> context, Request msg) {
ActorRef forwardedActor = ctx.getSpawnSystem()
.createActorRef(ActorIdentity.of("spawn-system", "MikeFriendActor", "MikeActor"));
return Value.at()
.flow(Forward.to(forwardedActor, "setLanguage"))
.noReply();
}
}Similarly, sometimes we want to chain a request through several processes. For example forwarding an actor's computational output as another actor's input. There is this type of routing we call Pipe, as the name suggests, a pipe forwards what would be the response of the received request to the input of another Action in another Actor. In the end, just like in a Forward, it is the response of the last Actor in the chain of routing to the original caller.
Example:
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.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<State> {
@Override
public ActorBehavior configure(BehaviorCtx context) {
return new NamedActorBehavior(
name("PipeActor"),
action("SetLanguage", ActionBindings.of(Request.class, this::setLanguage))
);
}
private Value setLanguage(ActorContext<State> context, Request msg) {
ActorRef pipeReceiverActor = ctx.getSpawnSystem()
.createActorRef(ActorIdentity.of("spawn-system", "JoeActor"));
return Value.at()
.response(Reply.newBuilder()
.setResponse("Hello From Java")
.build())
.flow(Pipe.to(pipeReceiverActor, "someAction"))
.state(updateState("java"))
.noReply();
}
// ...
}Forwards and pipes do not have an upper thread limit other than the request timeout.
The Spawn runtime handles the internal state of your actors. It is he who maintains its state based on the types of actors and configurations that you, the developer, have made.
The persistence of the state of the actors happens through snapshots that follow to Write Behind Pattern during the period in which the Actor is active and Write Ahead 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 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:
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.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<State> {
@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<State> 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();
}
// ...
}The most important thing in this example is the use of the last parameter with the true value:
state(updateState("java"), true)
It is this parameter that will indicate to the Spawn runtime that you want the data to be saved immediately after this Action is called back. In most cases this strategy is completely unnecessary, as the default strategy is sufficient for most use cases. But Spawn democratically lets you choose when you want your data persisted.
In addition to this functionality regarding state management, Spawn also allows you to perform some more operations on your Actors such as restoring the actor's state to a specific point in time:
Restore Example:
TODO
There are several ways to interact with our actors, some internal to the application code and others external to the application code. In this section we will deal with the internal ways of interacting with our actors and this will be done through direct calls to them. For more details on the external ways to interact with your actors see the Activators section.
In order to be able to call methods of an Actor, we first need to get a reference to the actor. This is done with the
help of the static method createAactorRef of the Spawn class.
In the sections below we will give some examples of how to invoke different types of actors in different ways.
To invoke an actor named like the one we defined in section Getting Started we could do as follows:
ActorRef joeActor = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "JoeActor"));
Request msg = Request.newBuilder()
.setLanguage("erlang")
.build();
Optional<Reply> maybeResponse = joeActor.invoke("setLanguage", msg, Reply.class);
Domain.Reply reply = maybeResponse.get();More detailed in complete main class:
package io.eigr.spawn.java.demo;
import io.eigr.spawn.api.Spawn;
import io.eigr.spawn.api.Spawn.SpawnSystem;
import io.eigr.spawn.api.ActorIdentity;
import io.eigr.spawn.api.ActorRef;
import io.eigr.spawn.api.TransportOpts;
import io.eigr.spawn.api.exceptions.SpawnException;
import io.eigr.spawn.java.demo.domain.Domain;
public class App {
public static void main(String[] args) throws SpawnException {
Spawn spawnSystem = new SpawnSystem()
.create("spawn-system")
.withActor(Joe.class)
.withTransportOptions(
TransportOpts.builder()
.port(8091)
.proxyPort(9003)
.build()
)
.build();
spawnSystem.start();
ActorRef joeActor = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "JoeActor"));
Domain.Request msg = Domain.Request.newBuilder()
.setLanguage("erlang")
.build();
joeActor.invoke("setLanguage", msg, Domain.Reply.class)
.ifPresent(response -> log.info("Response is: {}", response));
}
}Unnamed actors are equally simple to invoke. All that is needed is to inform the parent parameter which refers to the
name given to the actor that defines the ActorRef template.
To better exemplify, let's first show the Actor's definition code and later how we would call this actor with a concrete name at runtime:
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<State> {
@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<State> 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:
ActorRef mike = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "MikeInstanceActor", "MikeActor"));
Request msg = Request.newBuilder()
.setLanguage("erlang")
.build();
Optional<Reply> maybeResponse = mike.invoke("setLanguage", msg, Reply.class);
Domain.Reply reply = maybeResponse.get();The important part of the code above is the following snippet:
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 "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.
Basically Spawn can perform actor functions in two ways. Synchronously, where the callee waits for a response, or asynchronously, where the callee doesn't care about the return value of the call. In this context we should not confuse Spawn's asynchronous way with Java's concept of async like Promises because async for Spawn is just a fire-and-forget call.
Therefore, to call an actor's function asynchronously, just use the invokeAsync method:
mike.invokeAsync("setLanguage", msg);It is possible to change the request waiting timeout using the invocation options as below:
package io.eigr.spawn.java.demo;
// omitted for brevity
import java.util.Optional;
public class App {
public static void main(String[] args) {
Spawn spawnSystem = new Spawn.SpawnSystem()
.create("spawn-system")
.withActor(Joe.class)
.build();
spawnSystem.start();
ActorRef joeActor = spawnSystem.createActorRef(ActorIdentity.of("spawn-system", "JoeActor"));
Request msg = Request.newBuilder()
.setLanguage("erlang")
.build();
InvocationOpts opts = InvocationOpts.builder()
.timeoutSeconds(Duration.ofSeconds(30))
.build();
Optional<Reply> maybeResponse = joeActor.invoke("setLanguage", msg, Reply.class, opts);
}
}See Getting Started section from the main Spawn repository for more details on how to deploy a Spawn application.
See Getting Started section from the main Spawn repository for more details on how to define an ActorSystem.
See Getting Started section from the main Spawn repository for more details on how to define an ActorHost.
TODO
According to Wikipedia Actor Model is:
"A mathematical model of concurrent computation that treats actor as the universal primitive of concurrent computation. In response to a message it receives, an actor can: make local decisions, create more actors, send more messages, and determine how to respond to the next message received. Actors may modify their own private state, but can only affect each other indirectly through messaging (removing the need for lock-based synchronization).
The actor model originated in 1973. It has been used both as a framework for a theoretical understanding of computation and as the theoretical basis for several practical implementations of concurrent systems."
The Actor Model was proposed by Carl Hewitt, Peter Bishop, and Richard Steiger and is inspired by several characteristics of the physical world.
Although it emerged in the 70s of the last century, only in the previous two decades of our century has this model gained strength in the software engineering communities due to the massive amount of existing data and the performance and distribution requirements of the most current applications.
For more information about the Actor Model, see the following links:
https://en.wikipedia.org/wiki/Actor_model
https://codesync.global/media/almost-actors-comparing-pony-language-to-beam-languages-erlang-elixir/
https://doc.akka.io/docs/akka/current/general/actors.html
In the context of the Virtual Actor paradigm, actors possess the inherent ability to seamlessly retain their state. The underlying framework dynamically manages the allocation of actors to specific nodes. If a node happens to experience an outage, the framework automatically revives the affected actor on an alternate node. This process of revival maintains data integrity as actors are inherently designed to preserve their state. Interruptions to availability are minimized during this seamless transition, contingent on the actors correctly implementing their state preservation mechanisms.
The Virtual Actor model offers several merits:
-
Scalability: The system can effortlessly accommodate a higher number of actor instances by introducing additional nodes.
-
Availability: In case of a node failure, actors swiftly and nearly instantly regenerate on another node, all while safeguarding their state from loss.