Let’s take a closer look at our actors, how their life looks like, what exactly they’ve been doing and what they can do. As they are basic building block of applications built on actor model it’s crucial to understand and being able to communicate with them.

Actor Types

Most basic actors that we’ll build will derive from ReceiveActor class which derives from UntypedActor class. And that’s how we create our actor class, we just derive new class from one of these two types, our own abstract class with preimplemented events (like creating and storing logger) or some extended actor types like PersistentActor which I’ll cover in one of upcomin posts.

UntypedActor is simplest of actors, except ActorBase which we’ll probably never use in our own code since it’s just basic concept of an actor wrapped in abstract class. It doesn’t offer us much more that OnReceive method that accept message of type object as a parameter, hence the name “Untyped”. While it’s a bit more performant than ReceiveActor it doesn’t give us much out of the box so we’ll probably won’t use it in our code, especially if we won’t write our own ActorTypes.

ReceiveActor are giving us much more functionality out of the box and we’ll focus on them in this post. First of all and probably most importantly, we have access to typed Receive methods which allows us to handle received messages based on their types. Using Receive method is really simple and requires type of message as generic parameter and passing some form of handler in form of Action<T>.

public UserActor() { //... Receive<UserLoggedInMessage>(msg => HandleUserLoggedInMessage(msg)); Receive<AddSubscribedTagCommand>(msg => HandleAddSubscribedTagCommand(msg)); Receive<AddContentCommand>(msg => HandleAddContentCommand(msg)); } 1 2 3 4 5 6 7 8 9 public UserActor ( ) { //... Receive < UserLoggedInMessage > ( msg = > HandleUserLoggedInMessage ( msg ) ) ; Receive < AddSubscribedTagCommand > ( msg = > HandleAddSubscribedTagCommand ( msg ) ) ; Receive < AddContentCommand > ( msg = > HandleAddContentCommand ( msg ) ) ; }

We can also provide some Predicate<T> to indicate that this handler should be used to handle specific methods. In this case Receive method with predicate must be placed before other methods handling same type of message. It’s because handlers are prioritized by order in which you’ll insert them in your code and if one of them handles message, next one will not be invoked.

Receive<UserLoggedInMessage>( msg => HandleFacebookUserLoggedInMessage(msg), msg => msg.AuthenthicationType == "Facebook"); Receive<UserLoggedInMessage>(msg => HandleOtherUserLoggedInMessage(msg)); 1 2 3 4 5 Receive < UserLoggedInMessage > ( msg = > HandleFacebookUserLoggedInMessage ( msg ) , msg = > msg . AuthenthicationType == "Facebook" ) ; Receive < UserLoggedInMessage > ( msg = > HandleOtherUserLoggedInMessage ( msg ) ) ;

Receive methods doesn’t return anything at all, they sole purpose is to notify actor about something and provide necessary data to handle what comes next. There are also async overloads of receive methods and non-generic ones that requires passing of Type parameter.

Actor Lifecycle

Actors are often objects with pretty long lifetime but still they need to be created and someday they will be stopped or restarted. There are some moments in actor lifecycle when we can invoke some code and ie. log some info about creation, error or restart of an actor. Main events we can override are PostRestart, PostStop, PreRestart and PreStart.

First event ever is PreStart and there are some scenarios where people are doing initialization inside of this event. Why? Because actor can be restarted and then it’s constructor will be invoked and in case of restarting an actor you can skip PreStart event if you’ll override PostRestart method without invoking base.PostRestart (it calls to PreStart by default). Second event, executed only after actor restarting as first event in lifecycle, is PostRestart. Name is self explanatory.

Restarts are triggered mainly by parent actor supervision strategy which we will cover in post regarding actor hierarchies and handling errors. What you need to know now about restarts is that actor can be restarted and stopped entirely. And each of this event have it’s own method to override: PostStop and PreRestart.

protected override void PostRestart(Exception reason) { //Base implementation invokes PreStart Logger.Log(LogLevel.InfoLevel, $"UserActor {ActorState.UserName} has restarted due to {reason.Message}"); } protected override void PreStart() { //Called before starting Actor, during restart you can skip it by overriding PostRestart Logger.Log(LogLevel.InfoLevel, $"Starting UserActor {ActorState.UserName}"); } protected override void PreRestart(Exception reason, object message) { Logger.Log(LogLevel.InfoLevel, $"UserActor {ActorState.UserName} will restart in a moment due to {reason.Message}"); } protected override void PostStop() { Logger.Log(LogLevel.InfoLevel, $"UserActor {ActorState.UserName} has been stopped"); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 protected override void PostRestart ( Exception reason ) { //Base implementation invokes PreStart Logger . Log ( LogLevel . InfoLevel , $ "UserActor {ActorState.UserName} has restarted due to {reason.Message}" ) ; } protected override void PreStart ( ) { //Called before starting Actor, during restart you can skip it by overriding PostRestart Logger . Log ( LogLevel . InfoLevel , $ "Starting UserActor {ActorState.UserName}" ) ; } protected override void PreRestart ( Exception reason , object message ) { Logger . Log ( LogLevel . InfoLevel , $ "UserActor {ActorState.UserName} will restart in a moment due to {reason.Message}" ) ; } protected override void PostStop ( ) { Logger . Log ( LogLevel . InfoLevel , $ "UserActor {ActorState.UserName} has been stopped" ) ; }

Actor lifecycle events can be used for logging, notifying other actors about events or persisting/restoring state if you’re not using Akka.Persist library. It’s another tool in your box that you should know about.

Context

We have HttpContext, DbContext, PrincipalContext and others. Why would you think we won’t have ActorContext?

In fact you will work with the Context quite often. It can provide you reference (IActorRef) to itself, children, parent and even ActorSystem. It provides methods as children selector by name, creating child actors and IActorRef to sender of message you’re handling currently. Context is really important and allows you to access current state of your actor in relation to other actors in current time like mentioned Sender or child Actors. Many things you could do with it will be covered in upcoming, more detailed posts.

What actor should do?

We know basic tools we can use inside our actor, let’s focus what we should do with them. First of all, we should interact with other actors only by messages and we should not change messages contents. We will focus on that in post about them but remember that messages should be immutable, mostly they are just containers and we need their getters for anything we’ll do inside of our actors.

But what exactly we should do? Well, we can process something or change our actor state, we can create child actors and send messages to any other actor inside our system (or remote ones) without having direct reference to them (we’ll cover that in next post). But there is one very important matter – our operations should be as tiny as possible.

You should know from previous posts that actor processes one message at a time. While we can have hundres of thousands and even millions of actors at the same time, every one of them can process one message at a time. It give us A LOT of concurrent operations. If they are rather small things there is no problem. However if we try to do some heavylifting on that scale our application will not perform, we’ll receive a lot of timeouts and things could not work as intended.

But what if you really need to do some heavyweight operations? Well you can move part of your application to remote service with Akka.Remote, you can also create one actor or router with few child actors that will process expensive operations from entire applications in controlled environment (routers can distribute messages equally between configured ammount of children) . You can also delegate something to Azure Functions or other FaaS as I intend to do.

Just keep everything as lightweight and granular as possible, don’t block threads for too long and remeber that with actors saying “strength in numbers” is a way to go.

What I haven’t covered?

There are a lot of things that I’ve skipped and I’ll cover in near future. Truth is I could mention ie. switchable behaviors but this feature is a bit more complex than Receive method and as such it deserves dedicated blog post. Most notable mentions that I’ve skipped and will write about soon are:

Switchable behaviors

Routers

Remote Actors

Persisting Actors

Error handling and supervising strategy

Mailboxes

Returning results from an Actor

Other actor types that come with some packages (ie. PersistingActor)

Later this week I should publish post about actor references and paths so stay tuned!