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 ActorSystems: actor.ActorSystemactor.ActorSystem and actor.typed.ActorSystemactor.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
sourceimport 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
sourceobject 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
sourcepublic 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
sourceval classicActor = system.actorOf(Classic.props())
Java
sourceorg.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
sourceclass 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
sourcepublic 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
sourceval system = classic.ActorSystem("TypedWatchingClassic")
val typed = system.spawn(Typed.behavior, "Typed")
Java
sourceActorSystem 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
sourceobject 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
sourcepublic 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
sourceval system = classic.ActorSystem("TypedWatchingClassic")
val typed = system.spawn(Typed.behavior, "Typed")
Java
sourceActorSystem 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
sourceobject 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
sourcepublic 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 ActorRefActorRef 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.