Coexistence
We believe Akka Typed will be adopted in existing systems gradually and therefore it’s important to be able to use typed and untyped 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: akka.actor.ActorSystem
and akka.actor.typed.ActorSystem
.
Currently the typed actor system is implemented using an untyped actor system under the hood. This may change in the future.
Typed and untyped can interact the following ways:
- untyped actor systems can create typed actors
- typed actors can send messages to untyped actors, and opposite
- spawn and supervise typed child from untyped parent, and opposite
- watch typed from untyped, and opposite
- untyped actor system can be converted to a typed actor system
In the examples the akka.actor
package is aliased to untyped
.
- Scala
-
import akka.{ actor ⇒ untyped }
The examples use fully qualified class names for the untyped classes to distinguish between typed and untyped classes with the same name.
Untyped to typed
While coexisting your application will likely still have an untyped ActorSystem. This can be converted to a typed ActorSystem so that new code and migrated parts don’t rely on the untyped system:
- Scala
-
val system = akka.actor.ActorSystem("UntypedToTypedSystem") val typedSystem: ActorSystem[Nothing] = system.toTyped
- Java
-
akka.actor.ActorSystem untypedActorSystem = akka.actor.ActorSystem.create(); ActorSystem<Void> typedActorSystem = Adapter.toTyped(untypedActorSystem);
Then for new typed actors here’s how you create, watch and send messages to it from an untyped actor.
- Scala
-
object Typed { sealed trait Command final case class Ping(replyTo: ActorRef[Pong.type]) extends Command case object Pong val behavior: Behavior[Command] = Behaviors.immutable { (ctx, msg) ⇒ msg match { case Ping(replyTo) ⇒ println(s"${ctx.self} got Ping from $replyTo") // replyTo is an untyped actor that has been converted for coexistence replyTo ! Pong Behaviors.same } } }
- Java
-
public static abstract class Typed { interface Command { } public static class Ping implements Command { public final akka.actor.typed.ActorRef<Pong> replyTo; public Ping(ActorRef<Pong> replyTo) { this.replyTo = replyTo; } } public static class Pong { } public static Behavior<Command> behavior() { return Behaviors.immutable(Typed.Command.class) .onMessage(Typed.Ping.class, (ctx, msg) -> { msg.replyTo.tell(new Pong()); return same(); }) .build(); } }
The top level untyped actor is created in the usual way:
- Scala
-
val untypedActor = system.actorOf(Untyped.props())
- Java
-
akka.actor.ActorSystem as = akka.actor.ActorSystem.create(); akka.actor.ActorRef untyped = as.actorOf(Untyped.props());
Then it can create a typed actor, watch it, and send a message to it:
- Scala
-
class Untyped extends untyped.Actor { // context.spawn is an implicit extension method val second: ActorRef[Typed.Command] = context.spawn(Typed.behavior, "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 akka.actor.ActorRef to // akka.actor.typed.ActorRef second ! Typed.Ping(self) override def receive = { case Typed.Pong ⇒ println(s"$self got Pong from ${sender()}") // context.stop is an implicit extension method context.stop(second) case untyped.Terminated(ref) ⇒ println(s"$self observed termination of $ref") context.stop(self) } }
- Java
-
public static class Untyped extends AbstractActor { public static akka.actor.Props props() { return akka.actor.Props.create(Untyped.class); } private final akka.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, msg -> { Adapter.stop(getContext(), second); }) .match(akka.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
-
// adds support for typed actors to an untyped actor system and context import akka.actor.typed.scaladsl.adapter._
- Java
-
// In java use the static methods on Adapter to convert from typed to untyped import akka.actor.typed.javadsl.Adapter;
That adds some implicit extension methods that are added to untyped and typed ActorSystem
and ActorContext
in both directions. To convert between typed and untyped there are adapter methods in akka.typed.javadsl.Adapter
. Note the inline comments in the example above.
Typed to untyped
Let’s turn the example upside down and first start the typed actor and then the untyped as a child.
The following will show how to create, watch and send messages back and forth from a typed actor to this untyped actor:
- Scala
-
object Untyped { def props(): untyped.Props = untyped.Props(new Untyped) } class Untyped extends untyped.Actor { override def receive = { case Typed.Ping(replyTo) ⇒ replyTo ! Typed.Pong } }
- Java
-
public static class Untyped extends AbstractActor { public static akka.actor.Props props() { return akka.actor.Props.create(Untyped.class); } @Override public Receive createReceive() { return receiveBuilder() .match(Typed.Ping.class, msg -> { msg.replyTo.tell(new Typed.Pong()); }) .build(); } }
Creating the actor system and the typed actor:
- Scala
-
val system = untyped.ActorSystem("TypedWatchingUntyped") val typed = system.spawn(Typed.behavior, "Typed")
- Java
-
ActorSystem as = ActorSystem.create(); ActorRef<Typed.Command> typed = Adapter.spawn(as, Typed.behavior(), "Typed");
Then the typed actor creates the untyped actor, watches it and sends and receives a response:
- Scala
-
object Typed { final case class Ping(replyTo: akka.actor.typed.ActorRef[Pong.type]) sealed trait Command case object Pong extends Command val behavior: Behavior[Command] = Behaviors.setup { context ⇒ // context.spawn is an implicit extension method val untyped = context.actorOf(Untyped.props(), "second") // context.watch is an implicit extension method context.watch(untyped) // illustrating how to pass sender, toUntyped is an implicit extension method untyped.tell(Typed.Ping(context.self), context.self.toUntyped) Behaviors.immutablePartial[Command] { case (ctx, Pong) ⇒ // it's not possible to get the sender, that must be sent in message // context.stop is an implicit extension method ctx.stop(untyped) Behaviors.same } onSignal { case (_, akka.actor.typed.Terminated(_)) ⇒ Behaviors.stopped } } }
- Java
-
public static abstract class Typed { public static class Ping { public final akka.actor.typed.ActorRef<Pong> replyTo; public Ping(ActorRef<Pong> replyTo) { this.replyTo = replyTo; } } interface Command { } public static class Pong implements Command { } public static Behavior<Command> behavior() { return akka.actor.typed.javadsl.Behaviors.setup(context -> { akka.actor.ActorRef second = Adapter.actorOf(context, Untyped.props(), "second"); Adapter.watch(context, second); second.tell(new Typed.Ping(context.getSelf().narrow()), Adapter.toUntyped(context.getSelf())); return akka.actor.typed.javadsl.Behaviors.immutable(Typed.Command.class) .onMessage(Typed.Pong.class, (ctx, msg) -> { Adapter.stop(ctx, second); return same(); }) .onSignal(akka.actor.typed.Terminated.class, (ctx, sig) -> stopped()) .build(); }); } }
There is one caveat regarding supervision of untyped child from typed parent. If the child throws an exception we would expect it to be restarted, but supervision in Akka Typed defaults to stopping the child in case it fails. The restarting facilities in Akka Typed will not work with untyped children. However, the workaround is simply to add another untyped actor that takes care of the supervision, i.e. restarts in case of failure if that is the desired behavior.