Style guide
This is a style guide with recommendations of idioms and patterns for writing Pekko actors. Note that this guide does not cover the classic actor API.
As with all style guides, treat this as a list of rules to be broken. There are certainly times when alternative styles should be preferred over the ones given here.
Functional versus object-oriented style¶
There are two flavors of the Actor APIs.
- The functional programming style where you pass a function to a factory which then constructs a behavior, for stateful actors this means passing immutable state around as parameters and switching to a new behavior whenever you need to act on a changed state.
- The object-oriented style where a concrete class for the actor behavior is defined and mutable state is kept inside of it as fields.
An example of a counter actor implemented in the functional style:
source
import org.apache.pekko
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.ActorContext
import pekko.actor.typed.scaladsl.Behaviors
object Counter {
sealed trait Command
case object Increment extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
def apply(): Behavior[Command] =
counter(0)
private def counter(n: Int): Behavior[Command] =
Behaviors.receive { (context, message) =>
message match {
case Increment =>
val newValue = n + 1
context.log.debug("Incremented counter to [{}]", newValue)
counter(newValue)
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.same
}
}
}
sourceimport org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.actor.typed.SupervisorStrategy;
import org.apache.pekko.actor.typed.javadsl.ActorContext;
import org.apache.pekko.actor.typed.javadsl.Behaviors;
public class Counter {
public interface Command {}
public enum Increment implements Command {
INSTANCE
}
public static class GetValue implements Command {
public final ActorRef<Value> replyTo;
public GetValue(ActorRef<Value> replyTo) {
this.replyTo = replyTo;
}
}
public static class Value {
public final int value;
public Value(int value) {
this.value = value;
}
}
public static Behavior<Command> create() {
return Behaviors.setup(context -> counter(context, 0));
}
private static Behavior<Command> counter(final ActorContext<Command> context, final int n) {
return Behaviors.receive(Command.class)
.onMessage(Increment.class, notUsed -> onIncrement(context, n))
.onMessage(GetValue.class, command -> onGetValue(n, command))
.build();
}
private static Behavior<Command> onIncrement(ActorContext<Command> context, int n) {
int newValue = n + 1;
context.getLog().debug("Incremented counter to [{}]", newValue);
return counter(context, newValue);
}
private static Behavior<Command> onGetValue(int n, GetValue command) {
command.replyTo.tell(new Value(n));
return Behaviors.same();
}
}
Corresponding actor implemented in the object-oriented style:
source
import org.apache.pekko
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.ActorContext
import pekko.actor.typed.scaladsl.Behaviors
import org.apache.pekko.actor.typed.scaladsl.AbstractBehavior
import org.slf4j.Logger
object Counter {
sealed trait Command
case object Increment extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
def apply(): Behavior[Command] = {
Behaviors.setup(context => new Counter(context))
}
}
class Counter(context: ActorContext[Counter.Command]) extends AbstractBehavior[Counter.Command](context) {
import Counter._
private var n = 0
override def onMessage(msg: Command): Behavior[Counter.Command] = {
msg match {
case Increment =>
n += 1
context.log.debug("Incremented counter to [{}]", n)
this
case GetValue(replyTo) =>
replyTo ! Value(n)
this
}
}
}
sourceimport org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.actor.typed.SupervisorStrategy;
import org.apache.pekko.actor.typed.javadsl.ActorContext;
import org.apache.pekko.actor.typed.javadsl.Behaviors;
import org.apache.pekko.actor.typed.javadsl.AbstractBehavior;
import org.apache.pekko.actor.typed.javadsl.Receive;
public class Counter extends AbstractBehavior<Counter.Command> {
public interface Command {}
public enum Increment implements Command {
INSTANCE
}
public static class GetValue implements Command {
public final ActorRef<Value> replyTo;
public GetValue(ActorRef<Value> replyTo) {
this.replyTo = replyTo;
}
}
public static class Value {
public final int value;
public Value(int value) {
this.value = value;
}
}
public static Behavior<Command> create() {
return Behaviors.setup(Counter::new);
}
private int n;
private Counter(ActorContext<Command> context) {
super(context);
}
@Override
public Receive<Command> createReceive() {
return newReceiveBuilder()
.onMessage(Increment.class, notUsed -> onIncrement())
.onMessage(GetValue.class, this::onGetValue)
.build();
}
private Behavior<Command> onIncrement() {
n++;
getContext().getLog().debug("Incremented counter to [{}]", n);
return this;
}
private Behavior<Command> onGetValue(GetValue command) {
command.replyTo.tell(new Value(n));
return this;
}
}
Some similarities to note:
- Messages are defined in the same way.
- Both have an
apply
factory method in the companion object to create the initial behavior, i.e. from the outside they are used in the same way. - Pattern matching and handling of the messages are done in the same way.
- The
ActorContext
API is the same.
A few differences to note:
- There is no class in the functional style, but that is not strictly a requirement and sometimes it’s convenient to use a class also with the functional style to reduce number of parameters in the methods.
- Mutable state, such as the
var n
is typically used in the object-oriented style. - In the functional style the state is is updated by returning a new behavior that holds the new immutable state, the
n: Int
parameter of thecounter
method. - The object-oriented style must use a new instance of the initial
Behavior
for each spawned actor instance, since the state inAbstractBehavior
instance must not be shared between actor instances. This is “hidden” in the functional style since the immutable state is captured by the function. - In the object-oriented style one can return
this
to stay with the same behavior for next message. In the functional style there is nothis
soBehaviors.same
is used instead. - The
ActorContext
is accessed in different ways. In the object-oriented style it’s retrieved fromBehaviors.setup
and kept as an instance field, while in the functional style it’s passed in alongside the message. That said,Behaviors.setup
is often used in the functional style as well, and then often together withBehaviors.receiveMessage
that doesn’t pass in the context with the message.
Which style you choose to use is a matter of taste and both styles can be mixed depending on which is best for a specific actor. An actor can switch between behaviors implemented in different styles. For example, it may have an initial behavior that is only stashing messages until some initial query has been completed and then switching over to its main active behavior that is maintaining some mutable state. Such initial behavior is nice in the functional style and the active behavior may be better with the object-oriented style.
We would recommend using the tool that is best for the job. The APIs are similar in many ways to make it easy to learn both. You may of course also decide to just stick to one style for consistency and familiarity reasons.
When developing in Scala the functional style will probably be the choice for many.
Some reasons why you may want to use the functional style:
- You are familiar with a functional approach of structuring the code. Note that this API is still not using any advanced functional programming or type theory constructs.
- The state is immutable and can be passed to “next” behavior.
- The
Behavior
is stateless. - The actor lifecycle has several different phases that can be represented by switching between different behaviors, like a finite state machine. This is also supported with the object-oriented style, but it’s typically nicer with the functional style.
- It’s less risk of accessing mutable state in the actor from other threads, like
Future
or Streams callbacks.
Some reasons why you may want to use the object-oriented style:
- You are more familiar with an object-oriented style of structuring the code with methods in a class rather than functions.
- Some state is not immutable.
- It could be more familiar and easier to upgrade existing classic actors to this style.
- Mutable state can sometimes have better performance, e.g. mutable collections and avoiding allocating new instance for next behavior (be sure to benchmark if this is your motivation).
Passing around too many parameters¶
One thing you will quickly run into when using the functional style is that you need to pass around many parameters.
Let’s add name
parameter and timers to the previous Counter
example. A first approach would be to just add those as separate parameters:
source
// this is an anti-example, better solutions exists
object Counter {
sealed trait Command
case object Increment extends Command
final case class IncrementRepeatedly(interval: FiniteDuration) extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
def apply(name: String): Behavior[Command] =
Behaviors.withTimers { timers =>
counter(name, timers, 0)
}
private def counter(name: String, timers: TimerScheduler[Command], n: Int): Behavior[Command] =
Behaviors.receive { (context, message) =>
message match {
case IncrementRepeatedly(interval) =>
context.log.debug(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
name,
interval.toString,
n.toString)
timers.startTimerWithFixedDelay(Increment, interval)
Behaviors.same
case Increment =>
val newValue = n + 1
context.log.debug2("[{}] Incremented counter to [{}]", name, newValue)
counter(name, timers, newValue)
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.same
}
}
}
source
// this is an anti-example, better solutions exists
public class Counter {
public interface Command {}
public static class IncrementRepeatedly implements Command {
public final Duration interval;
public IncrementRepeatedly(Duration interval) {
this.interval = interval;
}
}
public enum Increment implements Command {
INSTANCE
}
public static class GetValue implements Command {
public final ActorRef<Value> replyTo;
public GetValue(ActorRef<Value> replyTo) {
this.replyTo = replyTo;
}
}
public static class Value {
public final int value;
public Value(int value) {
this.value = value;
}
}
public static Behavior<Command> create(String name) {
return Behaviors.setup(
context -> Behaviors.withTimers(timers -> counter(name, context, timers, 0)));
}
private static Behavior<Command> counter(
final String name,
final ActorContext<Command> context,
final TimerScheduler<Command> timers,
final int n) {
return Behaviors.receive(Command.class)
.onMessage(
IncrementRepeatedly.class,
command -> onIncrementRepeatedly(name, context, timers, n, command))
.onMessage(Increment.class, notUsed -> onIncrement(name, context, timers, n))
.onMessage(GetValue.class, command -> onGetValue(n, command))
.build();
}
private static Behavior<Command> onIncrementRepeatedly(
String name,
ActorContext<Command> context,
TimerScheduler<Command> timers,
int n,
IncrementRepeatedly command) {
context
.getLog()
.debug(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
name,
command.interval,
n);
timers.startTimerWithFixedDelay(Increment.INSTANCE, command.interval);
return Behaviors.same();
}
private static Behavior<Command> onIncrement(
String name, ActorContext<Command> context, TimerScheduler<Command> timers, int n) {
int newValue = n + 1;
context.getLog().debug("[{}] Incremented counter to [{}]", name, newValue);
return counter(name, context, timers, newValue);
}
private static Behavior<Command> onGetValue(int n, GetValue command) {
command.replyTo.tell(new Value(n));
return Behaviors.same();
}
}
Ouch, that doesn’t look good. More things may be needed, such as stashing or application specific “constructor” parameters. As you can imagine, that will be too much boilerplate.
As a first step we can place all these parameters in a class so that we at least only have to pass around one thing. Still good to have the “changing” state, the n: Int
here, as a separate parameter.
source
// this is better than previous example, but even better solution exists
object Counter {
sealed trait Command
case object Increment extends Command
final case class IncrementRepeatedly(interval: FiniteDuration) extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
private case class Setup(name: String, context: ActorContext[Command], timers: TimerScheduler[Command])
def apply(name: String): Behavior[Command] =
Behaviors.setup { context =>
Behaviors.withTimers { timers =>
counter(Setup(name, context, timers), 0)
}
}
private def counter(setup: Setup, n: Int): Behavior[Command] =
Behaviors.receiveMessage {
case IncrementRepeatedly(interval) =>
setup.context.log.debugN(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
setup.name,
interval,
n)
setup.timers.startTimerWithFixedDelay(Increment, interval)
Behaviors.same
case Increment =>
val newValue = n + 1
setup.context.log.debug2("[{}] Incremented counter to [{}]", setup.name, newValue)
counter(setup, newValue)
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.same
}
}
source
// this is better than previous example, but even better solution exists
public class Counter {
// messages omitted for brevity, same messages as above example
private static class Setup {
final String name;
final ActorContext<Command> context;
final TimerScheduler<Command> timers;
private Setup(String name, ActorContext<Command> context, TimerScheduler<Command> timers) {
this.name = name;
this.context = context;
this.timers = timers;
}
}
public static Behavior<Command> create(String name) {
return Behaviors.setup(
context ->
Behaviors.withTimers(timers -> counter(new Setup(name, context, timers), 0)));
}
private static Behavior<Command> counter(final Setup setup, final int n) {
return Behaviors.receive(Command.class)
.onMessage(
IncrementRepeatedly.class, command -> onIncrementRepeatedly(setup, n, command))
.onMessage(Increment.class, notUsed -> onIncrement(setup, n))
.onMessage(GetValue.class, command -> onGetValue(n, command))
.build();
}
private static Behavior<Command> onIncrementRepeatedly(
Setup setup, int n, IncrementRepeatedly command) {
setup
.context
.getLog()
.debug(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
setup.name,
command.interval,
n);
setup.timers.startTimerWithFixedDelay(Increment.INSTANCE, command.interval);
return Behaviors.same();
}
private static Behavior<Command> onIncrement(Setup setup, int n) {
int newValue = n + 1;
setup.context.getLog().debug("[{}] Incremented counter to [{}]", setup.name, newValue);
return counter(setup, newValue);
}
private static Behavior<Command> onGetValue(int n, GetValue command) {
command.replyTo.tell(new Value(n));
return Behaviors.same();
}
}
That’s better. Only one thing to carry around and easy to add more things to it without rewriting everything. Note that we also placed the ActorContext
in the Setup
class, and therefore switched from Behaviors.receive
to Behaviors.receiveMessage
since we already have access to the context
.
It’s still rather annoying to have to pass the same thing around everywhere.
We can do better by introducing an enclosing class, even though it’s still using the functional style. The “constructor” parameters can be immutable instance fields and can be accessed from member methods.
source
// this is better than previous examples
object Counter {
sealed trait Command
case object Increment extends Command
final case class IncrementRepeatedly(interval: FiniteDuration) extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
def apply(name: String): Behavior[Command] =
Behaviors.setup { context =>
Behaviors.withTimers { timers =>
new Counter(name, context, timers).counter(0)
}
}
}
class Counter private (
name: String,
context: ActorContext[Counter.Command],
timers: TimerScheduler[Counter.Command]) {
import Counter._
private def counter(n: Int): Behavior[Command] =
Behaviors.receiveMessage {
case IncrementRepeatedly(interval) =>
context.log.debugN(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
name,
interval,
n)
timers.startTimerWithFixedDelay(Increment, interval)
Behaviors.same
case Increment =>
val newValue = n + 1
context.log.debug2("[{}] Incremented counter to [{}]", name, newValue)
counter(newValue)
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.same
}
}
source
// this is better than previous examples
public class Counter {
// messages omitted for brevity, same messages as above example
public static Behavior<Command> create(String name) {
return Behaviors.setup(
context ->
Behaviors.withTimers(timers -> new Counter(name, context, timers).counter(0)));
}
private final String name;
private final ActorContext<Command> context;
private final TimerScheduler<Command> timers;
private Counter(String name, ActorContext<Command> context, TimerScheduler<Command> timers) {
this.name = name;
this.context = context;
this.timers = timers;
}
private Behavior<Command> counter(final int n) {
return Behaviors.receive(Command.class)
.onMessage(IncrementRepeatedly.class, command -> onIncrementRepeatedly(n, command))
.onMessage(Increment.class, notUsed -> onIncrement(n))
.onMessage(GetValue.class, command -> onGetValue(n, command))
.build();
}
private Behavior<Command> onIncrementRepeatedly(int n, IncrementRepeatedly command) {
context
.getLog()
.debug(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
name,
command.interval,
n);
timers.startTimerWithFixedDelay(Increment.INSTANCE, command.interval);
return Behaviors.same();
}
private Behavior<Command> onIncrement(int n) {
int newValue = n + 1;
context.getLog().debug("[{}] Incremented counter to [{}]", name, newValue);
return counter(newValue);
}
private Behavior<Command> onGetValue(int n, GetValue command) {
command.replyTo.tell(new Value(n));
return Behaviors.same();
}
}
That’s nice. One thing to be cautious with here is that it’s important that you create a new instance for each spawned actor, since those parameters must not be shared between different actor instances. That comes natural when creating the instance from Behaviors.setup
as in the above example. Having a apply
factory method in the companion object and making the constructor private is recommended.
This can also be useful when testing the behavior by creating a test subclass that overrides certain methods in the class. The test would create the instance without the apply
factory method . Then you need to relax the visibility constraints of the constructor and methods.
It’s not recommended to place mutable state and var
members in the enclosing class. It would be correct from an actor thread-safety perspective as long as the same instance of the enclosing class is not shared between different actor instances, but if that is what you need you should rather use the object-oriented style with the AbstractBehavior
class.
Similar can be achieved without an enclosing class by placing the def counter
inside the Behaviors.setup
block. That works fine, but for more complex behaviors it can be better to structure the methods in a class. For completeness, here is how it would look like:
source
// this works, but previous example is better for structuring more complex behaviors
object Counter {
sealed trait Command
case object Increment extends Command
final case class IncrementRepeatedly(interval: FiniteDuration) extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
def apply(name: String): Behavior[Command] =
Behaviors.setup { context =>
Behaviors.withTimers { timers =>
def counter(n: Int): Behavior[Command] =
Behaviors.receiveMessage {
case IncrementRepeatedly(interval) =>
context.log.debugN(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
name,
interval,
n)
timers.startTimerWithFixedDelay(Increment, interval)
Behaviors.same
case Increment =>
val newValue = n + 1
context.log.debug2("[{}] Incremented counter to [{}]", name, newValue)
counter(newValue)
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.same
}
counter(0)
}
}
}
Behavior factory method¶
The initial behavior should be created via a factory method in the companion object . Thereby the usage of the behavior doesn’t change when the implementation is changed, for example if changing between object-oriented and function style.
The factory method is a good place for retrieving resources like Behaviors.withTimers
, Behaviors.withStash
and ActorContext
with Behaviors.setup
.
When using the object-oriented style, AbstractBehavior
, a new instance should be created from a Behaviors.setup
block in this factory method even though the ActorContext
is not needed. This is important because a new instance should be created when restart supervision is used. Typically, the ActorContext
is needed anyway.
The naming convention for the factory method is apply
(when using Scala) . Consistent naming makes it easier for readers of the code to find the “starting point” of the behavior.
In the functional style the factory could even have been defined as a val
if all state is immutable and captured by the function, but since most behaviors need some initialization parameters it is preferred to consistently use a method (def
) for the factory.
Example:
sourceobject CountDown {
sealed trait Command
case object Down extends Command
// factory for the initial `Behavior`
def apply(countDownFrom: Int, notifyWhenZero: ActorRef[Done]): Behavior[Command] =
new CountDown(notifyWhenZero).counter(countDownFrom)
}
private class CountDown(notifyWhenZero: ActorRef[Done]) {
import CountDown._
private def counter(remaining: Int): Behavior[Command] = {
Behaviors.receiveMessage {
case Down =>
if (remaining == 1) {
notifyWhenZero.tell(Done)
Behaviors.stopped
} else
counter(remaining - 1)
}
}
}
sourcepublic class CountDown extends AbstractBehavior<CountDown.Command> {
public interface Command {}
public enum Down implements Command {
INSTANCE
}
// factory for the initial `Behavior`
public static Behavior<Command> create(int countDownFrom, ActorRef<Done> notifyWhenZero) {
return Behaviors.setup(context -> new CountDown(context, countDownFrom, notifyWhenZero));
}
private final ActorRef<Done> notifyWhenZero;
private int remaining;
private CountDown(
ActorContext<Command> context, int countDownFrom, ActorRef<Done> notifyWhenZero) {
super(context);
this.remaining = countDownFrom;
this.notifyWhenZero = notifyWhenZero;
}
@Override
public Receive<Command> createReceive() {
return newReceiveBuilder().onMessage(Down.class, notUsed -> onDown()).build();
}
private Behavior<Command> onDown() {
remaining--;
if (remaining == 0) {
notifyWhenZero.tell(Done.getInstance());
return Behaviors.stopped();
} else {
return this;
}
}
}
When spawning an actor from this initial behavior it looks like:
sourceval countDown = context.spawn(CountDown(100, doneRef), "countDown")
sourceActorRef<CountDown.Command> countDown =
context.spawn(CountDown.create(100, doneRef), "countDown");
Where to define messages¶
When sending or receiving actor messages they should be prefixed with the name of the actor/behavior that defines them to avoid ambiguities.
Such a style is preferred over using importing Down
and using countDown ! Down
. However, within the Behavior
that handle these messages the short names can be used.
Therefore it is not recommended to define messages as top-level classes.
For the majority of cases it’s good style to define the messages in the companion object together with the Behavior
.
sourceobject Counter {
sealed trait Command
case object Increment extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
}
sourcepublic class Counter extends AbstractBehavior<Counter.Command> {
public interface Command {}
public enum Increment implements Command {
INSTANCE
}
public static class GetValue implements Command {
public final ActorRef<Value> replyTo;
public GetValue(ActorRef<Value> replyTo) {
this.replyTo = replyTo;
}
}
public static class Value {
public final int value;
public Value(int value) {
this.value = value;
}
}
}
If several actors share the same message protocol, it’s recommended to define those messages in a separate object
for that protocol.
Here’s an example of a shared message protocol setup:
sourceobject CounterProtocol {
sealed trait Command
final case class Increment(delta: Int, replyTo: ActorRef[OperationResult]) extends Command
final case class Decrement(delta: Int, replyTo: ActorRef[OperationResult]) extends Command
sealed trait OperationResult
case object Confirmed extends OperationResult
final case class Rejected(reason: String) extends OperationResult
}
sourceinterface CounterProtocol {
interface Command {}
public static class Increment implements Command {
public final int delta;
private final ActorRef<OperationResult> replyTo;
public Increment(int delta, ActorRef<OperationResult> replyTo) {
this.delta = delta;
this.replyTo = replyTo;
}
}
public static class Decrement implements Command {
public final int delta;
private final ActorRef<OperationResult> replyTo;
public Decrement(int delta, ActorRef<OperationResult> replyTo) {
this.delta = delta;
this.replyTo = replyTo;
}
}
interface OperationResult {}
enum Confirmed implements OperationResult {
INSTANCE
}
public static class Rejected implements OperationResult {
public final String reason;
public Rejected(String reason) {
this.reason = reason;
}
}
}
Note that the response message hierarchy in this case could be completely avoided by using the API instead (see Generic Response Wrapper).
Public versus private messages¶
Often an actor has some messages that are only for its internal implementation and not part of the public message protocol, such as timer messages or wrapper messages for ask
or messageAdapter
.
Such messages should be declared private
so they can’t be accessed and sent from the outside of the actor. Note that they must still extend the public Command
trait .
Here is an example of using private
for an internal message:
sourceobject Counter {
sealed trait Command
case object Increment extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
// Tick is private so can't be sent from the outside
private case object Tick extends Command
def apply(name: String, tickInterval: FiniteDuration): Behavior[Command] =
Behaviors.setup { context =>
Behaviors.withTimers { timers =>
timers.startTimerWithFixedDelay(Tick, tickInterval)
new Counter(name, context).counter(0)
}
}
}
class Counter private (name: String, context: ActorContext[Counter.Command]) {
import Counter._
private def counter(n: Int): Behavior[Command] =
Behaviors.receiveMessage {
case Increment =>
val newValue = n + 1
context.log.debug2("[{}] Incremented counter to [{}]", name, newValue)
counter(newValue)
case Tick =>
val newValue = n + 1
context.log.debug2("[{}] Incremented counter by background tick to [{}]", name, newValue)
counter(newValue)
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.same
}
}
sourcepublic class Counter extends AbstractBehavior<Counter.Command> {
public interface Command {}
public enum Increment implements Command {
INSTANCE
}
public static class GetValue implements Command {
public final ActorRef<Value> replyTo;
public GetValue(ActorRef<Value> replyTo) {
this.replyTo = replyTo;
}
}
public static class Value {
public final int value;
public Value(int value) {
this.value = value;
}
}
// Tick is private so can't be sent from the outside
private enum Tick implements Command {
INSTANCE
}
public static Behavior<Command> create(String name, Duration tickInterval) {
return Behaviors.setup(
context ->
Behaviors.withTimers(
timers -> {
timers.startTimerWithFixedDelay(Tick.INSTANCE, tickInterval);
return new Counter(name, context);
}));
}
private final String name;
private int count;
private Counter(String name, ActorContext<Command> context) {
super(context);
this.name = name;
}
@Override
public Receive<Command> createReceive() {
return newReceiveBuilder()
.onMessage(Increment.class, notUsed -> onIncrement())
.onMessage(Tick.class, notUsed -> onTick())
.onMessage(GetValue.class, this::onGetValue)
.build();
}
private Behavior<Command> onIncrement() {
count++;
getContext().getLog().debug("[{}] Incremented counter to [{}]", name, count);
return this;
}
private Behavior<Command> onTick() {
count++;
getContext()
.getLog()
.debug("[{}] Incremented counter by background tick to [{}]", name, count);
return this;
}
private Behavior<Command> onGetValue(GetValue command) {
command.replyTo.tell(new Value(count));
return this;
}
}
An alternative approach is using a type hierarchy and narrow
to have a super-type for the public messages as a distinct type from the super-type of all actor messages. The former approach is recommended but it is good to know this alternative as it can be useful when using shared message protocol classes as described in Where to define messages.
Here’s an example of using a type hierarchy to separate public and private messages:
source// above example is preferred, but this is possible and not wrong
object Counter {
// The type of all public and private messages the Counter actor handles
sealed trait Message
/** Counter's public message protocol type. */
sealed trait Command extends Message
case object Increment extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
// The type of the Counter actor's internal messages.
sealed trait PrivateCommand extends Message
// Tick is a private command so can't be sent to an ActorRef[Command]
case object Tick extends PrivateCommand
def apply(name: String, tickInterval: FiniteDuration): Behavior[Command] = {
Behaviors
.setup[Counter.Message] { context =>
Behaviors.withTimers { timers =>
timers.startTimerWithFixedDelay(Tick, tickInterval)
new Counter(name, context).counter(0)
}
}
.narrow // note narrow here
}
}
class Counter private (name: String, context: ActorContext[Counter.Message]) {
import Counter._
private def counter(n: Int): Behavior[Message] =
Behaviors.receiveMessage {
case Increment =>
val newValue = n + 1
context.log.debug2("[{}] Incremented counter to [{}]", name, newValue)
counter(newValue)
case Tick =>
val newValue = n + 1
context.log.debug2("[{}] Incremented counter by background tick to [{}]", name, newValue)
counter(newValue)
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.same
}
}
source// above example is preferred, but this is possible and not wrong
public class Counter extends AbstractBehavior<Counter.Message> {
// The type of all public and private messages the Counter actor handles
public interface Message {}
/** Counter's public message protocol type. */
public interface Command extends Message {}
public enum Increment implements Command {
INSTANCE
}
public static class GetValue implements Command {
public final ActorRef<Value> replyTo;
public GetValue(ActorRef<Value> replyTo) {
this.replyTo = replyTo;
}
}
public static class Value {
public final int value;
public Value(int value) {
this.value = value;
}
}
// The type of the Counter actor's internal messages.
interface PrivateCommand extends Message {}
// Tick is a private command so can't be sent to an ActorRef<Command>
enum Tick implements PrivateCommand {
INSTANCE
}
public static Behavior<Command> create(String name, Duration tickInterval) {
return Behaviors.setup(
(ActorContext<Message> context) ->
Behaviors.withTimers(
timers -> {
timers.startTimerWithFixedDelay(Tick.INSTANCE, tickInterval);
return new Counter(name, context);
}))
.narrow(); // note narrow here
}
private final String name;
private int count;
private Counter(String name, ActorContext<Message> context) {
super(context);
this.name = name;
}
@Override
public Receive<Message> createReceive() {
return newReceiveBuilder()
.onMessage(Increment.class, notUsed -> onIncrement())
.onMessage(Tick.class, notUsed -> onTick())
.onMessage(GetValue.class, this::onGetValue)
.build();
}
private Behavior<Message> onIncrement() {
count++;
getContext().getLog().debug("[{}] Incremented counter to [{}]", name, count);
return this;
}
private Behavior<Message> onTick() {
count++;
getContext()
.getLog()
.debug("[{}] Incremented counter by background tick to [{}]", name, count);
return this;
}
private Behavior<Message> onGetValue(GetValue command) {
command.replyTo.tell(new Value(count));
return this;
}
}
private
visibility can be defined for the PrivateCommand
messages but it’s not strictly needed since they can’t be sent to an ActorRef[Command] , which is the public message type of the actor.
Partial versus total Function¶
It’s recommended to use a sealed
trait as the super type of the commands (incoming messages) of an actor as the compiler will emit a warning if a message type is forgotten in the pattern match.
sourcesealed trait Command
case object Down extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
That is the main reason for Behaviors.receive
, Behaviors.receiveMessage
taking a Function
rather than a PartialFunction
.
The compiler warning if GetValue
is not handled would be:
[warn] ... Counter.scala:45:34: match may not be exhaustive.
[warn] It would fail on the following input: GetValue(_)
[warn] Behaviors.receiveMessage {
[warn] ^
Note that a MatchError
will be thrown at runtime if a message is not handled, so it’s important to pay attention to those. If a Behavior
should not handle certain messages you can still include them in the pattern match and return Behaviors.unhandled
.
sourceval zero: Behavior[Command] = {
Behaviors.receiveMessage {
case GetValue(replyTo) =>
replyTo ! Value(0)
Behaviors.same
case Down =>
Behaviors.unhandled
}
}
It’s recommended to use the sealed
trait and total functions with exhaustiveness check to detect mistakes of forgetting to handle some messages. Sometimes, that can be inconvenient and then you can use a PartialFunction
with Behaviors.receivePartial
or Behaviors.receiveMessagePartial
sourceval zero: Behavior[Command] = {
Behaviors.receiveMessagePartial {
case GetValue(replyTo) =>
replyTo ! Value(0)
Behaviors.same
}
}
How to compose Partial Functions¶
Following up from previous section, there are times when one might want to combine different PartialFunction
s into one Behavior
.
A good use case for composing two or more PartialFunction
s is when there is a bit of behavior that repeats across different states of the Actor. Below, you can find a simplified example for this use case.
The Command definition is still highly recommended be kept within a sealed
Trait:
sourcesealed trait Command
case object Down extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
In this particular case, the Behavior that is repeating over is the one in charge to handle the GetValue
Command, as it behaves the same regardless of the Actor’s internal state. Instead of defining the specific handlers as a Behavior
, we can define them as a PartialFunction
:
sourcedef getHandler(value: Int): PartialFunction[Command, Behavior[Command]] = {
case GetValue(replyTo) =>
replyTo ! Value(value)
Behaviors.same
}
def setHandlerNotZero(value: Int): PartialFunction[Command, Behavior[Command]] = {
case Down =>
if (value == 1)
zero
else
nonZero(value - 1)
}
def setHandlerZero(log: Logger): PartialFunction[Command, Behavior[Command]] = {
case Down =>
log.error("Counter is already at zero!")
Behaviors.same
}
Finally, we can go on defining the two different behaviors for this specific actor. For each Behavior
we would go and concatenate all needed PartialFunction
instances with orElse
to finally apply the command to the resulting one:
sourceval zero: Behavior[Command] = Behaviors.setup { context =>
Behaviors.receiveMessagePartial(getHandler(0).orElse(setHandlerZero(context.log)))
}
def nonZero(capacity: Int): Behavior[Command] =
Behaviors.receiveMessagePartial(getHandler(capacity).orElse(setHandlerNotZero(capacity)))
// Default Initial Behavior for this actor
def apply(initialCapacity: Int): Behavior[Command] = nonZero(initialCapacity)
Even though in this particular example we could use receiveMessage
as we cover all cases, we use receiveMessagePartial
instead to cover potential future unhandled message cases.
ask versus ?¶
When using the AskPattern
it’s recommended to use the ask
method rather than the infix ?
operator, like so:
sourceimport org.apache.pekko
import pekko.actor.typed.scaladsl.AskPattern._
import pekko.util.Timeout
implicit val timeout: Timeout = Timeout(3.seconds)
val counter: ActorRef[Command] = ???
val result: Future[OperationResult] = counter.ask(replyTo => Increment(delta = 2, replyTo))
You may also use the more terse placeholder syntax _
instead of replyTo
:
sourceval result2: Future[OperationResult] = counter.ask(Increment(delta = 2, _))
However, using the infix operator ?
with the placeholder syntax _
, like is done in the following example, won’t typecheck because of the binding scope rules for wildcard parameters:
source// doesn't compile
val result3: Future[OperationResult] = counter ? Increment(delta = 2, _)
Adding the necessary parentheses (as shown below) makes it typecheck, but, subjectively, it’s rather ugly so the recommendation is to use ask
.
sourceval result3: Future[OperationResult] = counter ? (Increment(delta = 2, _))
Note that AskPattern
is only intended for request-response interaction from outside an actor. If the requester is inside an actor, prefer ActorContext.ask
as it provides better thread-safety by not requiring the use of a Future
inside the actor.
Nesting setup¶
When an actor behavior needs more than one of setup
, withTimers
and withStash
the methods can be nested to access the needed dependencies:
sourcedef apply(): Behavior[Command] =
Behaviors.setup[Command](context =>
Behaviors.withStash(100)(stash =>
Behaviors.withTimers { timers =>
context.log.debug("Starting up")
// behavior using context, stash and timers ...
}))
sourcepublic static Behavior<Command> apply() {
return Behaviors.setup(
context ->
Behaviors.withStash(
100,
stash ->
Behaviors.withTimers(
timers -> {
context.getLog().debug("Starting up");
// behavior using context, stash and timers ...
})));
}
The order of the nesting does not change the behavior as long as there is no additional logic in any other function than the innermost one. It can be nice to default to put setup
outermost as that is the least likely block that will be removed if the actor logic changes.
Note that adding supervise
to the mix is different as it will restart the behavior it wraps, but not the behavior around itself:
sourcedef apply(): Behavior[Command] =
Behaviors.setup { context =>
// only run on initial actor start, not on crash-restart
context.log.info("Starting")
Behaviors
.supervise(Behaviors.withStash[Command](100) { stash =>
// every time the actor crashes and restarts a new stash is created (previous stash is lost)
context.log.debug("Starting up with stash")
// Behaviors.receiveMessage { ... }
})
.onFailure[RuntimeException](SupervisorStrategy.restart)
}
sourcepublic static Behavior<Command> create() {
return Behaviors.setup(
context -> {
// only run on initial actor start, not on crash-restart
context.getLog().info("Starting");
return Behaviors.<Command>supervise(
Behaviors.withStash(
100,
stash -> {
// every time the actor crashes and restarts a new stash is created
// (previous stash is lost)
context.getLog().debug("Starting up with stash");
// Behaviors.receiveMessage { ... }
}))
.onFailure(RuntimeException.class, SupervisorStrategy.restart());
});
}
Additional naming conventions¶
Some naming conventions have already been mentioned in the context of other recommendations, but here is a list of additional conventions:
-
replyTo
is the typical name for theActorRef[Reply]
parameter in messages to which a reply or acknowledgement should be sent. -
Incoming messages to an actor are typically called commands, and therefore the super type of all messages that an actor can handle is typically
sealed trait Command
. -
Use past tense for the events persisted by an
EventSourcedBehavior
since those represent facts that have happened, for exampleIncremented
.