An Actor model example with Akka.NET

I have long been interested in designing distributed applications using the Actor model, taking a special interest in both Erlang, and Project Orleans for .NET.

The introduction of Akka.NET seems to hold a lot of promise, so with V1 just around the corner I wanted to get my hands on it and give it a try.

What is Akka.NET and the Actor model?

Akka.NET is a port of the popular JVM-based Akka toolkit for building applications using the Actor model.

If you are not familiar with the Actor model I suggest taking a look at one of these resources.

The Actor model is about being able to create scalable and distributed systems by abstracting away some of the horrors of concurrent computing like threading and shared state.

An Actor is a lightweight entity consisting of state and behaviour that will only communicate with its surroundings using message passing. Messages arrive to the Actor and will be processed asynchronously one by one.

Rather than having large and complicated actors we strive to break the functionality of the application into a hierarchy of small single-purpose actors, where actors higher in the hierarchy delegate to and supervise child actors.

Illustration of the Actor model

The purpose of this post

Akka.NET provides you with the tools needed to write distributed high-performance resilient applications using the Actor model. This post however is not about advanced features, but rather getting a simple application going to try out the basics of Akka.NET.

Since this is my first attempt at getting an application running using Akka.NET there might be cases of "you are holding it wrong", so take it with a grain of salt, and I will try to correct any mistakes as soon as I learn about them.

  • UPDATE: I have revisited the code and reimplemented it using a more functional approach. After reading this, you should check out the blog post I wrote about the update.

The example is an F# console application, that I have put together with a lot of help from these resources.

You can find the complete project on my Github account. Without further ado.

Introducing AkkaFlix

AkkaFlix is a pretend online video streaming company who needs a robust and scalable backend system. Mainly because of the coincidental similarity in names they have turned to Akka.NET for building the backend which needs to handle a variety of business critical tasks.

AkkaFlix Logo

It all starts with a "Play"-event that is sent to the backend every time one of their users starts streaming a video. The event holds two pieces information, the "User" (username) and the "Asset" (name of the video).

type PlayEvent = 
    { User: string
      Asset: string }

The play events are rich in information and can be used for a variety of purposes. AkkaFlix wants to use the events flowing through the system to achieve the following.

  • Keep track of how many people are streaming, for statistics
  • Count how many times the individual videos have been streamed, for reporting to content owners.
  • Keep track of what the individual user is currently watching, for the "You are currently watching" feature in the user interface.

In order to achieve this we will create a hierarcy of actors that look something like this.

AkkaFlix Actor based model

There will be one Root-actor (under the Akka.NET /user root) of type "Player", which will be responsible for receiving Play-events, and passing them on to two child actors, "Users" and "Reporting".

The "Users"-actor spawns an individual "User"-actor for each unique user it encounters. It then messages every subsequent Play-event to the correct "User"-actor.

When the backend is running we have a model consisting of a hierarchy of actors who each serve a single responsibility. The model is continuously updated as messages flow through the system.

  • "Reporting" keeps a list of all encountered assets, tracking how many times it has been watched.
  • "Users" keeps track of all users, knowing who have been streaming.
  • "User"-actors keep the current state for a single user, keeping "currently watching" up to date.

Lets look at the code

The entry point for the AkkaFlix backend creates an Akka.NET system for hosting actors. Actors can be spawned either by Akka.NET "systems", or be actors themselves.

open System
open Akka.Actor
open Akka.FSharp

module AkkaFlix =       
    let users = [| "Jack"; "Jill"; "Tom"; "Jane"; "Steven"; "Jackie" |]
    let assets = [| "The Sting"; "The Italian Job"; "Lock, Stock and Two Smoking Barrels"; "Inside Man"; "Ronin" |]
    let rnd = System.Random()

    // Send a Play-event with a randomly selected user and asset to the Player-actor.
    // Continue sending every time a key other than [Esc] is pressed.
    let rec loop player =            
        player <! { User = users.[rnd.Next(users.Length)] ; Asset = assets.[rnd.Next(assets.Length)] }    
        match Console.ReadKey().Key with
        | ConsoleKey.Escape -> ()
        | _ -> loop player       

    // Create an actor system and the root Player-actor and pass it to the
    // input loop to start sending Play-events to it.
    [<EntryPoint>]
    let main argv =    
        let system = System.create "akkaflix" (Configuration.load())
        let player = system.ActorOf(Props(typedefof<Player>, Array.empty))

        loop player

        system.Shutdown()
        0

After creating the system we immediately spawn a single actor of type "Player", which we pass to an input loop. The input loop will continue to generate a random Play-event and send it to Player-actor every time a key other than [Esc] is pressed in the console.

The Player Actor

The Player inherits from Akka.FSharp.Actors.Actor and overrides the message handler. Upon creation it also spawns two child actors of type "Player" and "Reporting".

type Player() = 
    inherit Actor()

    // Init child actors
    let player = Player.Context.ActorOf(Props(typedefof<Users>, Array.empty))
    let reporting = Player.Context.ActorOf(Props(typedefof<Reporting>, Array.empty))

    // Pass the Play-events on to child actors
    let notify event =
        player <! event
        reporting <! event

    // Incoming message handler
    override x.OnReceive message =
        match message with
        | :? PlayEvent as event -> notify event
        | _ ->  failwith "Unknown message"

When a PlayEvent message is received the Player passes it on to its two child actors.

The Reporting Actor

This actor keeps a dictionary for storing a counter for how many times each of the video assets have been played. When a message arrives the actor registers the view by incrementing the counter for the Asset (or creating a new counter if the asset is not yet registered).

type Reporting() =
    inherit Actor()

    let counters = new Dictionary<string, int>();

    // Increment the view count for an asset, or create a new
    // counter if the asset is viewed for the first time.
    let registerView asset =
        match counters.ContainsKey(asset) with
        | true -> counters.[asset] <- counters.[asset] + 1
        | false -> counters.Add(asset, 1)

    let printReport h =
        h
        |> Seq.sortBy (fun (KeyValue(k, v)) -> -v) 
        |> Seq.iter (fun (KeyValue(k, v)) -> printfn "%d\t%s" v k)

    // Incoming message handler
    override x.OnReceive message =
        match message with
        | :? PlayEvent as event -> 
            registerView event.Asset
            printReport counters
        | _ ->  failwith "Unknown message"

In the first version of the backend AkkaFlix decided that having the counters printed to the console every time an update occurs would be sufficient for reporting. So after the counter has been incremented, the updated statistics for all assets are printed to the console.

The Users Actor

When a message arrives to the Users actor, it will look up the child actor for the User provided in the Play-event. If the child actor for the user does not yet exist, one will be spawned. After finding the correct child actor, the Asset is sent as a message to it, so it can update its state accordingly.

type Users() = 
    inherit Actor()

    let context = Users.Context
    let users = new Dictionary<string, ActorRef>();

    // Return the User-actor identified by username
    // If none is found, create a new User-actor
    let rec findOrSpawn username =
        match users.ContainsKey(username) with
        | true -> users.[username]
        | false ->
            users.Add(username, context.ActorOf(Props(typedefof<User>, [| username :> obj |])))
            findOrSpawn username

    // Get the actor for a specific user and message the asset to it
    let updateUser user asset =
        (findOrSpawn user) <! asset

    // Incoming message handler
    override x.OnReceive message =
        match message with
        | :? PlayEvent as event -> 
            updateUser event.User event.Asset
            printfn "Unique users: %d" users.Count 
        | _ ->  failwith "Unknown message"

Every time a message is handled, the current count of unique users is also reported by writing it to the console.

The User Actor

The last actor we want to look at is the User. It is spawned by the "Users" actor, and takes a username as a constructor argument. When it receives a message containing an asset name, it updates it internal state to reflect what the user is watching.

type User(user) =
    inherit Actor()

    let user = user
    let mutable watching = null

    // Incoming message handler
    override x.OnReceive message =
        match message with
        | :? string as asset -> 
            watching <- asset
            printfn "%s is watching %s" user watching
        | _ ->  failwith "Unknown message"

Every time a message is handled it also reports what the user is watching by writing it to the console.

Running the backend

When we run the backend and allow messages to flow through the hierarchy, the model is updated as can be seen from this screenshot.

Running AkkaFlix

Notice the random order in which the messages arrive in the console? This illustrates the concurrent nature of the Actor model. Our system is updated only through message passing, with no guarantee of which Actor will be quickest to update and report to the console.

Again, if you want to try running it locally, go grab the code at https://github.com/clausasbjorn/AkkaFlix

What is next for AkkaFlix

If AkkaFlix is serious about using Akka.NET for their backend they should probably check features like persistence and running the backend remotely on a cluster of machines rather than locally as a console app. Maybe that's an idea for another blog post.

View Comments