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 theActorContext
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?
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 toString
s):
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 start
ed 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.