Learning Pekko Typed from Classic
Pekko Classic is the original Actor APIs, which have been improved by more type safe and guided Actor APIs, known as Pekko Typed.
If you already know the classic Actor APIs and would like to learn Pekko Typed, this reference is a good resource. Many concepts are the same and this page tries to highlight differences and how to do certain things in Typed compared to classic.
You should probably learn some of the basics of Pekko Typed to see how it looks like before diving into the differences and details described here. A good starting point for that is the IoT example in the Getting Started Guide or the examples shown in Introduction to Actors.
Note that Pekko Classic is still fully supported and existing applications can continue to use the classic APIs. It is also possible to use Pekko Typed together with classic actors within the same ActorSystem, see coexistence. For new projects we recommend using the new Actor APIs.
Dependencies
The dependencies of the Typed modules are named by adding -typed
suffix of the corresponding classic module, with a few exceptions.
For example pekko-cluster-typed
:
- sbt
val PekkoVersion = "1.1.2" libraryDependencies += "org.apache.pekko" %% "pekko-cluster-typed" % PekkoVersion
- Maven
<properties> <scala.binary.version>2.13</scala.binary.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.apache.pekko</groupId> <artifactId>pekko-bom_${scala.binary.version}</artifactId> <version>1.1.2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.apache.pekko</groupId> <artifactId>pekko-cluster-typed_${scala.binary.version}</artifactId> </dependency> </dependencies>
- Gradle
def versions = [ ScalaBinary: "2.13" ] dependencies { implementation platform("org.apache.pekko:pekko-bom_${versions.ScalaBinary}:1.1.2") implementation "org.apache.pekko:pekko-cluster-typed_${versions.ScalaBinary}" }
Artifact names:
Classic | Typed |
---|---|
pekko-actor | pekko-actor-typed |
pekko-cluster | pekko-cluster-typed |
pekko-cluster-sharding | pekko-cluster-sharding-typed |
pekko-cluster-tools | pekko-cluster-typed |
pekko-distributed-data | pekko-cluster-typed |
pekko-persistence | pekko-persistence-typed |
pekko-stream | pekko-stream-typed |
pekko-testkit | pekko-actor-testkit-typed |
Cluster Singleton and Distributed Data are included in pekko-cluster-typed
.
Artifacts not listed in above table don’t have a specific API for Pekko Typed.
Package names
The convention of the package names in Pekko Typed is to add typed.scaladsl
and typed.javadsl
to the corresponding Pekko classic package name. scaladsl
and javadsl
is the convention to separate Scala and Java APIs, which is familiar from Pekko Streams.
Examples of a few package names:
Classic | Typed for Scala | Typed for Java |
---|---|---|
org.apache.pekko.actor | org.apache.pekko.actor.typed.scaladsl | org.apache.pekko.actor.typed.javadsl |
org.apache.pekko.cluster | org.apache.pekko.cluster.typed | org.apache.pekko.cluster.typed |
org.apache.pekko.cluster.sharding | org.apache.pekko.cluster.sharding.typed.scaladsl | org.apache.pekko.cluster.sharding.typed.javadsl |
org.apache.pekko.persistence | org.apache.pekko.persistence.typed.scaladsl | org.apache.pekko.persistence.typed.javadsl |
Actor definition
A classic actor is defined by a class extending org.apache.pekko.actor.Actor
org.apache.pekko.actor.AbstractActor
.
An actor in Typed is defined by a class extending org.apache.pekko.actor.typed.scaladsl.AbstractBehavior
org.apache.pekko.actor.typed.javadsl.AbstractBehavior
.
It’s also possible to define an actor in Typed from functions instead of extending a class. This is called the functional style.
Classic HelloWorld actor:
- Scala
-
source
import org.apache.pekko import pekko.actor.Actor import pekko.actor.ActorLogging import pekko.actor.Props object HelloWorld { final case class Greet(whom: String) final case class Greeted(whom: String) def props(): Props = Props(new HelloWorld) } class HelloWorld extends Actor with ActorLogging { import HelloWorld._ override def receive: Receive = { case Greet(whom) => log.info("Hello {}!", whom) sender() ! Greeted(whom) } }
- Java
-
source
import org.apache.pekko.actor.AbstractActor; import org.apache.pekko.actor.Props; import org.apache.pekko.event.Logging; import org.apache.pekko.event.LoggingAdapter; public class HelloWorld extends AbstractActor { public static final class Greet { public final String whom; public Greet(String whom) { this.whom = whom; } } public static final class Greeted { public final String whom; public Greeted(String whom) { this.whom = whom; } } public static Props props() { return Props.create(HelloWorld.class, HelloWorld::new); } private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this); @Override public Receive createReceive() { return receiveBuilder().match(Greet.class, this::onGreet).build(); } private void onGreet(Greet command) { log.info("Hello {}!", command.whom); getSender().tell(new Greeted(command.whom), getSelf()); } }
Typed HelloWorld actor:
- Scala
-
source
import org.apache.pekko import pekko.actor.typed.ActorRef import pekko.actor.typed.Behavior import pekko.actor.typed.scaladsl.AbstractBehavior import pekko.actor.typed.scaladsl.ActorContext import pekko.actor.typed.scaladsl.Behaviors object HelloWorld { final case class Greet(whom: String, replyTo: ActorRef[Greeted]) final case class Greeted(whom: String, from: ActorRef[Greet]) def apply(): Behavior[HelloWorld.Greet] = Behaviors.setup(context => new HelloWorld(context)) } class HelloWorld(context: ActorContext[HelloWorld.Greet]) extends AbstractBehavior[HelloWorld.Greet](context) { import HelloWorld._ override def onMessage(message: Greet): Behavior[Greet] = { context.log.info("Hello {}!", message.whom) message.replyTo ! Greeted(message.whom, context.self) this } }
- Java
-
source
import org.apache.pekko.actor.typed.ActorRef; import org.apache.pekko.actor.typed.Behavior; import org.apache.pekko.actor.typed.javadsl.AbstractBehavior; import org.apache.pekko.actor.typed.javadsl.ActorContext; import org.apache.pekko.actor.typed.javadsl.Behaviors; import org.apache.pekko.actor.typed.javadsl.Receive; import java.util.HashMap; import java.util.Map; public class HelloWorld extends AbstractBehavior<HelloWorld.Greet> { public static final class Greet { public final String whom; public final ActorRef<Greeted> replyTo; public Greet(String whom, ActorRef<Greeted> replyTo) { this.whom = whom; this.replyTo = replyTo; } } public static final class Greeted { public final String whom; public final ActorRef<Greet> from; public Greeted(String whom, ActorRef<Greet> from) { this.whom = whom; this.from = from; } } public static Behavior<Greet> create() { return Behaviors.setup(HelloWorld::new); } private HelloWorld(ActorContext<Greet> context) { super(context); } @Override public Receive<Greet> createReceive() { return newReceiveBuilder().onMessage(Greet.class, this::onGreet).build(); } private Behavior<Greet> onGreet(Greet command) { getContext().getLog().info("Hello {}!", command.whom); command.replyTo.tell(new Greeted(command.whom, getContext().getSelf())); return this; } }
Why is it called Behavior
and not Actor
?
In Typed, the Behavior
defines how to handle incoming messages. After processing a message, a different Behavior
may be returned for processing the next message. This means that an actor is started with an initial Behavior
and may change Behavior
over its lifecycle. This is described more in the section about become.
Note that the Behavior
has a type parameter describing the type of messages that it can handle. This information is not defined explicitly for a classic actor.
Links to reference documentation:
actorOf and Props
A classic actor is started with the actorOf
method of the ActorContext
or ActorSystem
.
Corresponding method in Typed is called spawn
in the org.apache.pekko.actor.typed.scaladsl.ActorContext
org.apache.pekko.actor.typed.javadsl.ActorContext
.
There is no spawn
method in the org.apache.pekko.actor.typed.scaladsl.ActorSystem
org.apache.pekko.actor.typed.javadsl.ActorSystem
for creating top level actors. Instead, there is a single top level actor defined by a user guardian Behavior
that is given when starting the ActorSystem
. Other actors are started as children of that user guardian actor or children of other actors in the actor hierarchy. This is explained more in ActorSystem.
Note that when mixing classic and typed and have a classic system, spawning top level actors from the side is possible, see Coexistence.
The actorOf
method takes an org.apache.pekko.actor.Props
parameter, which is like a factory for creating the actor instance, and it’s also used when creating a new instance when the actor is restarted. The Props
may also define additional properties such as which dispatcher to use for the actor.
In typed, the spawn
method creates an actor directly from a given Behavior
without using a Props
factory. It does however accept an optional org.apache.pekko.actor.typed.Props
for specifying Actor metadata. The factory aspect is instead defined via Behaviors.setup
when using the object-oriented style with a class extending AbstractBehavior
. For the function style there is typically no need for the factory.
Additional properties such as which dispatcher to use for the actor can still be given via an optional org.apache.pekko.actor.typed.Props
parameter of the spawn
method.
The name
parameter of actorOf
is optional and if not defined the actor will have a generated name. Corresponding in Typed is achieved with the spawnAnonymous
method.
Links to reference documentation:
ActorRef
org.apache.pekko.actor.ActorRef
has its correspondence in org.apache.pekko.actor.typed.ActorRef
. The difference being that the latter has a type parameter describing which messages the actor can handle. This information is not defined for a classic actor and you can send any type of message to a classic ActorRef
even though the actor may not understand it.
ActorSystem
org.apache.pekko.actor.ActorSystem
has its correspondence in org.apache.pekko.actor.typed.ActorSystem
. One difference is that when creating an ActorSystem
in Typed you give it a Behavior
that will be used as the top level actor, also known as the user guardian.
Additional actors for an application are created from the user guardian alongside performing the initialization of Pekko components such as Cluster Sharding. In contrast, in a classic ActorSystem
, such initialization is typically performed from the “outside”.
The actorOf
method of the classic ActorSystem
is typically used to create a few (or many) top level actors. The ActorSystem
in Typed doesn’t have that capability. Instead, such actors are started as children of the user guardian actor or children of other actors in the actor hierarchy. The rationale for this is partly about consistency. In a typed system you can’t create children to an arbitrary actor from anywhere in your app without messaging it, so this will also hold true for the user guardian actor. That noted, in cases where you do need to spawn outside of this guardian then you can use the SpawnProtocol
to spawn as needed.
become
A classic actor can change its message processing behavior by using become
in ActorContext
. In Typed this is done by returning a new Behavior
after processing a message. The returned Behavior
will be used for the next received message.
There is no correspondence to unbecome
in Typed. Instead you must explicitly keep track of and return the “previous” Behavior
.
Links to reference documentation:
sender
There is no sender()
getSender()
in Typed. Instead you have to explicitly include an ActorRef
representing the sender—or rather representing where to send a reply to—in the messages.
The reason for not having an implicit sender in Typed is that it wouldn’t be possible to know the type for the sender ActorRef[T]
ActorRef<T>
at compile time. It’s also much better to define this explicitly in the messages as it becomes more clear what the message protocol expects.
Links to reference documentation:
parent
There is no parent
getParent
in Typed. Instead you have to explicitly include the ActorRef
of the parent as a parameter when constructing the Behavior
.
The reason for not having a parent in Typed is that it wouldn’t be possible to know the type for the parent ActorRef[T]
ActorRef<T>
at compile time without having an additional type parameter in the Behavior
. For testing purposes it’s also better to pass in the parent
since it can be replaced by a probe or being stubbed out in tests.
Supervision
An important difference between classic and typed is that in typed, actors are stopped by default if an exception is thrown and no supervision strategy is defined. In contrast, in classic, by default, actors are restarted.
In classic actors the supervision strategy for child actors are defined by overriding the supervisorStrategy
method in the parent actor.
In Typed the supervisor strategy is defined by wrapping the Behavior
of the child actor with Behaviors.supervise
.
The classic BackoffSupervisor
is supported by SupervisorStrategy.restartWithBackoff
as an ordinary SupervisorStrategy
in Typed.
SupervisorStrategy.Escalate
isn’t supported in Typed, but similar can be achieved as described in Bubble failures up through the hierarchy.
Links to reference documentation:
Lifecycle hooks
Classic actors have methods preStart
, preRestart
, postRestart
and postStop
that can be overridden to act on changes to the actor’s lifecycle.
This is supported with corresponding PreRestart
and PostStop
signal messages in Typed. There are no PreStart
and PostRestart
signals because such action can be done from Behaviors.setup
or the constructor of the AbstractBehavior
class.
Note that in classic, the postStop
lifecycle hook is also called when the actor is restarted. That is not the case in Typed, only the PreRestart
signal is emitted. If you need to do resource cleanup on both restart and stop, you have to do that for both PreRestart
and PostStop
.
Links to reference documentation:
watch
watch
and the Terminated
message are pretty much the same, with some additional capabilities in Typed.
Terminated
is a signal in Typed since it is a different type than the declared message type of the Behavior
.
The watchWith
method of the ActorContext
in Typed can be used to send a message instead of the Terminated
signal.
When watching child actors it’s possible to see if the child terminated voluntarily or due to a failure via the ChildFailed
signal, which is a subclass of Terminated
.
Links to reference documentation:
Stopping
Classic actors can be stopped with the stop
method of ActorContext
or ActorSystem
. In Typed an actor stops itself by returning Behaviors.stopped
. There is also a stop
method in the ActorContext
but it can only be used for stopping direct child actors and not any arbitrary actor.
PoisonPill
is not supported in Typed. Instead, if you need to request an actor to stop you should define a message that the actor understands and let it return Behaviors.stopped
when receiving that message.
Links to reference documentation:
ActorSelection
ActorSelection
isn’t supported in Typed. Instead the Receptionist is supposed to be used for finding actors by a registered key.
ActorSelection
can be used for sending messages to a path without having an ActorRef
of the destination. Note that a Group Router can be used for that.
Links to reference documentation:
ask
The classic ask
pattern returns a Future
CompletionStage
for the response.
Corresponding ask
exists in Typed and is good when the requester itself isn’t an actor. It is located in org.apache.pekko.actor.typed.scaladsl.AskPattern
org.apache.pekko.actor.typed.javadsl.AskPattern
.
When the requester is an actor it is better to use the ask
method of the ActorContext
in Typed. It has the advantage of not having to mix Future
CompletionStage
callbacks that are running on different threads with actor code.
Links to reference documentation:
pipeTo
pipeTo
is typically used together with ask
in an actor. The ask
method of the ActorContext
in Typed removes the need for pipeTo
. However, for interactions with other APIs that return Future
CompletionStage
it is still useful to send the result as a message to the actor. For this purpose there is a pipeToSelf
method in the ActorContext
in Typed.
ActorContext.children
The ActorContext
has methods children
and child
getChildren
and getChild
to retrieve the ActorRef
of started child actors in both Typed and Classic.
The type of the returned ActorRef
is unknown, since different types can be used for different children. Therefore, this is not a useful way to lookup children when the purpose is to send messages to them.
Instead of finding children via the ActorContext
, it is recommended to use an application specific collection for bookkeeping of children, such as a Map[String, ActorRef[Child.Command]]
Map<String, ActorRef<Child.Command>>
. It can look like this:
- Scala
-
source
object Parent { sealed trait Command case class DelegateToChild(name: String, message: Child.Command) extends Command private case class ChildTerminated(name: String) extends Command def apply(): Behavior[Command] = { def updated(children: Map[String, ActorRef[Child.Command]]): Behavior[Command] = { Behaviors.receive { (context, command) => command match { case DelegateToChild(name, childCommand) => children.get(name) match { case Some(ref) => ref ! childCommand Behaviors.same case None => val ref = context.spawn(Child(), name) context.watchWith(ref, ChildTerminated(name)) ref ! childCommand updated(children + (name -> ref)) } case ChildTerminated(name) => updated(children - name) } } } updated(Map.empty) } }
- Java
-
source
public class Parent extends AbstractBehavior<Parent.Command> { public interface Command {} public static class DelegateToChild implements Command { public final String name; public final Child.Command message; public DelegateToChild(String name, Child.Command message) { this.name = name; this.message = message; } } private static class ChildTerminated implements Command { final String name; ChildTerminated(String name) { this.name = name; } } public static Behavior<Command> create() { return Behaviors.setup(Parent::new); } private Map<String, ActorRef<Child.Command>> children = new HashMap<>(); private Parent(ActorContext<Command> context) { super(context); } @Override public Receive<Command> createReceive() { return newReceiveBuilder() .onMessage(DelegateToChild.class, this::onDelegateToChild) .onMessage(ChildTerminated.class, this::onChildTerminated) .build(); } private Behavior<Command> onDelegateToChild(DelegateToChild command) { ActorRef<Child.Command> ref = children.get(command.name); if (ref == null) { ref = getContext().spawn(Child.create(), command.name); getContext().watchWith(ref, new ChildTerminated(command.name)); children.put(command.name, ref); } ref.tell(command.message); return this; } private Behavior<Command> onChildTerminated(ChildTerminated command) { children.remove(command.name); return this; } }
Remember to remove entries from the Map
when the children are terminated. For that purpose it’s convenient to use watchWith
, as illustrated in the example above, because then you can include the key to the Map
in the termination message. In that way the name of the actor doesn’t have to be the same as identifier used for bookkeeping.
Retrieving the children from the ActorContext
can still be useful for some use cases, such as:
- see if a child name is in use
- stopping children
- the type of the child is well known and
unsafeUpcast
of theActorRef
is considered “safe enough”
Remote deployment
Starting an actor on a remote node—so called remote deployment—isn’t supported in Typed.
This feature would be discouraged because it often results in tight coupling between nodes and undesirable failure handling. For example if the node of the parent actor crashes, all remote deployed child actors are brought down with it. Sometimes, that can be desired but many times it is used without realizing. This can be achieved by other means, such as using watch
.
Routers
Routers are provided in Typed, but in a much simplified form compared to the classic routers.
Destinations of group routers are registered in the Receptionist
, which makes them Cluster aware and also more dynamic than classic group routers.
Pool routers are only for local actor destinations in Typed, since remote deployment isn’t supported.
Links to reference documentation:
FSM
With classic actors there is explicit support for building Finite State Machines. No support is needed in Pekko Typed as it is straightforward to represent FSMs with behaviors.
Links to reference documentation:
Timers
In classic actors you mixin with Timers
extend AbstractActorWithTimers
to gain access to delayed and periodic scheduling of messages. In Typed you have access to similar capabilities via Behaviors.withTimers
.
Links to reference documentation:
Stash
In classic actors you mixin with Stash
extend AbstractActorWithStash
to gain access to stashing of messages. In Typed you have access to similar capabilities via Behaviors.withStash
.
Links to reference documentation:
PersistentActor
The correspondence of the classic PersistentActor
is org.apache.pekko.persistence.typed.scaladsl.EventSourcedBehavior
org.apache.pekko.persistence.typed.javadsl.EventSourcedBehavior
.
The Typed API is much more guided to facilitate Event Sourcing best practices. It also has tighter integration with Cluster Sharding.
Links to reference documentation:
Asynchronous Testing
The Test Kits for asynchronous testing are rather similar.
Links to reference documentation:
Synchronous Testing
Classic and typed have different Test Kits for synchronous testing.
Behaviors in Typed can be tested in isolation without having to be packaged into an actor. As a consequence, tests can run fully synchronously without having to worry about timeouts and spurious failures.
The BehaviorTestKit
provides a nice way of unit testing a Behavior
in a deterministic way, but it has some limitations to be aware of. Similar limitations exists for synchronous testing of classic actors.
Links to reference documentation: