Using Apache Pekko actors and GPars actors with Groovy

Author: Paul King
Published: 2023-07-17 11:24PM (Last updated: 2023-07-26 03:02PM)


pekko logo Apache Pekko is a project undergoing incubation at the Apache Software Foundation. It 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.

gpars 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):

actors in our system - from 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 the HelloWorld actor and gives the HelloWorldBot as the reply address.

  • The HelloWorld actor is listening for Greet messages. When it receives one, it sends a Greeted 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.13 and Pekko 1.0.1. It was tested with JDK 11 and 17.

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<HelloWorld.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-3] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Hello World!
[hello-pekko.actor.default-dispatcher-3] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Hello Pekko!
[hello-pekko.actor.default-dispatcher-5] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Greeting 1 for World
[hello-pekko.actor.default-dispatcher-3] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Greeting 1 for Pekko
[hello-pekko.actor.default-dispatcher-3] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Hello World!
[hello-pekko.actor.default-dispatcher-3] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Hello Pekko!
[hello-pekko.actor.default-dispatcher-5] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Greeting 2 for World
[hello-pekko.actor.default-dispatcher-3] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Hello World!
[hello-pekko.actor.default-dispatcher-3] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Greeting 3 for World
[hello-pekko.actor.default-dispatcher-6] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Greeting 2 for Pekko
[hello-pekko.actor.default-dispatcher-6] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Hello Pekko!
[hello-pekko.actor.default-dispatcher-6] INFO org.codehaus.groovy.vmplugin.v8.IndyInterface - Greeting 3 for Pekko
[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.13 and GPars 1.2.1. It was tested with JDK 8, 11 and 17.

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:

Update history

17/Jul/2023: Initial version.
18/Jul/2023: Add discussion about type-safe messages.
26/Jul/2023: Update to Pekko 1.0.1.