Migrating project to typed actor system

We have been using typed actors for more that a year now. But the ActorSystem at top level was untyped, and we were creating typed actors from untyped system using spawn/spawnAnonymous adapter methods.

Now with Akka 2.5.22 being production ready we are thinking to migrate our untyped ActorSystem to typed. While trying to achieve this, in the process, we were having typed system and untyped system both existing in our code base for some time. Having both (typed or untyped) actor system at the same time gave us an exception:

object App1 extends App {

  val typedSystem = ActorSystem(Behaviors.empty, "test")

  val untypedSystem = typedSystem.toUntyped

  untypedSystem.actorOf(Props.empty)
}

gives following error:

[error] Exception in thread "main" java.lang.UnsupportedOperationException: cannot create top-level actor from the outside on ActorSystem with custom user guardian
[error] 	at akka.actor.ActorSystemImpl.actorOf(ActorSystem.scala:786)
[error] 	at App1$.delayedEndpoint$App1$1(App1.scala:12)
[error] 	at App1$delayedInit$body.apply(App1.scala:6)
[error] 	at scala.Function0.apply$mcV$sp(Function0.scala:39)
[error] 	at scala.Function0.apply$mcV$sp$(Function0.scala:39)
[error] 	at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17)
[error] 	at scala.App.$anonfun$main$1$adapted(App.scala:80)
[error] 	at scala.collection.immutable.List.foreach(List.scala:392)
[error] 	at scala.App.main(App.scala:80)
[error] 	at scala.App.main$(App.scala:78)
[error] 	at App1$.main(App1.scala:6)
[error] 	at App1.main(App1.scala)

While tracing back the exception, we got to know that untypedSystem.actorOf checks if a custom guardian is wired or not, if it is then it throws the above mentioned exception and in case of untypedSystem being created from typed system will always have a guardian actor (in our case guardian actor is an actor with empty behaviour).

Hence, we concluded that typed system being converted to untyped system and actor created via actorOf method is not an ideal approach. Maybe we should create untyped actor via ctx.actorOf.

This popped up a question for us that if Extensions(CorordinatedShutdown, Http, Tcp, etc.) are taking untyped actor system and spawning actors internally, can we give typedSystem.toUntyped to extensions (since we want to have typedSystem at the top level of our wiring)? The observation is that we can give typedSystem.toUntyped to extensions because they spawn actors via systemActorOf instead of actorOf and systemActorOf always create actors under systemGuardian so it does not fail with java.lang.UnsupportedOperationException: cannot create top-level actor from the outside on ActorSystem with custom user guardian exception.

The next question is if a third party library creates extensions and if they are using actorOf instead of systemActorOf then having a typedSystem at the top and converting it to untyped to pass to extension will fail.

So, what should be the our approach of actorsystem at the top of our wiring :

  • Do we keep typedSystem at the top and expect third party extensions to use systemActorOf
  • Or we keep untypedSystem at top and make it typed to use it everywhere for e.g. create
    actors?

Hi @skvithalani

If you are doing a step by step migration to typed, I think the best way in general would be to start from the leaves of the actor tree rather than the top, so to speak. Typed and untyped can co-exist, so it is fine to transform one actor at a time, but still have its parent untyped, and then work upward from there. This should make replacing the user guardian actor and the untyped actor system the last thing you change on your path to running only typed actors.

All the cluster tools etc. should work with an untyped system that is adapted into a typed one, so if you find any problems with extensions, please let us know!

As you say if a third party library tries to spawn arbitrary actors that could be a problem, the same goes there, please report such issues to those third party libraries if you find them.

Thanks for the reply @johanandren. We had started with leaf actors and converted them to typed. Now we are at the stage where only top level actor system is untyped and there is untyped user guardian. So, the question is should we change top level actorsystem to be created as typed as in ActorSystem(guardianBeh, "top-level-system") or leave it untyped as is ActorSystem("top-level-system")?

If there are no untyped actors there isn’t any reason to keep the actor system untyped.

The idea is to let the user guardian bootstrap all parts of the application (and potentially keep track that they are still running etc), but it is also possible to use something more like the untyped style where you start actors from outside of the actor system by using akka.actor.typed.SpawnProtocol#behavior as the user guardian and sending Spawn messages to it.

Thanks @johanandren. One more thought on a slightly different scenario. Let’s say if my code accepts a typedActorSystem ActorSystem[_] as shown below:

class Foo (val system: ActorSystem[_])

and the way I create typed actorSystem to pass in above Foo class is as follows:

val untypedSystem = ActorSystem("top-level-system")
new Foo(untypedSystem.toTyped)

Now in Foo class if I want to create any actors using the typed actorSystem I can only create in system space via system.systemActorOf. The other option is to adapt it to untyped system and create actor via actorOf method.

So, is it a good idea to provide a mechanism to create a guardian actor for typed actorSystem at later stage after typed system is created, for example:

typedSystem.createGuardian(behavior)

What are your views on this?

There is probably an area here that is left to explore best practices around and if there is something missing in the APIs.

In general I’d recommend try to avoid passing the actor system around to components that need to create actors, and instead have those components be behaviors themselves that will spawn actors they need as children (so that’d be a Foo.behavior instead of a Foo(system)).

The user guardian lifecycle is tightly tied to the actor system, they are essentially started at the same time and whenever the user guardian is stopped, the actor system will stop. This means it cannot be created later, the behavior must be provided up front. It can however do nothing when it is started and wait for a message before it spawns subsystems/other actors etc.

Okay thanks.