Akka.NET - Introduction to the actor model

Akka.NET - Introduction to the actor model

The life of a consultant is an exciting one! We are not only learning from past technologies and implementing current state of the art technologies. Occasionally we dabble with future (sometimes visionary) technologies and paradigms. Akka.NET is one of those technologies that can be found in all those time periods starting from the theoretical paper in 1973 to well into the foreseeable future.

What is Akka.NET

The official website of Akka.NET states that Akka.NET is a toolkit and runtime for building highly concurrent, distributed, and fault tolerant event-driven applications on .NET & Mono.” So, a lot of fancy words… But is it all they claim? To be frank, it is, and much more! Akka.NET was build asynchronously and distributed from the ground up. All interactions are based on passing messages between different Actors. An actor can be described as a high-level abstraction of well… almost everything. Its responsibility is to handle incoming messages and handle them accordingly or more explicitly as you program them to do. Because of the event-driven nature and the possibility to create finite state machines, you can create resilient, self-healing applications more easily.

Actor_Model_Communication_Diagram.png

This blog post was created to show you how easy it is to setup a basic project with Akka.NET. The focus will be on the practicality of the base setup and where to go from there. The example files are available in different stages at Github

Let us dive in

Setting up the actor system

First things first, let us create a new project. For this example, we will be creating a console application because we all miss those days staring at a black screen with white or green text. I will be using .Net Core but full .Net will work just the same. I will be naming this application IntroductionToAkka

Step_001_Create_Project.png

The next step is adding the Akka.Net NuGet package either through the NuGet manager or by executing the following command in the package manager console: Install-Package Akka This package contains the core functionality to handle messages, create mailboxes (queues) and configure Actors. Next, we will create an Actor system. So open up Program.cs and add the following statements:

using Akka.Actor;
using IntroductionToAkka.Helpers;
using System;

namespace IntroductionToAkka
{
    class Program
    {
        private static ActorSystem StreamingActorSystem;

        static void Main(string[] args)
        {
            // Create the actor system with the name StreamingActorSystem
            StreamingActorSystem = ActorSystem.Create(nameof(StreamingActorSystem));
            ConsoleHelper.WriteColoredLine("Actor system created.");
            // Terminate the actor system
            (StreamingActorSystem.Terminate()).GetAwaiter().GetResult();
            Console.WriteLine("Actor system terminated");
            Console.ReadLine();
        }
    }
}

The code above will create a root ActorSystem that will manage the lifetime of our Actors and will deal with communication between them. We also added the command for terminating the ActorSystem, although not necessary, it is best practice. Akka.Net has the possibility to persist state/messages on shutdown or let Actors finish their current queue before terminating. Triggering the Terminate command handles all things properly without giving it a second thought.

Additionally we used a ConsoleHelper to color our messages. We will implement this little helper in a moment. First add these folders to the root of the application

  • Actors

  • Helpers

  • Messages

Next create a new class named ConsoleHelper that is responsible for adding some color to our different type of messages. So go and add the code listed below:

using System;

namespace IntroductionToAkka.Helpers
{
    public class ConsoleHelper
    {
        public static void WriteColoredLine(string message, ConsoleColor color = ConsoleColor.White)
        {
            // Save the previous color
            var previousColor = Console.ForegroundColor;
            // Change the current console text color
            Console.ForegroundColor = color;
            Console.WriteLine(message);
            // Reset the color back to the previous
            Console.ForegroundColor = previousColor;
        }
    }
}

Adding your first Actor

To further our journey we will create a new PlaybackActor class in the Actors folder. Replace the contents with the snippet below:

using Akka.Actor;
using IntroductionToAkka.Helpers;
using IntroductionToAkka.Messages;
using System;

namespace IntroductionToAkka.Actors
{
    public class PlaybackActor : UntypedActor
    {
        private string _currentlyStreaming;

        public PlaybackActor()
        {

        }

        protected override void OnReceive(object message)
        {
            switch (message)
            {
                case PlayMessage m:
                    Play(m);
                    break;
                case StopMessage m:
                    Stop(m);
                    break;
                default:
                    Unhandled(message);
                    break;
            }
        }

        private void Play(PlayMessage item)
        {
            if (String.IsNullOrWhiteSpace(_currentlyStreaming))
            {
                ConsoleHelper.WriteColoredLine($"PlayMessage received, playing movie: {item.Title}", ConsoleColor.DarkGreen);
                this._currentlyStreaming = item.Title;
            }
            else
            {
                ConsoleHelper.WriteColoredLine($"ERROR: cannot start playing movie while another is playing", ConsoleColor.DarkGreen);
            }
        }


        private void Stop(StopMessage item)
        {
            if (String.IsNullOrWhiteSpace(_currentlyStreaming))
            {
                ConsoleHelper.WriteColoredLine($"ERROR: there was no movie playing", ConsoleColor.DarkGreen);
            }
            else
            {
                ConsoleHelper.WriteColoredLine($"StopMessage received, stopping movie", ConsoleColor.DarkGreen);
                this._currentlyStreaming = String.Empty;
            }
        }
    }
}

One thing you will notice is that our class is inheriting from UntypedActor. By inheriting from UntypedActor we also need to implement the OnReceivemethod. When a message comes in it will be handled by the OnReceive method and handled accordingly. Congratulations, we now have our base Actor setup!

The incoming message parameter is of the object type, this makes it ideal for pattern matching as introduced in C# 7.0. In our switch statement we can handle different types of messages and trigger custom actions or use Akka's built in Unhandled() method. By using Unhandled we can configure Akka to put them on the Actor's event stream and transform these messages to debug messages.

Communicating through messages

We already implemented the code for the Actor and defined actions based on 2 types of messages, the PlayMessage and the StopMessage. To further our example we will create classes for both and add them to the Messages folder.

PlayMessage.cs

namespace IntroductionToAkka.Messages
{
    public class PlayMessage
    {
        public string Title { get; private set; }
        public string Username { get; private set; }

        public PlayMessage(string title, string username)
        {
            Title = title;
            Username = username;
        }
    }
}

StopMessage.cs

namespace IntroductionToAkka.Messages
{
    public class StopMessage
    {
        public string Username { get; private set; }

        public StopMessage(string username)
        {
            Username = username;
        }
    }
}

Now let us talk

We have configured our Actor and created our messages, now it's time to get talking to our Actor. To to this we need to actually create the actor and tell him what to do. Open up Program.cs and paste in this content:

using Akka.Actor;
using IntroductionToAkka.Actors;
using IntroductionToAkka.Helpers;
using IntroductionToAkka.Messages;
using System;

namespace IntroductionToAkka
{
    class Program
    {
        private static ActorSystem StreamingActorSystem;

        static void Main(string[] args)
        {
            ...
            ConsoleHelper.WriteColoredLine("Actor system created.");

            // Create the playbackActor
            Props playbackActorProps = Props.Create<PlaybackActor>();
            IActorRef playbackActorRef = StreamingActorSystem.ActorOf(playbackActorProps, nameof(PlaybackActor));

            var user = "Timmy";
            var movie1 = "North Park - The Movie";
            var movie2 = "Eastworld";

            // Start playing movie1
            ConsoleHelper.WriteColoredLine($"Sending PlayMessage for {movie1}", ConsoleColor.Cyan);
            playbackActorRef.Tell(new PlayMessage(movie1, user));
            Console.ReadLine();

            // Try playing movie2 while movie1 is playing
            ConsoleHelper.WriteColoredLine($"Sending PlayMessage for {movie2}", ConsoleColor.Cyan);
            playbackActorRef.Tell(new PlayMessage(movie2, user));
            Console.ReadLine();

            // Stop the current movie
            ConsoleHelper.WriteColoredLine($"Sending StopMessage for {user}", ConsoleColor.Red);
            playbackActorRef.Tell(new StopMessage(user));
            Console.ReadLine();

            // Try stopping the movie when no movie is playing
            ConsoleHelper.WriteColoredLine($"Sending StopMessage for {user}", ConsoleColor.Red);
            playbackActorRef.Tell(new StopMessage(user));
            Console.ReadLine();

            // Start playing movie1 again
            ConsoleHelper.WriteColoredLine($"Sending PlayMessage for {movie1}", ConsoleColor.Cyan);
            playbackActorRef.Tell(new PlayMessage(movie1, user));
            Console.ReadLine();

            // Kill the playbackActor
            ConsoleHelper.WriteColoredLine("Killing the UserActor", ConsoleColor.Cyan);
            playbackActorRef.Tell(PoisonPill.Instance);
            Console.ReadLine();

            // Try sending message after killing actor
            ConsoleHelper.WriteColoredLine($"Sending PlayMessage for {movie1}", ConsoleColor.Cyan);
            playbackActorRef.Tell(new PlayMessage(movie1, user));
            Console.ReadLine();

            // Terminate the actor system
            ...
        }
    }
}

Previous code adds a whole scenario to our program. Instead of just adding 1 message, we will be sending lots of messages. Because let's face it, this is what the Actor Model is all about.

The first thing we did was create the actor and add it to our ActorSystem.

// Create the playbackActor
Props playbackActorProps = Props.Create<PlaybackActor>();
IActorRef playbackActorRef = StreamingActorSystem.ActorOf(playbackActorProps, nameof(PlaybackActor));

Props.Create is an Akka.NET method for creating the Actor recipe and optionally adding additional configuration to it. Afterwards we told the ActorSystem to use its factory method ActorOf to create an Actor based on the created recipe by handing over the recipe and choosing a name.

We scripted our scenario to start watching a movie, see what happens if we try to start another movie will playing another and so on. These actions will be executed by creating a message and passing it to our Actor reference by using the Tell method. Example below:

playbackActorRef.Tell(new PlayMessage(movie1, user));

But what if we want to stop our Actor from receiving messages? Well we it sounds like something in a crime scene but we just send a poison pill.

playbackActorRef.Tell(PoisonPill.Instance);

If everything was setup correctly, build the project and start our application.

Step_002_Output_Window.png

Don't be a star, share the stage

Time to spices thing up a bit and add another actor that will keep count how many times a movie was played.

Create a new IncrementPlayCountMessage.cs file in the Messages folder:

namespace IntroductionToAkka.Messages
{
    public class IncrementPlayCountMessage
    {
        public string Title { get; private set; }

        public IncrementPlayCountMessage(string title)
        {
            Title = title;
        }
    }
}

Add this code to Program.cs

...
ConsoleHelper.WriteColoredLine("Actor system created.");

// Create the counterActor
Props counterActorProps = Props.Create<CounterActor>();
IActorRef counterActorRef = StreamingActorSystem.ActorOf(counterActorProps, nameof(CounterActor));

// Create the playbackActor
...

Create a new CounterActor.cs file in the Actors folder:

using Akka.Actor;
using IntroductionToAkka.Helpers;
using IntroductionToAkka.Messages;
using System;
using System.Collections.Generic;

namespace IntroductionToAkka.Actors
{
    public class CounterActor : ReceiveActor
    {
        private readonly Dictionary _playCounts;

        public CounterActor()
        {
            _playCounts = new Dictionary();

            Receive(msg => HandleIncrementMessage(msg));
        }

        public void HandleIncrementMessage(IncrementPlayCountMessage msg)
        {
            if(_playCounts.ContainsKey(msg.Title))
            {
                _playCounts[msg.Title]++;
            }
            else
            {
                _playCounts.Add(msg.Title, 1);
            }

            ConsoleHelper.WriteColoredLine($"Movie {msg.Title} was played {_playCounts[msg.Title]} time(s)", ConsoleColor.Gray);
        }
    }
}

What happened here? Instead of UntypedActor we inherited from ReceiveActor. Now we do not need to implement the OnReceive method and get message typing out of the box! In our constructor we defined that we want to receive a message from the IncrementPlayCountMessage type. We also specified which method to call when a message of this type has been received. By inheriting from ReceiveActor we also do not need to specifiy the Unhandled method anymore, Akka.NET will take care of this. It is a lot cleaner to use the ReceiveActor, it also gives us a lot more options that we will not go into right now.

Open up PlaybackActor.cs and change the code as stated below:

...
private void Play(PlayMessage item)
{
    if (String.IsNullOrWhiteSpace(_currentlyStreaming))
    {
        ConsoleHelper.WriteColoredLine($"PlayMessage received, playing movie: {item.Title}", ConsoleColor.DarkGreen);
        this._currentlyStreaming = item.Title;

        Context.ActorSelection("/user/CounterActor")
            .Tell(new IncrementPlayCountMessage(item.Title));
    }
    else
    ...
}
...

If PlaybackActor receives a message to start playing a movie and there is no movie currently playing, there will be a message sent to the CounterActor by executing:

Context.ActorSelection("/user/CounterActor")
    .Tell(new IncrementPlayCountMessage(item.Title));

This command will tell the current ActorySystem to look for an Actor found under the root (/user) that is named CounterActor and send this a message to his mailbox for handling. Start the project to check if everything is wired up alright.

Step_003_Final_Output_Window.png

Conclusion

As you have seen, Akka.NET is real easy to get started. It shields us developers of dealing with things like scaling and concurrency if we want to, or give us control when we need to. There are a lot more things to explore in Akka.NET and a lot of powerful features to discover. In times of Microservices and distributed computing, Akka.NET really shines and shows us that things do not always have to be hard.