Actors and Messages: The Building Blocks of Akka.NET

The Actor Model is programming paradigm that is well suited to working in the world of asynchronous, distributed applications. As I described in a previous article, the Actor Model is message driven architecture in which every entity within the framework is an actor. Work gets done by message passing between actors. An actor receives a message and reacts to it by doing something or by passing another message onto another actor to do work the first actor can’t or won’t do.

AKKA1

Actors created by ActorSystem are top level actors. Actors are objects that inherit from Akka.NET’s UntypedActor. Top level Actors create children Actors. In our little Stock Trading example, the StockBroker actor is a top level actor of the TradingSystem. The FloorTrader is a child of the StockBroker actor. Actors supervise their children. Supervision strategies are an important part of the Akka.NET Actor Model and will be discussed in future articles. For now, the important thing to understand is that once an Actor creates another Actor, that subsequent Actor is a child.

Communicating with Messages

Now that we’ve covered ActorSystem and Actors, let’s take a look at the concept of a message as it applies to Akka.NET. In Akka.NET, a message is a Plain Old CSharp Object (POCO), a class. (The strong type nature of a Message is an essential concept of Akka.NET.) For example, the POCO shown in Listing 1 can be a message:

public namespace Messages
{
   public class AlarmSet
   {
      //Make a constructor so time can be set
      //only when the object is created
      public AlarmSet(DateTime ringTime)
      {
         RingTime = ringTime
      }
      public DateTime RingTime{get; private set:}
   }
}

Listing 1: You define an Akka.NET message using C# classes

AlarmSet is a trivial message, but one can well imagine what to do when receiving such a message. “Oh yeah, I need to set my clock to go off at the value of RingTime.” Unlike full-blown, complex C# classes that do work, a class that defines a message does nothing more than contain information, similar to a structure.

The Importance of Immutability

Please notice that the AlarmSet message is immutable. The only way the property, RingTime, can be set is when the message is created. Nobody can come along later down the line and twiddle with things. Being immutable is a very important best practice of Akka.NET. When the message gets passed about to who knows where, being immutable means that nobody can alter the message contents when the message is in transit.

The AkkaNetDemo application has one message, Trade, shown in Figure 4. Trade inherits from the abstract class, AbstractTrade. This inheritance model is of my own doing. It has nothing to do with Akka.NET.

AKKA4
Figure 4: The Trade message describes a stock trade and its state.

The message, Trade, has properties by which an actor will determine behavior. A Trade message will be sent among Actors to perform work. Let’s take a look at details of sending messages.

Sending Messages

As we mentioned above, an Actor sends a message to another Actor to get something done. One of the nice things about Akka.NET is that the framework uses intuative, nature language to describe sending a message. You use Actor.Tell(message) to send a message to an actor on a fire-and-forget basis. You use Actor.Ask(message) when you want to get to the underlying Task object in which the message is sent.

Under the Hood: Task Parallel Library

Akka.NET uses the .NET Task Parallel Library to do its thread management. As a result, whenever you Actor.Ask(message) you will get the underlying Task asking the return object. Also, you use Actor.Ask<MyReturnMessageType>(message) to get back the underlying message the Actor receives in response to the ask. (You’ll see an Ask() scenario later in the sample code.)

Also, you use Actor.Forward(message) when you want an Actor to forward a received message on to another Actor. When you Forward() a message, information about the originating sender is passed along too.

So, let’s review. There are three ways to pass a message from one Actor to another:

  • Actor.Send(message)
  • Task task = Actor.Ask<MyReturnMessageType>(message)
  • Actor.Forward(message)

Now that we know how to send a message, let’s talk about processing messages in an Actor.

OnReceive(): An Actor’s Message Clearing House

Actors inherit from an abstract class, UntypedActor. UntypedActor defines an abstract method, OnReceive(message. OnReceive(message) needs to be implemented by the class that inherits from UntypedActor. The internals of Akka.NET are smart enough to do all the automagic required for an Actor to receive a message and the pass it on to OnReceive(). OnReceive() is the place where message processing takes place.

When an Actor receives a message in OnReceive(message), the Actor can analyze the message and react to the analysis. That analysis might be to do some processing and then respond to the sender with another message. The Actor might decompose the message and create one or more new messages to send on to other existing actors or child actors it creates. Or, the Actor can simply forward the message onto another Actor.

The important thing to remember is that the OnReceive(message) is the place where processing will happen.

Understanding Context

Akka.NET is designed to be a loosely coupled, distributed, asynchronous system. A lot of the magic of the system is hidden at the programming level. When you send a message, using Tell(message), for example, it might seem as if you are doing nothing more than sending a simple POCO. This is not the case. When you invoke Actor.Tell(message), information about the Sender of the message tags along (no pun intended). Also, within a given Actor, there is an implicit property, Context. You can use Context to determine the parent of the Actor, using Context.Parent. You use Context.System to access the root ActorSystem, under which the Actor resides. You can get a reference to the Actor itself using Context.Self. As you begin to program with Akka.NET, you’ll find yourself working with Context a lot.

Okay, so up to this point we’ve explored a lot of concepts. We’ve covered Actors, Messages, Tell(), Ask(), Forward(), and OnReceive(), as well as taken a glimpse at Context. And, we looked at the specific program flow and actors that are part of the AkkaNetDemo program. Now, let’s move beyond concept into actually programming.

Coding the AkkaNetDemo Program

As mentioned at the beginning of this article, when we introduced the program flow of the AkkaNetDemo, the AkkaNetDemo console application decomposes a comma delimited string that describes a trade. The comma delimited string is sent to the console by the user. The string is split into ticker, share, and B (or S) values. These three values are sent to the TradingSystem by way of the method, TradingSystem.Trade(ticker, share, tradeType). Then, TradingSystem creates a StockBroker object that it will Ask() to do the trade. Listing 2 shows the code:

using System;
using System.Threading.Tasks;
using Akka.Actor;
using AkkaNetDemo.Messages;

namespace AkkaNetDemo
{
   /// <summary>
   /// This class is the root level System for Stock Trading.
   /// </summary>
   public class TradingSystem
   {  {
      /// <summary>
      /// This method starts the trade messaging process. A
      /// top level Akka.NET <see cref="ActorSystem"/> object,
      /// tradingSystem is created. Also, a <see cref="Trade"/>
      /// message is defined. The ActorSystem creates the
      /// <see cref="StockBroker"/> to execute the trade.
      ///      ///
      /// NOTICE: The StockBroker uses
      /// <see cref="UntypedActor"/>.Ask(message) to execute the trade.
      /// <see cref="UntypedActor"/>.Ask(message) returns the underlying
      /// <see cref="Task"/> doing the asking. We'll Task.Wait() for the
      /// ask to complete. The response <see cref="Trade"/> message of
      /// the trade will be reflected in the Task.Result
      /// </summary>
      /// <param name="ticker">The ticker symbol of the stock you want
      ///    to trade</param>
      /// <param name="shares">The number of shares to trade</param>
      /// <param name="tradeType">Describes the trade to
      /// execute.<see cref="TradeType"/>.Buy or TradeType.Sell.</param>
      /// <returns>The response <see cref="Trade"/> message</returns>
      public static Trade Trade(string ticker, int shares, TradeType tradeType)
      {      {

         //Create the ActorSystem
         var tradingSystem = ActorSystem.Create("TradingSystem");
         //Create the StockBroker
         var broker = tradingSystem.ActorOf(Props.Create(() =>
            new StockBroker()), "MyBroker");
         //Create the Trade message
         var trade = new Messages.Trade(ticker, shares,
            tradeType,TradeStatus.Open,Guid.NewGuid());
         //Send the Trade message to the broker by way of an Ask(message).
         //We want the underlying Task.
         //Notice also that in Ask we are providing the Trade message as
         //a generic type. This is how the Task knows how to pass the
         //response Trade message out to the caller.
         var task = broker.Ask<Trade>(trade);
         //Wait for the Task to complete. The task is completed upon
         //the Broker receiving response Trade message.
         task.Wait();
         //Return the response Trade message.
         return task.Result;
      }
   }
}

Listing 2: The TradingSystem code creates a StockBroker and sends the broker a Trade message via Ask()

Why Ask()?

The reason we are having the TradingSystem ask the broker to send the Trade message is that we want to wait until the trade is complete before moving on to anything else. Remember that Actor.Ask<MyResponseType>(message) returns the underlying Task object. We’ll invoke Task.Wait() to block the task, waiting for it to complete before allowing program control flow to move forward.

The way that a Task completes is when the Actor doing the “ask” gets a message back. That response message is reflected in the Task.Result.

The broker receives the Trade message and creates either a Buy Or Sell FloorTrader object. Then, the broker calls FloorTrader.Forward(message), passing the message provided by the TradingSystem (see Listing 3).

using Akka.Actor;
using AkkaNetDemo.Messages;

namespace AkkaNetDemo
{
   /// <summary>
   ///     An object the describes a Stock Broker. A StockBroker
   ///     will use an <see cref="FloorTrader" />
   ///     to execute a trade.
   /// </summary>
   public class StockBroker : UntypedActor
   {
      /// <summary>
      ///    This overrideen method provides the behavior a StockBroker
      ///    executes upon receipt of a message. If the
      ///    <see cref="Trade" /> message's <see cref="TradeType" />
      ///    = TradeType.Sell, the StockBroker creates a Sell
      ///    <see cref="FloorTrader" /> and calls
      ///    <see cref="UntypedActor" />.Forward() to pass the Trade
      ///     message on.
      ///    If the Trade is TradeType.Buy, a Buy Floor Trader is created
      ///    and the Trade message is forwarded.
      /// </summary>
      /// <param name="message">The <see cref="Trade" />
      ///    message to process</param>
      protected override void OnReceive(object message)
      {
         var trade = message as AbstractTrade;

         if (trade != null)
         {
            //Make sure you are processing only open trades
            if (trade.TradeStatus.Equals(TradeStatus.Open))
            {
               if (trade.TradeType.Equals(TradeType.Sell))
               {
                  //create the Sell FloorTrader and forward the message
                  var sellTrader = Context.ActorOf(Props.Create(() =>
                     new FloorTrader()), "SellFloorTrader");
                  sellTrader.Forward(trade);
               }
               else
               {
                  //create the Buy FloorTrader and forward the message
                  var buyTrader = Context.ActorOf(Props.Create(() =>
                     new FloorTrader()), "BuyFloorTrader");
                  buyTrader.Forward(trade);
               }
            }
         }
      }
   }
}

Listing 3: The StockBroker forwards a Message on to a FloorTrader

The FloorTrader inspects the message. If Trade.Shares is more than the TradeLimit, the FloorTrader creates a new Trade message, setting the Trade.TradeStatus = TradeStatus.Failure. Otherwise, the FloorTrader creates the new Trade message, setting the Trade.TradeStatus = TradeStatus.Success.

The FloorTrader uses the Sender object, implicit in the Actor object, to send the new Trade message back up the hierarchy (see Listing 4).

using Akka.Actor;
using AkkaNetDemo.Messages;

namespace AkkaNetDemo
{
   /// <summary>
   ///    This class describes a Floor Trader, someone who buys and
   ///    sells stocks on the trading floor of a Stock Exchange
   /// </summary>
   public class FloorTrader : UntypedActor
   {
      private const int TradingLimit = 200;

      /// <summary>
      ///    This overrideen method provides the behavior a
      ///    Floor Trader executes upon receipt of a message.
      ///    If the number of shares in the <see cref="Trade" />
      ///    message is under the TradingLimit, the Floor Trader
      ///    executes the trade, then creates a new Trade message
      ///    with a <see cref="TradeStatus" /> = TradeStatus.Success.
      ///    Otherwise, a new Trade message is created with a
      ///    TradeStatus = TradeStatus.Fail.
      /// </summary>
      /// <param name="message">The message for the Floor Trader
      ///    to process upon receipt</param>
      protected override void OnReceive(object message)
      {      {
         var trade = message as Trade;
         if (trade != null)
         {
            Trade responseTrade;
            if (trade.Shares > TradingLimit)
            {
               var msg =
                  string.Format(
                     "You want to trade {0} Shares. The trade exceeds the
                        trading limit of {1}. The system will not {2} {0}
                        of {3}.",
                     trade.Shares, TradingLimit, trade.TradeType, trade.Ticker);
               responseTrade = new Trade(trade.Ticker, trade.Shares,
                  trade.TradeType, TradeStatus.Fail, trade.TadingSessionId, msg);
            }
            else
            {
               var msg = string.Format("I am {0}ing {1} shares of {2}",
                  trade.TradeType, trade.Shares, trade.Ticker);
               responseTrade = new Trade(trade.Ticker, trade.Shares,
                  trade.TradeType, TradeStatus.Success, trade.TadingSessionId,
                  msg);
            }
            Sender.Tell(responseTrade, Self);
         }
      }
   }
}

Listing 4: The FloorTrader object sends a Message response back to the Sender

The new Trade message is sent to the broker doing the asking. The TradingSystem extracts the Trade message from Task underlying the Ask() method, by way of the Task‘s Result.

            var task = broker.Ask<Trade>(trade);
            //Wait for the Task to complete. The task is completed
            //upon the Broker receiving response Trade message.
            task.Wait();

The TradingSystem returns the response Trade message to the caller.

            //Return the response Trade message.
            return task.Result;

The console converts the Trade message to JSON and dumps the output to the screen.

            //Call the static object, Trading System. TradingSystem will
            //return the response message of the Trade.
            Trade responseTradeMessage = TradingSystem.Trade(ticker,
               shares, tradeType);

            Console.WriteLine("The following is the result of your trade:\n");
            //Convert the response Trade message into a JSON string and show it
            var startColor = Console.ForegroundColor;
            //Make the console text RED upon failure
            if (responseTradeMessage.TradeStatus.Equals(TradeStatus.Fail))
               Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(JsonConvert.SerializeObject
               (responseTradeMessage,Newtonsoft.Json.Formatting.Indented));
            Console.ForegroundColor = startColor;

Listing 5: The AkkaNetDemo application console displays the returned Trade message as JSON.

Console output is shown below in Figures 5 and 6.

AKKA5
Figure 5: The AkkaNetDemo program converts the response Trade message to JSON when rendering on the console.

AKKA6
Figure 6: Failed trades are displayed in red

Putting It All Together

We’ve done a lot in this article. I showed you the very basics of using Actors and Messages to do a simple transactional scenario under Akka.NET. I showed you how to create a root Actor by using ActorSystem. I showed you how to create child actors by using Context. Also, we’ve discussed the three ways of sending a message: Actor.Tell(), Actor.Ask(), and Ask.Forward().

However, as much as we’ve done, we’ve only scratched the surface. Akka.NET is very big framework. Remember, Akka.NET is meant to be run on very big, dynamic, distributed systems in a fault tolerant manner. Actor supervision, monitoring, and routing are but a few of the topics we’ll cover later one in subsequent articles. In the meantime, you might want take a look the Akka.NET bootcamps and video documentation on the GetAkka.Net site. Also, there is an active chat site that has a very helpful community.

As you can see, the Actor Model is well suited to modern distributed computing. Beginning to take the journey to master Akka.NET is a good first step toward being able to do modern programming in the world of dynamic, distributed computing.

More by Author

Must Read