Type-safe actor model for Java
Concurrent programming has always been tricky. Accessing a shared state from multiple threads is a central problem, where hard-to-catch errors may easily appear. Java has all the instruments for safe and flawless concurrency, but the compiler would deliberately let developers write dangerous code. There is a strong need for higher-level frameworks ensuring safe concurrent programming.
Actor model
The actor model is one of the approaches to safe concurrency. Actors are objects (class instances in Java sense) that may have mutable state and normally follow the standard rules:
- Everything is an actor. Actors communicate with each other exclusively by sending asynchronous messages. There is no shared state, public static variables, etc. Only the actor may change its state.
- Messages sent to an actor are processed sequentially: although the message handlers may the called in different threads, the framework guarantees that the actor’s state changes are safe and visible to all the subsequent message handlers.
What’s wrong with the existing actor frameworks
However, when I looked into the popular Java actor frameworks, I had a strange feeling. I tried akka, actor4j, kilim, quasar, reactors.io, orbit, offbynull/actors, edescourtis/actor, vlingo-actors, pcd-actors and all of them looked same… wrong to me.
Let’s look into a simple actor example for akka — one of the most popular actor frameworks for JVM:
public class MyActor extends AbstractActor {
public Receive createReceive() {
return receiveBuilder().match(SomeMessage.class, msg -> {
doSomeCommandProcessing(msg);
...
}).match(AnotherMessage.class, anotherMessage -> {
doAnotherMessageProcessing(anotherMessage);
...
}).build();
}
}
What in this Hello World example makes me feel odd in 2019?
- This API is not type-safe. The handler code in the actor’s implementation checks the incoming message type (a sort of instanceof in there) and dispatches the message properly. But I can send any message to any actor and there’s no compile-time check to prevent this.
- You need to extend from an abstract class. I hate extending application classes from framework classes. It produces an ugly mix of business logic and library and brings me back to the old times with EJB beans. Moreover, this won’t allow me to extend from my base class if I want to.
- You have to create a class for every type of message. In most cases, this would be a classic immutable DTO, just fields+constructors+getters, all those annoying boilerplate lines. Not a big deal though, and it should get much better with JDK14 record classes, but as I’ll show later, this is not necessary.
It’s interesting that for most actors frameworks that I looked into, the APIs had the same issues (but please see below for some exceptions).
Better message calls
“A message is a Java object” remains the cornerstone of the traditional actor frameworks. Let’s reject this concept. Java has a better way of sending messages: calling class methods. (Note that in Smalltalk sending a message to an object and calling an object’s method are equivalent notions). The apparent benefits are:
- the protocol is statically defined: method signature defines what methods (messages) an actor class supports and what type of message (parameters) it accepts;
- no need to create a DTO class for a message type, multiple message parameters are naturally implemented by multiple method parameters.
Using this approach we might target issues 1 and 3 — excellent!
I could find 2 Java actor frameworks using this approach:
Akka Typed Actors use JDK proxying to wrap an object and return a safe interface that converts normal Java method calls to asynchronous. Having an interface Squarer
and its implementation SquarerImpl
, you can get a “safe” handle:
Squarer safeSquarer = TypedActor.get(system)
.typedActorOf(new TypedProps<SquarerImpl>(Squarer.class, SquarerImpl.class));
Now, calls to void returning methods on safeSquarer
will result in tell async actor calls, calls to value returning methods will block until the method returns in its thread (synchronous calls). Truly async ask method calling is possible when a method returns a Future
:
interface Squarer {
void setParam(int param); // async tell
double square(double val); // sync ask (blocking call)
Future<Double> squareF(double val); // async ask
}
Jumi Actors have a similar approach, but it only supports tell messages:
ActorRef<Squarer> safeSquarer = actorThread.bindActor(Squarer.class,
SquarerImpl.class);safeSquarer.tell().setParam(param);
However, both libs are not great with asynchronous ask. Typed Actors’ requirement for returning a Future is not elegant, Jumi does not support asking at all.
Building an even better actor framework
We can implement both tell and ask in a very elegant way by wrapping the method call in a high-order function rather than proxying an interface:
IActorRef<Squarer> safeSquarer = system.actorOf(Squarer.class);safeSquarer.tell(squarer -> squarer.setParam(param)); //async tellsafeSquarer.ask(squarer -> squarer.square(5),
result -> System.out.println(result)); //async ask v1CompletableFuture<Double> safeSquarer.ask(squarer ->
squarer.square(5)); //async ask v2
Benefits of this API:
- Elegant and safe implementation of both async tell and ask. All the three above mentioned issues are resolved.
- No need to have a separate interface and implementation of the actor.
Squarer
in the above example can be an interface or a concrete class. - Now there are no limiting restrictions for the actor class at all. No need to extend a framework abstract class, no need to return a special type. Any class can now be an actor class, even a third-party class (wrapping a non-thread safe library into a single-thread actor and use it from multiple threads is now super-easy!)
I implemented this approach in a lightweight library called actr: https://github.com/zakgof/actr.
It doesn’t have advanced features like addressing actors by path or distributed actors support, but being lightweight it demonstrated nice performance comparing to akka on some simple benchmarks: https://github.com/zakgof/akka-actr-benchmark.
actr is packed with some simple schedulers (and an ability to provide your own one when needed). It also supports virtual threads from JDK14 EA — you are welcomed to try.