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()
:
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 Become
s Authenticated
:
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 useUnbecome
.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 Unbecome
s:
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 orContext.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
.
- Read more about:
- Akka.NET
- Case Studies
- Videos
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.