Using Apache Pekko actors and GPars actors with Groovy
Author: Paul King
Published: 2023-07-17 11:24PM (Last updated: 2024-09-28 04:59AM)
Apache Pekko is an Apache-licensed fork of the Akka project (based on Akka version 2.6.x) and provides a framework for building applications that are concurrent, distributed, resilient and elastic. Pekko provides high-level abstractions for concurrency based on actors, as well as additional libraries for persistence, streams, HTTP, and more. It provides Scala and Java APIs/DSLs for writing your applications. We’ll be using the latter. We’ll look at just one example of using Pekko actors.
By way of comparison, we’ll also be looking at GPars, a concurrency library for Java and Groovy with support for actors, agents, concurrent & parallel map/reduce, fork/join, asynchronous closures, dataflow, and more. A previous blog post looks at additional features of GPars and how to use it with virtual threads. Here, we’ll just look at the comparable actor features for our Pekko example.
The example
A common first example involving actors involves creating two actors where one actor sends a message to the second actor which sends a reply back to the first. We could certainly do that, but we’ll use a slightly more interesting example involving three actors. The example comes from the Pekko documentation and is illustrated in the following diagram (from the Pekko documentation):
The system consists of the following actors:
-
The
HelloWorldMain
actor creates the other two actors and sends an initial message to kick off our little system. The initial message goes to theHelloWorld
actor and gives theHelloWorldBot
as the reply address. -
The
HelloWorld
actor is listening forGreet
messages. When it receives one, it sends aGreeted
acknowledgement back to a reply address. -
The
HelloWorldBot
is like an echo chamber. It returns any message it receives. This would potentially be an infinite loop, however, the actor has a parameter to tell it the maximum number of times to echo the message before stopping.
A Pekko implementation in Groovy
This example uses Groovy 4.0.23 and Pekko 1.1.1. It was tested with JDK 11, 17, 21 and 23.
The Pekko documentation gives Java and Scala implementations. You should notice that the Groovy implementation is similar to the Java one but just a little shorter. The Groovy code is a little more complex than the equivalent Scala code. We could certainly use Groovy meta-programming to simplify the Groovy code in numerous ways but that is a topic for another day.
Here is the code for HelloWorld
:
class HelloWorld extends AbstractBehavior<Greet> {
static record Greet(String whom, ActorRef<Greeted> replyTo) {}
static record Greeted(String whom, ActorRef<Greet> from) {}
static Behavior<Greet> create() {
Behaviors.setup(HelloWorld::new)
}
private HelloWorld(ActorContext<Greet> context) {
super(context)
}
@Override
Receive<Greet> createReceive() {
newReceiveBuilder().onMessage(Greet.class, this::onGreet).build()
}
private Behavior<Greet> onGreet(Greet command) {
context.log.info "Hello $command.whom!"
command.replyTo.tell(new Greeted(command.whom, context.self))
this
}
}
First we define Greet
and Greeter
records to have strong typing for the messages in our system.
We then define the details of our actor. A fair bit of this is boilerplate. The interesting part
is inside the onGreet
method. We log the message details before sending back the Greeted
acknowledgement.
The HelloWorldBot
is similar. You should notice some state variables which keep an
invocation counter and a maximum number of invocations before terminating:
class HelloWorldBot extends AbstractBehavior<HelloWorld.Greeted> {
static Behavior<HelloWorld.Greeted> create(int max) {
Behaviors.setup(context -> new HelloWorldBot(context, max))
}
private final int max
private int greetingCounter
private HelloWorldBot(ActorContext<HelloWorld.Greeted> context, int max) {
super(context)
this.max = max
}
@Override
Receive<HelloWorld.Greeted> createReceive() {
newReceiveBuilder().onMessage(HelloWorld.Greeted.class, this::onGreeted).build()
}
private Behavior<HelloWorld.Greeted> onGreeted(HelloWorld.Greeted message) {
greetingCounter++
context.log.info "Greeting $greetingCounter for $message.whom"
if (greetingCounter == max) {
return Behaviors.stopped()
} else {
message.from.tell(new HelloWorld.Greet(message.whom, context.self))
return this
}
}
}
The interesting logic is in the onGreeted
method. We increment the counter and either stop,
if we have reached the maximum count threshold, or echo back the message contents to the sender.
Let’s have a look at the final actor:
class HelloWorldMain extends AbstractBehavior<HelloWorldMain.SayHello> {
static record SayHello(String name) { }
static Behavior<SayHello> create() {
Behaviors.setup(HelloWorldMain::new)
}
private final ActorRef<HelloWorld.Greet> greeter
private HelloWorldMain(ActorContext<SayHello> context) {
super(context)
greeter = context.spawn(HelloWorld.create(), 'greeter')
}
@Override
Receive<SayHello> createReceive() {
newReceiveBuilder().onMessage(SayHello.class, this::onStart).build()
}
private Behavior<SayHello> onStart(SayHello command) {
var replyTo = context.spawn(HelloWorldBot.create(3), command.name)
greeter.tell(new HelloWorld.Greet(command.name, replyTo))
this
}
}
There is a SayHello
record, to act as a strongly typed incoming message.
The HelloWorldMain
actor creates the other actors.
It creates one HelloWorld
actor which is the greeter target of subsequent messages.
For each incoming SayHello
message, it creates a bot, then sends a message
to the greeter containing the SayHello
payload and telling it to reply to the bot.
Finally, we need to kick off our system. We create the HelloWorldMain
actor and
send it two messages:
var system = ActorSystem.create(HelloWorldMain.create(), 'hello')
system.tell(new HelloWorldMain.SayHello('World'))
system.tell(new HelloWorldMain.SayHello('Pekko'))
The log output from running the script will look similar to this:
[hello-pekko.actor.default-dispatcher-6] INFO pekko.HelloWorld - Hello World! [hello-pekko.actor.default-dispatcher-6] INFO pekko.HelloWorld - Hello Pekko! [hello-pekko.actor.default-dispatcher-3] INFO pekko.HelloWorldBot - Greeting 1 for Pekko [hello-pekko.actor.default-dispatcher-5] INFO pekko.HelloWorldBot - Greeting 1 for World [hello-pekko.actor.default-dispatcher-6] INFO pekko.HelloWorld - Hello Pekko! [hello-pekko.actor.default-dispatcher-6] INFO pekko.HelloWorld - Hello World! [hello-pekko.actor.default-dispatcher-5] INFO pekko.HelloWorldBot - Greeting 2 for Pekko [hello-pekko.actor.default-dispatcher-3] INFO pekko.HelloWorldBot - Greeting 2 for World [hello-pekko.actor.default-dispatcher-5] INFO pekko.HelloWorld - Hello Pekko! [hello-pekko.actor.default-dispatcher-5] INFO pekko.HelloWorld - Hello World! [hello-pekko.actor.default-dispatcher-3] INFO pekko.HelloWorldBot - Greeting 3 for Pekko [hello-pekko.actor.default-dispatcher-5] INFO pekko.HelloWorldBot - Greeting 3 for World [hello-pekko.actor.default-dispatcher-6] INFO org.apache.pekko.actor.CoordinatedShutdown - Running CoordinatedShutdown with reason [ActorSystemTerminateReason]
A GPars implementation in Groovy
This example uses Groovy 4.0.23 and GPars 1.2.1. It was tested with JDK 8, 11, 17, 21 and 23.
We’ll follow the same conventions for strongly typed messages in our GPars example. Here are our three message containers:
record Greet(String whom, Actor replyTo) { }
record Greeted(String whom, Actor from) {}
record SayHello(String name) { }
Now we’ll define our helloWorld
actor:
greeter = actor {
loop {
react { Greet command ->
println "Hello $command.whom!"
command.replyTo << new Greeted(command.whom, greeter)
}
}
}
Here, we are using GPars Groovy continuation-style DSL for defining actors.
The loop
indicates that the actor will loop continually.
When we receive the Greet
message, we log the details to stdout and
send the acknowledgement.
If we don’t want to use the DSL syntax, we can use the related classes directly.
Here we’ll define a HelloWorldBot
using this slightly more verbose style.
It shows adding the state variables we need for tracking the invocation count:
class HelloWorldBot extends DefaultActor {
int max
private int greetingCounter = 0
@Override
protected void act() {
loop {
react { Greeted message ->
greetingCounter++
println "Greeting $greetingCounter for $message.whom"
if (greetingCounter < max) message.from << new Greet(message.whom, this)
else terminate()
}
}
}
}
Our main actor is very simple. It is waiting for SayHello
messages, and when it receives one,
it sends the payload to the helloWorld greeter telling it to reply to a newly created bot.
var main = actor {
loop {
react { SayHello command ->
greeter << new Greet(command.name, new HelloWorldBot(max: 3).start())
}
}
}
Finally, we start the system going by sending some initial messages:
main << new SayHello('World')
main << new SayHello('GPars')
The output looks like this:
Hello World! Hello GPars! Greeting 1 for World Greeting 1 for GPars Hello World! Hello GPars! Greeting 2 for World Hello World! Greeting 2 for GPars Hello GPars! Greeting 3 for World Greeting 3 for GPars
Discussion
The GPars implementation is less verbose compared to the Pekko implementation but Pekko is known for providing additional type safety for actor messages and that is partly what we are seeing.
GPars supports a mixture of styles, some offering less verbosity at the expense of capturing some errors at runtime rather than compile-time. Such code can be useful when wanting very succinct code using Groovy’s dynamic nature. When using Groovy’s static nature or Java, you might consider using select parts of the GPars API.
For example, we can provide an alternative definition for HelloWorldBot
like this:
class HelloWorldBot extends StaticDispatchActor<Greeted> {
int max
private int greetingCounter = 0
@Override
void onMessage(Greeted message) {
greetingCounter++
println "Greeting $greetingCounter for $message.whom"
if (greetingCounter < max) message.from << new Greet(message.whom, this)
else terminate()
}
}
The StaticDispatchActor
dispatches the message solely based on the compile-time information.
This can be more efficient when dispatching based on message run-time type is not necessary.
We could also provide an alternative definition for Greet
as follows:
record Greet(String whom, StaticDispatchActor<Greeted> replyTo) { }
With changes like these in place we can code a solution with additional message type safety when using Groovy’s static nature.
Conclusion
We have had a quick glimpse at using actors with Apache Pekko and GPars.
The sample code can be found here: