An actor is a concurrent unit of computation which guarantees serial processing of messages with no need for state synchronization and coordination. This guarantee of actors mainly comes from the way actors communicate with each other, each actor send asynchronous messages to other actors and each actor only reads messages from its mailbox.
- Java 21+ (with --enable-preview flag)
There are two styles of creating actors, one is the Object-oriented style and Functional style
Caution: When dealing with state of actors, be sure not to allow mutable objects to escape to external actors, this could cause unwanted state mutations, this is mainly due to the nature of referential objects in Java.
- Object-oriented style
This is the default way of creating actors, we extend from the Actor<M>
class and we implement the receive
method
where we add logic to handle the message and state mutations.
public sealed interface GreetingMessage permits HelloMessage, ByeMessage, GetHelloCount, Shutdown {
}
public record HelloMessage() implements GreetingMessage {
}
public record ByeMessage() implements GreetingMessage {
}
public record Shutdown() implements GreetingMessage {
}
public record GetHelloCount(Pid replyTo) implements GreetingMessage {
}
public class GreetingActor extends Actor<GreetingMessage> {
private int helloCount;
public GreetingActor(ActorSystem system, String actorId) {
super(system, actorId);
this.helloCount = 0;
}
@Override
public void receive(GreetingMessage message) {
switch (message) {
case HelloMessage ignored -> {
// Updating state of the actor
helloCount++;
}
case GetHelloCount ghc -> {
// Replying back to calling actor
ghc.replyTo().tell(new HelloCount(helloCount));
}
case ByeMessage ignored -> {
// Sending a message to self
self().tell(new Shutdown());
}
case Shutdown ignored -> {
// Stopping actor after processing all messages
stop();
}
}
}
}
- Functional style actor
When creating a FunctionalActor
we need to know the State and Message that the actor is going to be using,
then we define call receiveMessage
on the FunctionalActor
to program the state changes and message handling logic.
sealed interface CounterProtocol {
record CountUp() implements CounterProtocol {
}
record GetCount(Pid replyTo) implements CounterProtocol {
}
}
public static void main(String[] args) {
var counterActor = new FunctionalActor<Integer, CounterProtocol>();
var counter = actorSystem.register(counterActor.receiveMessage((state, message) -> {
switch (message) {
case CounterProtocol.CountUp ignored -> {
return state + 1;
}
case CounterProtocol.GetCount gc -> gc.replyTo().tell(new HelloCount(i));
}
return state;
}, 0), "Counter-Actor");
var receiverActor = actorSystem.register(CountReceiver.class, "count-receiver-1");
counter.tell(new CounterProtocol.CountUp());
counter.tell(new CounterProtocol.CountUp());
counter.tell(new CounterProtocol.CountUp());
counter.tell(new CounterProtocol.CountUp());
counter.tell(new CounterProtocol.GetCount(receiverActor));
}
After creating the actor we have to use the actor system to spawn them and send messages.
class CountReceiver extends Actor<HelloCount> {
public CountReceiver(ActorSystem system, String actorId) {
super(system, actorId);
}
@Override
protected void receive(HelloCount helloCount) {
System.out.println("Count" + helloCount);
}
}
public static void main(String[] args) {
var actorSystem = new ActorSystem();
var pid1 = actorSystem.register(GreetingActor.class, "greeting-actor-1");
var receiverActor = actorSystem.register(CountReceiver.class, "count-receiver");
pid1.tell(new HelloMessage());
pid1.tell(new GetHelloCount(receiverActor)); // Count: 1
}
To run examples in the project, you can leverage the gradle task runner (--enable-preview flag is already enabled for gradle tasks)
./gradlew -PmainClass=examples.TimedCounter run
- Actor system and actor lifecycle
- Create Actor and Actor System
- Support message to self for actor
- Support hooks for start and shutdown of actor
- Stateful functional style actor
- Timed messages
- Actor metadata management with etcd
- Actor supervision hierarchy and fault tolerance
- Persistent state and messaging for actors
- Partitioned state and sharding strategy