Search code examples
javaakkaakka-typed

Switching to different behaviors does not work as intended


So I was playing with different behaviors in Akka. When I executed this code:

@Override
public Receive<CommonCommand> createReceive() {
    return notYetStarted();
}

public Receive<CommonCommand> notYetStarted() {
    return newReceiveBuilder()

            .onMessage(RaceLengthCommand.class, message -> {
                
                // business logic

                return running();
            })

            .build();
}

public Receive<CommonCommand> running() {
    return newReceiveBuilder()

            .onMessage(AskPosition.class, message -> {

                if ("some_condition") {

                    // business logic
                    
                    return this;

                } else {

                    // business logic

                    return completed(completedTime);
                }

            })

            .build();

}

public Receive<CommonCommand> completed(long completedTime) {
    return newReceiveBuilder()

            .onMessage(AskPosition.class, message -> {
                
                // business logic
                
                return this;
            })

            .build();

}

I got following log:

21:46:41.038 [monitor-akka.actor.default-dispatcher-6] INFO akka.actor.LocalActorRef - Message [learn.tutorial._5_racing_game_akka.RacerBehavior$AskPosition] to Actor[akka://monitor/user/racer_1#-1301834398] was unhandled. [1] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.

Initially the RaceLengthCommand message is sent to notYetStarted() behavior. That works fine. Then this behavior should transition to running() behavior, and this second one should receive the message AskPosition.

But according to my tests, the AskPosition message is delivered to notYetStarted() behavior. This contradicts my whole understanding of the concept.

I confirmed this by copying the onMessage() part from running() behavior and pasting on notYetStarted() behavior. Now the code executes fine and no more deadletters.

So apparently notYetStarted() behavior is indeed receiving messages even after I switched behaviors? Why is this happening???


Solution

  • It seems like your actor definition is mixing the OO and functional styles and hitting some warts in the interplay between using those styles in the same actor. Some of this confusion arises from Receive in the Java API being a Behavior but not an AbstractBehavior. This may be a vestige of an earlier evolution of the API (I've suggested to the Akka maintainers that this vestige be dropped in 2.7 (which is the absolute earliest it could be dropped owing to binary compatibility); communications with some of them haven't yielded a reason for this distinction and there's no such distinction in the analogous Scala API).

    Disclaimer: I tend to work exclusively in the Scala functional API for actor definition.

    There's a duality in the actor model between the state of an actor (i.e. its fields) and its behavior (how it responds to the next message it receives): to the world outside the actor, they are one and the same because the only way (ignoring something like taking a heap dump) to observe the actor's state is to observe its response to a message. Of course, in fact, the current behavior is a field in the runtime representation of the actor, and in the functional definitions, the behavior generally has fields for state.

    The OO style of behavior definition favors:

    • mutable fields in the actor
    • the current behavior being immutable (the behavior can make decisions based on the fields)

    The functional style of behavior definition favors:

    • no mutable fields in the actor
    • updating the behavior (which is an implicitly mutable field)

    (the distinction is analogous to imperative programming having a while/for loop where a variable is updated vs. functional programming's preference for defining a recursive function which the compiler turns behind the scenes into a loop).

    The AbstractBehavior API seems to assume that the message handler is createReceive(): returning this within an AbstractBehavior means to go back to the createReceive(). Conversely, Behaviors.same() in the functional style means "whatever the current behavior is, don't change it". Where there are multiple sub-Behaviors/Receives in one AbstractBehavior, this difference is important (it's not important when there's one Receive in the AbstractBehavior).

    TL;DR: if defining multiple message handlers in an AbstractBehavior, prefer return Behaviors.same to return this in message handlers. Alternatively: only define one message handler per AbstractBehavior.