Today we’re going to learn about one of the really cool things actors can do: change their behavior at run-time!

This capability allows you to do all sorts of cool stuff, like build Finite State Machines (FSM) or change how your actors handle messages based on other messages they’ve received.

Today, we’re going to cover how to make a basic FSM using switchable behavior. We’ll go over advanced FSM approaches in a future post.

Let’s start with a real-world scenario in which you’d want the ability to change an actor’s behavior.

Real-world Scenario: Authentication

Imagine you’re building a simple chat system using Akka.NET actors, and here’s what your UserActor looks like - this is the actor that is responsible for all communication to and from a specific human user.

public class UserActor : ReceiveActor {
    private readonly string _userId;
    private readonly string _chatRoomId;

    public UserActor(string userId, string chatRoomId) {
        _userId = userId;
        _chatRoomId = chatRoomId;
        Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // print message for user
            });
        Receive<OutgoingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // send message to chatroom
            });
    }
}

So we have basic chat working - yay! But… right now there’s nothing to guarantee that this user is who they say they are. This system needs some authentication.

How could we rewrite this actor to handle these same types of chat messages differently when:

  • The user is authenticating
  • The user is authenticated, or
  • The user couldn’t authenticate?

Simple: we can use switchable actor behaviors to do this!

What Is Switchable Behavior?

One of the core attributes of an actor in the Actor Model is that an actor can change its behavior between messages that it processes.

Switchable behavior is one of the most powerful and fundamental capabilities of any true actor system. It’s one of the key features enabling actor reusability, and helping you to do a massive amount of work with a very small code footprint.

How does switchable behavior work?

The Behavior Stack

Akka.NET actors have the concept of a “behavior stack”. Whichever method sits at the top of the behavior stack defines the actor’s current behavior. Currently, that behavior is Authenticating():

Initial Behavior Stack for UserActor

Use Become and BecomeStacked to Adopt a New Behavior

Whenever we call Become, we tell the ReceiveActor (docs) to change to a new behavior (not preservingthe behavior stack). To change behavior and preserve the previous behavior stack, call BecomeStacked (note: this API changed in the v1.0 release).

This new behavior dictates which Receive methods will be used to process any messages delivered to an actor.

Here’s what happens to the behavior stack when our example actor Becomes Authenticated:

Become Authenticated - push a new behavior onto the stack

NOTE: By default, Become will delete the old behavior off of the stack - so the stack will never have more than one behavior in it at a time. This is because most Akka.NET users don’t use Unbecome.

To preserve the previous behavior on the stack, call Become(Method(), false)

Use Unbecome to Revert to Old Behavior

To make an actor revert to the previous behavior, all we have to do is call Unbecome.

Whenever we call Unbecome, we pop our current behavior off of the stack and replace it with the previous behavior from before (again, this new behavior will dictate which Receive methods are used to handle incoming messages).

Here’s what happens to the behavior stack when our example actor Unbecomes:

Unbecome - pop the current behavior off of the stack

Isn’t It Problematic For Actors to Change Behaviors?

No, actually it’s safe and is a feature that gives your ActorSystem a ton of flexibility and code reuse.

Here are some common questions about switchable behavior:

When Is the New Behavior Applied?

We can safely switch actor message-processing behavior because Akka.NET actors only process one message at a time. The new message processing behavior won’t be applied until the next message arrives.

How Deep Can the Behavior Stack Go?

The stack can go really deep, but it’s not unlimited.

Also, each time your actor restarts, the behavior stack is cleared and the actor starts from the initial behavior you’ve coded.

What Happens If You Call Unbecome With Nothing Left In the Behavior Stack?

The answer is: nothing - Unbecome is a safe method and won’t do anything unless there’s more than one behavior in the stack.

Back To the Real-World Example

Okay, now that you understand switchable behavior, let’s return to our real-world scenario and see how it is used. Recall that we need to add authentication to our chat system actor.

So, how could we rewrite this actor to handle chat messages differently when:

  • The user is authenticating
  • The user is authenticated, or
  • The user couldn’t authenticate?

Here’s one way we can implement switchable message behavior in our UserActor to handle basic authentication:

public class UserActor : ReceiveActor {
    private readonly string _userId;
    private readonly string _chatRoomId;

    public UserActor(string userId, string chatRoomId) {
        _userId = userId;
        _chatRoomId = chatRoomId;

        // start with the Authenticating behavior
        Authenticating();
    }

    protected override void PreStart() {
        // start the authentication process for this user
        Context.ActorSelection("/user/authenticator/")
            .Tell(new AuthenticatePlease(_userId));
    }

    private void Authenticating() {
        Receive<AuthenticationSuccess>(auth => {
            Become(Authenticated); //switch behavior to Authenticated
        });
        Receive<AuthenticationFailure>(auth => {
            Become(Unauthenticated); //switch behavior to Unauthenticated
        });
        Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // can't accept message yet - not auth'd
            });
        Receive<OutgoingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // can't send message yet - not auth'd
            });
    }

    private void Unauthenticated() {
        //switch to Authenticating
        Receive<RetryAuthentication>(retry => Become(Authenticating));
        Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // have to reject message - auth failed
            });
        Receive<OutgoingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // have to reject message - auth failed
            });
    }

    private void Authenticated() {
        Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // print message for user
            });
        Receive<OutgoingMessage>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                // send message to chatroom
            });
    }
}

Whoa! What’s all this stuff? Let’s review it.

First, we took the Receive<T> handlers defined on our ReceiveActor and moved them into three separate methods. Each of these methods represents a state that will control how the actor processes messages:

  • Authenticating(): this behavior is used to process messages when the user is attempting to authenticate (initial behavior).
  • Authenticated(): this behavior is used to process messages when the authentication operation is successful; and,
  • Unauthenticated(): this behavior is used to process messages when the authentication operation fails.

We called Authenticating() from the constructor, so our actor began in the Authenticating() state.

This means that only the Receive<T> handlers defined in the Authenticating() method will be used to process messages (initially).

However, if we receive a message of type AuthenticationSuccess or AuthenticationFailure, we use the Become method (docs) to switch behaviors to either Authenticated or Unauthenticated, respectively.

Can I Switch Behaviors In An UntypedActor?

Yes, but the syntax is a little different inside an UntypedActor. To switch behaviors in an UntypedActor, you have to access Become and Unbecome via the ActorContext, instead of calling them directly.

These are the API calls inside an UntypedActor:

  • Context.Become - switches to new behavior, but does not preserve the behavior stack.
  • Context.BecomeStacked(Receive rec, bool discardPrevious = true) - pushes a new behavior on the stack or
  • Context.UnbecomeStacked() - pops the current behavior and switches to the previous (if applicable.)

The first argument to Context.Become is a Receive delegate, which is really any method with the following signature:

void MethodName(object someParameterName);

This delegate is just used to represent another method in the actor that receives a message and represents the new behavior state.

Here’s an example (OtherBehavior is the Receive delegate):

public class MyActor : UntypedActor {
    protected override void OnReceive(object message) {
        if(message is SwitchMe) {
            // preserve the previous behavior on the stack
            Context.Become(OtherBehavior, false);
        }
    }

    // OtherBehavior is a Receive delegate
    private void OtherBehavior(object message) {
        if(message is SwitchMeBack) {
            // switch back to previous behavior on the stack
            Context.Unbecome();
        }
    }
}

Aside from those syntactical differences, behavior switching works exactly the same way across both UntypedActor and ReceiveActor.

If you liked this post, you can share it with your followers or follow us on Twitter!
Written by Aaron Stannard on March 5, 2015

 

 

Observe and Monitor Your Akka.NET Applications with Phobos

Did you know that Phobos can automatically instrument your Akka.NET applications with OpenTelemetry?

Click here to learn more.