Skip to content

Pods

An actor pod encapsulates a subdomain of an application: a group of actors with a common entry point. Pods are part of Actor4j - Core (io.actor4j.core.pods). They can be deployed and undeployed at runtime, replicated (primary and secondary replicas) and sharded [1]. In comparison to microservices, the subdomains run as pods within the same instance, so calls between them are messages instead of network calls (see Fig. 1).

Monoliths, microservices and Actor4j Cloud

Fig. 1: From monoliths and microservices to subdomains as actor pods (© 2022 IEEE, from [1])

Each ActorPod (see Fig. 2) contains the actors of the subdomain, derived from PodChildActor. The PodActor manages the pod. The HandlerPodActor accepts the requests of clients, forwards them to a PodChildActor if needed and sends the response back to the client. A pod is either a primary or a secondary replica [1].

Actor pod pattern

Fig. 2: Actor pod pattern (© 2022 IEEE, from [1])

The state of a PodChildActor can be saved via event sourcing or via caching. In 2022, caching was done with cache actors within the pod (Fig. 2). This is still possible, but today a shared local cache is recommended (hybrid approach). Persistence and caching are part of the actor4j libraries.

A pod is defined by a domain and its actors. The easiest way is DefaultActorPod: its HandlerPodActor forwards each request to the child actor registered under the domain as alias, and sends the answer back to the client. Only the child actor has to be provided (factory).

public class ExampleActorPod extends DefaultActorPod {
@Override
public ActorFactory factory(UUID groupId, PodContext context) {
return () -> new HelloActor(groupId, context);
}
@Override
public String domain() {
return "ExampleActorPod";
}
}
public class HelloActor extends PodChildActor {
public HelloActor(UUID groupId, PodContext context) {
super(groupId, context);
}
@Override
public void preStart() {
setAlias(context.domain()); // DefaultActorPod forwards to this alias
}
@Override
public void receive(ActorMessage<?> message) {
tell(String.format("Hello %s! [domain:%s, primaryReplica:%s]",
message.value(), context.domain(), context.primaryReplica()),
42, message.source(), message.interaction());
}
}
/* Full examples under https://github.com/relvaner/actor4j-core/tree/master/actor4j-core-tck */

For more control (e.g. several child actors, own routing), ActorPod is extended directly and create() returns the PodActor, e.g. a DefaultPodActor with an own HandlerPodActor.

The pod is deployed with a PodConfiguration (domain, class name, minimum and maximum number of replicas) and addressed via its domain as alias:

ActorSystem system = ActorSystem.create(ActorRuntime.factory(), ActorSystemConfig.builder().serverMode().build());
system.deployPods(
() -> new ExampleActorPod(),
new PodConfiguration("ExampleActorPod", ExampleActorPod.class.getName(), 1, 1));
ActorId client = system.addActor(() -> new Actor() {
@Override
public void receive(ActorMessage<?> message) {
System.out.println(message.valueAsString());
}
});
system.start();
system.sendViaAlias(ActorMessage.create("Test", 0, client, null), "ExampleActorPod");

A pod can also be deployed from a JAR file (deployPods(File jarFile, PodConfiguration podConfiguration)) and be removed with undeployPods(domain). The number of replicas can be changed at runtime with updateReplicaLimits(domain, minReplica, maxReplica). A horizontal pod autoscaler adjusts the replicas within these limits (horizontalPodAutoscalerEnabled in the configuration).

If the logic of a pod fits into one function, a FunctionPod is enough. The function returns the answer as Reply. Reply.pending() marks an answer that follows later, Reply.none() means no answer.

public class ExampleFunctionPod extends FunctionPod {
@Override
public PodFunction createFunction(ActorRef host, PodContext context) {
return new PodFunction(host, context) {
@Override
public Reply handle(ActorMessage<?> message, UUID interaction, boolean replyAllowed) {
return Reply.of(String.format("Hello %s! [domain:%s, primaryReplica:%s]",
message.value(), context.domain(), context.primaryReplica()), 42);
}
};
}
@Override
public String domain() {
return "ExampleFunctionPod";
}
}

Function pods with additional services (state store, streams, queries, I/O) and with scripts in JavaScript or Python are part of the actor4j libraries.

Pods can also be addressed from outside the actor system, e.g. by a REST interface of a server (ActorService). The request arrives as RemotePodMessage, the answer is passed back via the InternalServerCallback of the runtime configuration. For this, the remote variants are used. They handle local messages as before:

Local Remote
DefaultActorPod DefaultRemoteActorPod
DefaultShardActorPod DefaultRemoteShardActorPod
FunctionPod RemoteFunctionPod (with PodRemoteFunction)

ExampleActorPod from above becomes a remote pod by extending DefaultRemoteActorPod instead of DefaultActorPod. The child actor stays the same.

In the example, the client is reachable from outside with its global id (expose()). The InternalServerCallback gets the reply address of the request, the result and the tag, and delivers the answer. In a real server, this is done by the server interface (e.g. REST), here the answer is sent back into the actor system.

public class ExampleRemoteActorPod extends DefaultRemoteActorPod {
@Override
public ActorFactory factory(UUID groupId, PodContext context) {
return () -> new HelloActor(groupId, context);
}
@Override
public String domain() {
return "ExampleRemoteActorPod";
}
}
ActorService system = ActorService.create(ActorRuntime.factory());
system.deployPods(
() -> new ExampleRemoteActorPod(),
new PodConfiguration("ExampleRemoteActorPod", ExampleRemoteActorPod.class.getName(), 1, 1));
ActorId client = system.addActor(() -> new Actor() {
@Override
public void preStart() {
expose(); // reachable from outside with its global id
}
@Override
public void receive(ActorMessage<?> message) {
System.out.println(message.valueAsString());
}
});
// delivers the answer of a remote request (replyAddress, result, tag)
InternalServerCallback internalServerCallback = (replyAddress, result, tag) ->
system.sendAsServer(ActorMessage.create(result, tag, system.SYSTEM_ID(), GlobalId.of((String)replyAddress)));
((InternalActorSystem)system).setRuntimeConfig(ActorRuntimeConfig.builder()
.internalServerCallback(internalServerCallback)
.build());
system.start();
// remote request: payload, tag, alias of the pod, reply expected
RemotePodMessage request = new RemotePodMessage(
new RemotePodMessageDTO("Test", 0, "ExampleRemoteActorPod", true),
client.globalId().toString(), null);
system.sendViaAlias(ActorMessage.create(request, 0, system.SYSTEM_ID(), null), "ExampleRemoteActorPod");

For sharding, DefaultShardActorPod is used. In addition to the child actor, it determines the shard of a message (shardId(message, totalShardCount)). The number of shards is set in the PodConfiguration (shardCount).

public class ExampleShardActorPod extends DefaultShardActorPod {
@Override
public ActorFactory factory(UUID groupId, PodContext context) {
return () -> new HelloActor(groupId, context);
}
@Override
public String shardId(ActorMessage<?> message, int totalShardCount) {
int hashCode = String.valueOf(message.value()).hashCode();
return String.valueOf(Math.floorMod(hashCode, totalShardCount)); // shard ids 0..n-1
}
@Override
public String domain() {
return "ExampleShardActorPod";
}
}

As a demo, the services of Google’s microservices demo “Online Boutique” were implemented as actor pods (see Fig. 3) [1].

Actor pods of the demo application

Fig. 3: Actor pods of the demo application (cp. Google’s Online Boutique) (© 2022 IEEE, from [1])

[1] D. A. Bauer and J. Mäkiö, “Actor-Oriented Scalable Domain-Specific Cluster Architecture for Cloud-Applications,” IECON 2022 – 48th Annual Conference of the IEEE Industrial Electronics Society, Brussels, Belgium, 2022, pp. 1-6, https://doi.org/10.1109/IECON49645.2022.9968983