Coexistence
Dependency
To use Pekko Actor Typed, you must add the following dependency in your project:
- sbt
val PekkoVersion = "1.0.0" libraryDependencies += "org.apache.pekko" %% "pekko-actor-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.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.apache.pekko</groupId> <artifactId>pekko-actor-typed_${scala.binary.version}</artifactId> </dependency> </dependencies>
- Gradle
def versions = [ ScalaBinary: "2.13" ] dependencies { implementation platform("org.apache.pekko:pekko-bom_${versions.ScalaBinary}:1.0.0") implementation "org.apache.pekko:pekko-actor-typed_${versions.ScalaBinary}" }
Introduction
We believe Pekko Typed will be adopted in existing systems gradually and therefore it’s important to be able to use typed and classic actors together, within the same ActorSystem
. Also, we will not be able to integrate with all existing modules in one big bang release and that is another reason for why these two ways of writing actors must be able to coexist.
There are two different ActorSystem
s: actor.ActorSystem
actor.ActorSystem
and actor.typed.ActorSystem
actor.typed.ActorSystem
.
Currently the typed actor system is implemented using the classic actor system under the hood. This may change in the future.
Typed and classic can interact the following ways:
- classic actor systems can create typed actors
- typed actors can send messages to classic actors, and opposite
- spawn and supervise typed child from classic parent, and opposite
- watch typed from classic, and opposite
- classic actor system can be converted to a typed actor system
In the examples the pekko.actor
package is aliased to classic
.
- Scala
-
source
import org.apache.pekko.{ actor => classic }
The examples use fully qualified class names for the classic classes to distinguish between typed and classic classes with the same name.
Classic to typed
While coexisting your application will likely still have a classic ActorSystem. This can be converted to a typed ActorSystem so that new code and migrated parts don’t rely on the classic system:
- Scala
-
source
// adds support for actors to a classic actor system and context import org.apache.pekko.actor.typed.scaladsl.adapter._ val system = pekko.actor.ActorSystem("ClassicToTypedSystem") val typedSystem: ActorSystem[Nothing] = system.toTyped
- Java
-
source
// In java use the static methods on Adapter to convert from typed to classic import org.apache.pekko.actor.typed.javadsl.Adapter; org.apache.pekko.actor.ActorSystem classicActorSystem = org.apache.pekko.actor.ActorSystem.create(); ActorSystem<Void> typedActorSystem = Adapter.toTyped(classicActorSystem);
Then for new typed actors here’s how you create, watch and send messages to it from a classic actor.
- Scala
-
source
object Typed { sealed trait Command final case class Ping(replyTo: ActorRef[Pong.type]) extends Command case object Pong def apply(): Behavior[Command] = Behaviors.receive { (context, message) => message match { case Ping(replyTo) => context.log.info(s"${context.self} got Ping from $replyTo") // replyTo is a classic actor that has been converted for coexistence replyTo ! Pong Behaviors.same } } }
- Java
-
source
public abstract static class Typed { interface Command {} public static class Ping implements Command { public final org.apache.pekko.actor.typed.ActorRef<Pong> replyTo; public Ping(ActorRef<Pong> replyTo) { this.replyTo = replyTo; } } public static class Pong {} public static Behavior<Command> behavior() { return Behaviors.receive(Typed.Command.class) .onMessage( Typed.Ping.class, message -> { message.replyTo.tell(new Pong()); return same(); }) .build(); } }
The top level classic actor is created in the usual way:
- Scala
-
source
val classicActor = system.actorOf(Classic.props())
- Java
-
source
org.apache.pekko.actor.ActorSystem as = org.apache.pekko.actor.ActorSystem.create(); org.apache.pekko.actor.ActorRef classic = as.actorOf(Classic.props());
Then it can create a typed actor, watch it, and send a message to it:
- Scala
-
source
class Classic extends classic.Actor with ActorLogging { // context.spawn is an implicit extension method val second: ActorRef[Typed.Command] = context.spawn(Typed(), "second") // context.watch is an implicit extension method context.watch(second) // self can be used as the `replyTo` parameter here because // there is an implicit conversion from org.apache.pekko.actor.ActorRef to // org.apache.pekko.actor.typed.ActorRef // An equal alternative would be `self.toTyped` second ! Typed.Ping(self) override def receive = { case Typed.Pong => log.info(s"$self got Pong from ${sender()}") // context.stop is an implicit extension method context.stop(second) case classic.Terminated(ref) => log.info(s"$self observed termination of $ref") context.stop(self) } }
- Java
-
source
public static class Classic extends AbstractActor { public static org.apache.pekko.actor.Props props() { return org.apache.pekko.actor.Props.create(Classic.class); } private final org.apache.pekko.actor.typed.ActorRef<Typed.Command> second = Adapter.spawn(getContext(), Typed.behavior(), "second"); @Override public void preStart() { Adapter.watch(getContext(), second); second.tell(new Typed.Ping(Adapter.toTyped(getSelf()))); } @Override public Receive createReceive() { return receiveBuilder() .match( Typed.Pong.class, message -> { Adapter.stop(getContext(), second); }) .match( org.apache.pekko.actor.Terminated.class, t -> { getContext().stop(getSelf()); }) .build(); } }
There is one import
that is needed to make that work. We import the Adapter class and call static methods for conversion.
- Scala
-
source
// adds support for actors to a classic actor system and context import org.apache.pekko.actor.typed.scaladsl.adapter._
- Java
-
source
// In java use the static methods on Adapter to convert from typed to classic import org.apache.pekko.actor.typed.javadsl.Adapter;
That adds some implicit extension methods that are added to classic and typed ActorSystem
, ActorContext
and ActorRef
in both directions. To convert between typed and classic ActorSystem
, ActorContext
and ActorRef
in both directions there are adapter methods in pekko.actor.typed.javadsl.Adapter
. Note the inline comments in the example above.
This method of using a top level classic actor is the suggested path for this type of co-existence. However, if you prefer to start with a typed top level actor then you can use the implicit spawn
-methodAdapter.spawn
directly from the typed system:
- Scala
-
source
val system = classic.ActorSystem("TypedWatchingClassic") val typed = system.spawn(Typed.behavior, "Typed")
- Java
-
source
ActorSystem as = ActorSystem.create(); ActorRef<Typed.Command> typed = Adapter.spawn(as, Typed.create(), "Typed");
The above classic-typed difference is further elaborated in the ActorSystem
section of “Learning Pekko Typed from Classic”.
Typed to classic
Let’s turn the example upside down and first start the typed actor and then the classic as a child.
The following will show how to create, watch and send messages back and forth from a typed actor to this classic actor:
- Scala
-
source
object Classic { def props(): classic.Props = classic.Props(new Classic) } class Classic extends classic.Actor { override def receive = { case Typed.Ping(replyTo) => replyTo ! Typed.Pong } }
- Java
-
source
public static class Classic extends AbstractActor { public static org.apache.pekko.actor.Props props() { return org.apache.pekko.actor.Props.create(Classic.class); } @Override public Receive createReceive() { return receiveBuilder().match(Typed.Ping.class, this::onPing).build(); } private void onPing(Typed.Ping message) { message.replyTo.tell(Typed.Pong.INSTANCE); } }
Creating the actor system and the typed actor:
- Scala
-
source
val system = classic.ActorSystem("TypedWatchingClassic") val typed = system.spawn(Typed.behavior, "Typed")
- Java
-
source
ActorSystem as = ActorSystem.create(); ActorRef<Typed.Command> typed = Adapter.spawn(as, Typed.create(), "Typed");
Then the typed actor creates the classic actor, watches it and sends and receives a response:
- Scala
-
source
object Typed { final case class Ping(replyTo: pekko.actor.typed.ActorRef[Pong.type]) sealed trait Command case object Pong extends Command val behavior: Behavior[Command] = Behaviors.setup { context => // context.actorOf is an implicit extension method val classic = context.actorOf(Classic.props(), "second") // context.watch is an implicit extension method context.watch(classic) // illustrating how to pass sender, toClassic is an implicit extension method classic.tell(Typed.Ping(context.self), context.self.toClassic) Behaviors .receivePartial[Command] { case (context, Pong) => // it's not possible to get the sender, that must be sent in message // context.stop is an implicit extension method context.stop(classic) Behaviors.same } .receiveSignal { case (_, pekko.actor.typed.Terminated(_)) => Behaviors.stopped } } }
- Java
-
source
public static class Typed extends AbstractBehavior<Typed.Command> { public static class Ping { public final org.apache.pekko.actor.typed.ActorRef<Pong> replyTo; public Ping(ActorRef<Pong> replyTo) { this.replyTo = replyTo; } } interface Command {} public enum Pong implements Command { INSTANCE } private final org.apache.pekko.actor.ActorRef second; private Typed(ActorContext<Command> context, org.apache.pekko.actor.ActorRef second) { super(context); this.second = second; } public static Behavior<Command> create() { return org.apache.pekko.actor.typed.javadsl.Behaviors.setup( context -> { org.apache.pekko.actor.ActorRef second = Adapter.actorOf(context, Classic.props(), "second"); Adapter.watch(context, second); second.tell( new Typed.Ping(context.getSelf().narrow()), Adapter.toClassic(context.getSelf())); return new Typed(context, second); }); } @Override public Receive<Command> createReceive() { return newReceiveBuilder() .onMessage(Typed.Pong.class, message -> onPong()) .onSignal(org.apache.pekko.actor.typed.Terminated.class, sig -> Behaviors.stopped()) .build(); } private Behavior<Command> onPong() { Adapter.stop(getContext(), second); return this; } }
Note that when sending from a typed actor to a classic ActorRef
ActorRef
there is no sender in scope as in classic. The typed sender should use its own ActorContext[T].self
explicitly, as shown in the snippet.
Supervision
The default supervision for classic actors is to restart whereas for typed it is to stop. When combining classic and typed actors the default supervision is based on the default behavior of the child, for example if a classic actor creates a typed child, its default supervision will be to stop. If a typed actor creates a classic child, its default supervision will be to restart.