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

185 Views Asked by At

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?

2

There are 2 best solutions below

3
Levi Ramsey On BEST ANSWER

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.

1
Gastón Schabas On

That is the expected behavior. Maybe Behaviors as finite state machines can help.

An actor is similar to an instance of a class, but instead of calling its methods you can only interact with them just sending messages through the ActorRef. A simple example of that is

import akka.actor.typed.ActorSystem

// start the actor system with a behavior that ignores any message of type `String`
val actorSystem = ActorSystem(Behaviors.empty[String], "empty-behavior")

// send a message of type `String` to the `actorRef`
actorSystem ! "hello"

Here I'm creating an actor system that only accept messages of type String, but as you can see in the docs of Behaviors.empty it will just ignore whatever it receives

A behavior that treats every incoming message as unhandled.

So, an actor can have a Behavior of type T, which means it will only accept messages of type T. Let's see an example where the actor do something.

import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors

case object HelloMessage

// setup the behavior
val behavior = Behaviors.setup[HelloMessage.type] { context =>
  Behaviors.receiveMessage[HelloMessage.type] {
    // the logic applied for the message received
    case HelloMessage =>
      context.log.info("hello message received")
      Behaviors.same
  }
}

// start the actor system
val actorSystem: ActorSystem[HelloMessage.type] = ActorSystem(behavior, "hello-behavior")

// send a message through the actor ref
actorSystem ! HelloMessage
actorSystem ! HelloMessage
actorSystem ! HelloMessage

// the following line will note compile due to the actor system only
// accepts messages of type `HelloMessage`
actorSystem ! "hello"

If we execute the above example, we will see the message hello message received three times because we sent the HelloMessage three times. Let's see another example but this time instead of using Behaviors.same we will return a different Behavior.

import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors

// define messages that will be accepted by our actor
sealed trait Message
case object Ping extends Message
case object Pong extends Message

val behavior = Behaviors.setup[Message] { context =>
  def pingBehavior: Behaviors.Receive[Message] =
    Behaviors.receiveMessage[Message] {
      case Ping =>
        context.log.info("Ping received. Waiting for Pong")
        pongBehavior // return different behavior
      case Pong =>
        context.log.warn("Waiting for a Pong. Don't send Ping")
        Behaviors.same // return same behavior
    }

  def pongBehavior: Behaviors.Receive[Message] =
    Behaviors.receiveMessage[Message] {
      case Ping =>
        context.log.warn("Waiting for a Pong. Don't send Ping")
        Behaviors.same // return same behavior
      case Pong =>
        context.log.info("Pong received. Waiting for Ping")
        pingBehavior // return different behavior
    }
  pingBehavior // initial behavior
}

// start the actor system with the behaviors defined
val actorSystem = ActorSystem(behavior, "ping-pong-behavior")

// send Ping and Pong messages
actorSystem ! Pong
actorSystem ! Ping
actorSystem ! Ping
actorSystem ! Pong

The output of executing this will be

WARN - Waiting for a Pong. Don't send Ping
INFO - Ping received. Waiting for Pong
WARN - Waiting for a Pong. Don't send Ping
INFO - Pong received. Waiting for Ping

As we already see, we can send messages to an actor. Also the actors can send messages to other actors and to themself. If an actor sends a message to itself and returns the same behavior, we will be able to have the infinite output you mentioned in your question.

import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors

case object HelloMessage

// setup the behavior
val behavior = Behaviors.setup[HelloMessage.type] { context =>
  Behaviors.receiveMessage[HelloMessage.type] {
    case HelloMessage =>
      context.log.info("hello message received")
      context.self ! HelloMessage // send a message to itself
      Behaviors.same // return the same behavior
  }
}

val actorSystem: ActorSystem[HelloMessage.type] = ActorSystem(behavior, "infinite-behavior")

actorSystem ! HelloMessage

Once this code is executed, it will start printing hello message received for ever until you manually stop the process.