scalaakkaakka-typed

What is the effect of returning Behaviors.same from within Behaviors.setup?


Akka's documentation says:

def same[T]: Behavior[T]

Return this behavior from message processing in order to advise the system to reuse the previous behavior. This is provided in order to avoid the allocation overhead of recreating the current behavior where that is not necessary.

def setup[T](factory: (ActorContext[T]) => Behavior[T]): Behavior[T]

setup is a factory for a behavior. Creation of the behavior instance is deferred until the actor is started, as opposed to Behaviors.receive that creates the behavior instance immediately before the actor is running. The factory function pass the ActorContext as parameter and that can for example be used for spawning child actors.

setup is typically used as the outer most behavior when spawning an actor, but it can also be returned as the next behavior when processing a message or signal. In that case it will be started immediately after it is returned, i.e. next message will be processed by the started behavior.

I have tried to construct an actor system with the following guardian behavior:

public static Behavior<SayHello> create() {
    return Behaviors.setup(ctx -> {
        System.out.println(ctx.getSelf().path() + ": returning same()");
        return Behaviors.same();
    });
}

I was expecting Akka to recursively apply the behavior in question. In other words, I was expecting Akka to produce this infinite output:

akka://helloakka/user: returning same
akka://helloakka/user: returning same
akka://helloakka/user: returning same
...

However, it just prints it one time.

Is this behavior expected? What is the actual meaning of the behavior I provided? Can you devise a scenario where returning same from within setup makes sense?

Edit: I did another experiment, where I return the named behavior itself instead of same. I expected no differences, since that same should just be an optimization for reusing the previous behavior rather than allocating a new one. However, to my surprise, the output is actually infinite.

public static Behavior<SayHello> create() {
    return Behaviors.setup(ctx -> {
        System.out.println(ctx.getSelf().path() + ": returning create()");
        return create();
    });
}

What am I missing here?


Solution

  • Behaviors.setup (in the recent implementations: this hasn't changed semantically since prior to 2.6) is just a factory for an akka.actor.typed.internal.BehaviorImpl.DeferredBehavior (since your examples are using the javadsl, I'm starting from the javadsl; the scaladsl is the same under the hood here):

    // factory is the function from `javadsl.ActorContext[T]` to `Behavior[T]` passed in
    BehaviorImpl.DeferredBehavior(ctx => factory.apply(ctx.asJava))
    

    Where DeferredBehavior is (omitting things like toStrings):

    object DeferredBehavior {
      def apply[T](factory: scaladsl.ActorContext[T] => Behavior[T]): Behavior[T] =
        new DeferredBehavior[T] {
          def apply(ctx: TypedActorContext[T]): Behavior[T] = factory(ctx.asScala)
        }
    }
    
    abstract class DeferredBehavior[T] extends Behavior[T](BehaviorTags.DeferredBehavior) {
      def apply(ctx: TypedActorContext[T]): Behavior[T]
    }
    

    Note that the factory isn't called until DeferredBehavior::apply is called.

    When you spawn an actor with that behavior (DeferredBehavior), a classic actor which is an instance of ActorAdapter is spawned.

    // _initialBehavior is the DeferredBehavior in this case, omitting `if` checks that follow from this
    private var behavior: Behavior[T] = _initialBehavior
    def currentBehavior: Behavior[T] = behavior
    
    def preStart(): Unit =
      try {
        // ctx is the typed ActorContext, context is the classic ActorContext
        behavior = Behavior.validateAsInitial(Behavior.start(behavior, ctx))
        if (!Behavior.isAlive(behavior)) context.stop(self)
      } finally ctx.clearMdc()
    

    Behavior.start is effectively, for our purposes:

    if (behavior.isInstanceOf[DeferredBehavior[T]]) {
      Behavior.start(behavior.asInstanceOf[DeferredBehavior[T]].apply(ctx), ctx)
    } else behavior
    

    So now we call the factory method, which in this case ultimately returns BehaviorImpl.SameBehavior after executing your println. This SameBehavior gets passed to validateAsInitial, which throws an IllegalArgumentException because Behaviors.same and Behaviors.unhandled aren't valid initial behaviors. This exception effectively kills the actor as it's being born (grisly, I know).

    When you call back into create, on the other hand, the factory will return another DeferredBehavior with the same factory, so that will get repeatedly passed to start; depending on whether the Scala compiler used to build Akka noticed that Behavior.start in this case is tail-recursive this would either result in an infinite loop or a stack overflow.

    A Behaviors.setup which results in a Behaviors.same only makes sense if you want an actor to be stillborn. The side effects in Behaviors.setup will still happen, but if that's all you want, why not just do them directly and save the pointless overhead?

    The foregoing technically only applies to normal actors. The guardian behavior is special, in that it first waits for the delivery of a special message from the actor system signalling that the actor system is ready, after which it wraps the behavior in an interceptor which tears down the actor system if the behavior is no longer alive (viz. the behavior is stopped or failed). At no point is the behavior validated as initial, but the wrapped behavior is started as above, which runs the Behaviors.setup block once and forgets the factory.

    At this point the behavior is a bare SameBehavior which hasn't handled a single message. If you sent a message to the ActorSystem (which is an ActorRef), it would be interpreted by Behaviors.interpret which would find the SameBehavior and throw then, which would be a crash of the user actor hierarchy, but doesn't seem to (in my experiment) stop the actor system.