facebook

Blog

Stay updated

Let's see how to implement Actor Model in .NET
Build scalable and resilient applications with Akka Actors
Wednesday, March 10, 2021

One of our clients working in the gaming field has commissioned us a platform for managing a virtual football championship, with both team and individual rankings.

After analyzing the problem, we wonder how to create a platform that would keep updated rankings and statistics, even if the event is not strictly connected to them (such as a player’s goal or yellow card), maintaining the system scalable and resilient.

The best solution we found is to use Akka Actors, available on .NET in the Akka.NET package (https://getakka.net/articles/intro/what-is-akka.html). They are successfully used in distributed systems and virtualized architectures, where components crash without responding, messages get lost, and the network is not stable.

To deal with these issues, Akka.NET provides:

  • Multi-threaded behavior without using low-level concurrency constructs like atomics or locks
  • Transparent remote communication between systems and their components
  • A clustered, high-availability architecture that is elastic and scalable.

Developers can use packages such as Akka.Cluster and Akka.Cluster.Sharding, which allow configuring clusters to distribute actors across multiple nodes.

 Akka.NET is based on Actor model ((https://hal.archives-ouvertes.fr/hal-01163534v7/document) pattern and gives us a library set to correctly implement this pattern so that we can get a higher abstraction level and easier design of concurrent, parallels, and distributed systems.

Actor Model 

The basic characteristic of actors is that they model the world as stateful entities communicating with each other by passing messages. The actor model provides an abstraction that allows you to think about communication in your code. 

Actors have these characteristics: 

  1. They communicate with asynchronous messaging
  2. They manage their own state  
  3. When responding to a message, they can:  
    • Create child actors 
    • Send messages to other actors 
    • Stop child actors or themselves 

Consequently, in the actor model, two actors will never share the same memory nor directly change another actor’s status. It will always have to send a message to the actor it wants to change, and the receiving actor will take care of receiving and processing the message.

Actor Model

Application architecture 

Let‘s introduce our solution, either taking a look at Akka Actors architecture.

It will be composed of a front-end Angular App, a Web API .NET Core actor-based layer that supports long polling using the SignalR hub, and an events simulator.

Application diagram

When one creates an actor in Akka.NET it always belongs to a certain parent. That’s because actors are always organized into a tree, and creating an actor can only happen inside another actor. Hierarchically, the created actor becomes a child of the parent it was created from.

At the top of this tree, there’s a default layer containing three actors: root guardian (/), parent of all system actors, with tho children, user guardian (/user), parent of all user-created actors, and system guardian (/system), that is to achieve an orderly shut-down sequence where logging remains active.

These built-in actors’ names contain the word “guardian” because these are supervising every actor living as a child of them.

Akka tree

In our project, we’ve introduced three kinds of actors: two top-level as MatchesActor, which is used to update data of all matches, and StandingsActor, to update standings data; and one second-level actor, MatchActor, that manages single match data.

var firstRef = System.ActorOf(Props.Create(() => new MatchesActor()), "matches"); 

Console.WriteLine($"First: {firstRef}"); 

It returns as output: 

First: Actor[akka://akkaActors/user/matches#1213311313] 

In our project top-level actors configuration is defined in ActorsEnvironment -> Configure method:

public static ActorsEnvironment Configure(IServiceProvider provider) 

{ 

        var hubContext = provider.GetService<IHubContext<Hub>>(); 

        var system = ActorSystem.Create("akkaActors"); 
 
        system.ActorOf(Props.Create(() => new MatchesActor()), "matches"); 
        system.ActorOf(Props.Create(() => new StandingsActor()), "standings"); 

        return new ActorsEnvironment(system); 

} 

This method is called in Startup.cs, in Configure Services:

 services.AddSingleton<ActorsEnvironment>(context => ActorsEnvironment.CreateAndConfigure(context));

In this case, top-level actor MatchesActor has /user/matches path.

We also defined constants for actors path, so that we can easily use them.

public class ActorPaths 

{ 
        public const string MatchesActor = "/user/matches"; 
        public const string StandingsActor = "/user/standings"; 
        public const string SignalRActor = "/user/signalR"; 

        public const string AllActors = "/user/*/*"; 
 } 

To create non-top-level actors, we can call Context.ActorOf() from parent actor, as it follows:

public class MatchActor : UntypedActor 
{ 
} 

public class MatchesActor : UntypedActor 
{ 

        protected override void OnReceive(object message) 
        { 
                switch (message) 
                { 
                        case "create-new": 
                        IActorRef secondRef = Context.ActorOf(Props.Create(() => new MatchActor()), $"first-match"); 
                        Console.WriteLine($"Second: {secondRef}"); 
                        break; 
                } 
        } 
} 

var firstRef = Sys.ActorOf(Props.Create<MatchesActor>(), "matches"); 
Console.WriteLine($"First: {firstRef}"); 
firstRef.Tell("create-new", ActorRefs.NoSender); 

In this way, we’re creating a first-level actor and print its reference and, after that, we send it a message containing child id to create (if not existing yet), then we print child reference.

Generated output is:

First : Actor[akka://akkaActors/user/matches#1213311313] 
Second: Actor[akka://akkaActors/user/matches/first-match#12261716] 

Life cycle 

Stopping an actor can be done by calling Context.Stop(actorRef). It is considered a bad practice to stop arbitrary actors this way. The recommended pattern is to call Context.Stop(self) inside an actor to stop itself, usually as a response to some user-defined stop message or when the actor is done with its job.

The actor API exposes many lifecycle hooks that the actor implementation can override. The most commonly used are PreStart() and PostStop().

PreStart() is invoked after the actor has started but before it processes its first message.

PostStop() is invoked just before the actor stops.

In our case:

public class MatchActor : ReceiveActor 
{ 
        protected override void PreStart() 
        { 
                Console.WriteLine($"Match actor {Self.Path.Name} started"); 
        } 

        protected override void PostStop() 
        { 
                Console.WriteLine($"Match actor {Self.Path.Name} stopped"); 
        } 

} 

public class MatchesActor : ReceiveActor 
{ 
        protected override void PreStart() 
        { 
                Console.WriteLine($"Matches actor {Self.Path.Name} started"); 
        } 

        protected override void PostStop() 
        { 
                Console.WriteLine($"Matches actor {Self.Path.Name} stopped"); 
        } 

        protected override void OnReceive(object message) 
        { 
                switch (message) 
                { 
                        case "create-new": 
                                IActorRef secondRef = Context.ActorOf(Props.Create(() => new MatchActor()), $"first-match"); 
                        
                                break;             
                        case "stop": 
                                Context.Stop(Self); 
                                break;             
                }   

        }
 
} 

var firstRef = Sys.ActorOf(Props.Create<MatchesActor>(), "matches"); 
firstRef.Tell("create-new", ActorRefs.NoSender); 
firstRef.Tell("stop"); 

Generated output is: 

Matches actor matches started 
Match actor first-match started 
Match actor first-match stopped 
Matches actor matches stopped 

We deduce that all PostStop() hooks of the children are called before the PostStop() hook of the parent is called.

Error management 

Whenever an actor fails, it is temporarily suspended, and the failure information is propagated to the parent, which decides how to handle the exception caused by the child actor.

The default supervisor strategy is to stop and restart the child.

public class MatchActor : ReceiveActor 
{ 
        protected override void PreStart() 
        { 
                Console.WriteLine($"Match actor {Self.Path.Name} started"); 
        } 

        protected override void PostStop() 
        { 
                Console.WriteLine($"Match actor {Self.Path.Name} stopped"); 
        } 

        protected override void OnReceive(object message) 
        { 
                switch (message) 
                { 
                        case "fail": 
                                Console.WriteLine("Match actor fails"); 
                                throw new Exception("I failed!"); 
                } 
        } 
} 

public class MatchesActor : ReceiveActor 
 { 
        protected override void PreStart() 
        { 
                Console.WriteLine($"Matches actor {Self.Path.Name} started"); 
        } 

        protected override void PostStop() 
        { 
                Console.WriteLine($"Matches actor {Self.Path.Name} stopped"); 
        } 


protected override void OnReceive(object message) 
        { 
                switch (message) 
                { 
                        case "fail-child": 
                                IActorRef child = Context.ActorOf(Props.Create(() => new MatchActor()), $"first-match"); 

                                child.Tell("fail"); 
                                break;             
                } 

        } 

  } 

var firstRef = Sys.ActorOf(Props.Create<MatchesActor>(), "matches"); 
firstRef.Tell("fail-child", ActorRefs.NoSender); 
firstRef.Tell("stop"); 

The generated output is:  

Matches actor matches started 
Match actor first-match started 
Match actor fails 
Matches actor matches stopped 
Matches actor matches started 

[ERROR][11.02.2021 13:34:50][Thread 0003][akka://akkaActors/user/matches/first-match] I failed! 
Cause: System.Exception: I failed! 
   at AkkaActors.MatchActor.OnReceive(Object message) 
   at Akka.Actor.UntypedActor.Receive(Object message) 
   at Akka.Actor.ActorBase.AroundReceive(Receive receive, Object message) 
   at Akka.Actor.ActorCell.ReceiveMessage(Object message) 
   at Akka.Actor.ActorCell.Invoke(Envelope envelope) 

We see that after failure the actor is stopped and immediately started. If one prefers to have a different behaviour between first execution and the other ones, it’s enough to override PreRestart() and PostRestart() methods.

We have 3 kind of events managed by MatchesActor:

  • MatchStarted,
  • MatchEnded 
  • MatchScoreChanged. 

defined as follows:

public class MatchStarted 
{ 
        public int MatchId { get; set; } 
} 

 public class MatchEnded 
{ 
        public int MatchId { get; set; } 
} 

 public class MatchScoreChanged 
{ 
        public int MatchId { get; set; } 
        public int Team1Score { get; set; } 
        public int Team2Score { get; set; } 
} 

They are managed by handlers registered in MatchesActor class:

public MatchesActor() 
{ 
        ReceiveAsync<MatchStarted>(request => Handle(request)); 
        ReceiveAsync<MatchEnded>(request => Handle(request)); 
        ReceiveAsync<MatchScoreChanged>(request => Handle(request)); 
} 

private IActorRef GetMatchActor(int matchId) 
{ 
        var childActor = Context.Child($"{matchId}"); 

        if (childActor is Nobody) 
        { 
                childActor = Context.ActorOf(Props.Create(() => new MatchActor()), $"{matchId}"); 
        } 

         return childActor; 

} 

 private async Task Handle(MatchStarted request) 
{ 
        var child = GetMatchActor(request.MatchId); 
        child.Forward(request); 
} 
 // [...] 

In this way, message will be forwarded to correct MatchActor that knows how to manage them.

public MatchActor() 
{ 
        ReceiveAsync<MatchStarted>(request => Handle(request)); 
        ReceiveAsync<MatchEnded>(request => Handle(request)); 
        ReceiveAsync<MatchScoreChanged>(request => Handle(request)); 
} 

 private async Task Handle(MatchStarted request) 
{ 
        CurrentMatch.Started = true; 
        CurrentMatch.Team1Score = 0; 
        CurrentMatch.Team2Score = 0; 

         Context.ActorSelection(ActorPaths.StandingsActor).Tell(new StandingsChanged() {  
                Teams = new Dictionary<string, int>() 
                { 
                        { CurrentMatch.Team1, 1 }, 
                        { CurrentMatch.Team2, 1 } 
                } 

        }); 
        Context.ActorSelection(ActorPaths.SignalRActor).Tell(request); 

} 
 // [...] 

Where StandingsChanges is:

public class StandingsChanged 
{ 
        public Dictionary<string, int> Teams { get; set; } 
} 

MancheActor manages the ranking update logic and first forwards the message to the StandingsActor, which updates the ranking and is defined as follows:

public class StandingsActor : ReceiveActor 
{ 
        private Standings CurrentStandings { get; set; } 
        public StandingsActor() 

        { 
                Receive<StandingsChanged>(request => Handle(request)); 
        } 
  
        private void Handle(StandingsChanged request) 
        { 
                CurrentStandings = new Standings() 
                { 
                        Teams = request.Teams 
                }; 

                Context.ActorSelection(ActorPaths.SignalRActor).Tell(request); 

        } 

 }  

If needed, events could be forwarded to SignalRActor, which will poll in broadcast data to be shown in the UI, basing on the type of message to manage.

public class SignalRActor : ReceiveActor 
{ 
        private readonly IHubContext<Hub> hubContext; 

        public SignalRActor(IHubContext<Hub> hubContext) 
        { 
                this.hubContext = hubContext; 

                Receive<MatchStarted>(message => Handle(message)); 
                Receive<MatchEnded>(message => Handle(message)); 
                Receive<MatchScoreChanged>(message => Handle(message)); 
                Receive<StandingsChanged>(message => Handle(message)); 

        } 

         private void Handle(MatchStarted message) 
        { 
                hubContext.Clients.All.SendAsync("BroadcastMatchStarted", message); 
        } 
  
        private void Handle(MatchEnded message) 
        { 
                hubContext.Clients.All.SendAsync("BroadcastMatchEnded", message); 
        } 

        private void Handle(MatchScoreChanged message) 
        { 
                hubContext.Clients.All.SendAsync("BroadcastMatchScoreChanged", message); 
        } 
  
        private void Handle(StandingsChanged message) 
        { 
                 hubContext.Clients.All.SendAsync("BroadcastStandingsChanged", message); 
        } 

} 

Our Web API controllers use actors and perform operation by using them.

For instance, in MatchesController class there are 3 endpoints to modify match status:

 [HttpPut] 
[Route("{matchId}/start")] 
public IActionResult StartMatch(int matchId) 
{ 
        environment 
                .SelectActor(ActorPaths.MatchesActor) 
                .Tell(new MatchStarted 
                { 
                        MatchId = matchId 
                }); 

         return Ok(); 

} 

[HttpPut] 
[Route("{matchId}/stop")] 
public IActionResult StopMatch(int matchId) 
{ 
        environment 
                .SelectActor(ActorPaths.MatchesActor) 
                .Tell(new MatchEnded 
                { 
                        MatchId = matchId 
                }); 

        return Ok(); 

} 
 
[HttpPut] 
[Route("{matchId}/changescore/{team1Score}/{team2Score}")] 
public IActionResult ChangeScore(int matchId, int team1Score, int team2Score) 
{ 
        environment 
                .SelectActor(ActorPaths.MatchesActor) 
                .Tell(new MatchScoreChanged 
                { 
                        MatchId = matchId, 
                        Team1Score = team1Score, 
                        Team2Score = team2Score 
                }); 
  
        return Ok(); 

} 

The front-end application is a simple Angular app using a SignalRService that intercepts communications with SignalRHub:

export class SignalRService { 
        private hub: HubConnection; 
        
        constructor() { 
                this.hub = new signalR.HubConnectionBuilder() 
                       	.withUrl(`${environment.websocketUrl}akkaActorsHub`) 
        		.build(); 

                this.hub.start().then(() => { 
               	        console.log(`Hub connection started`); 
    		        }).catch(err => document.write(err)); 

        } 

         public on<T>(name: string): Observable<T> { 
                const subject = new Subject<T>(); 

                this.hub.on(name, (data) => { 
                subject.next(data); 
                }); 

                return subject.asObservable(); 

        } 

} 

This class is injected in a Angular components that need it, in their ngOnInit method, in this way:

ngOnInit() { 
        const onMatchScoreChanged$: Observable<any> = this.signalR.on(`BroadcastMatchScoreChanged`); 
        onMatchScoreChanged$.subscribe((message: any) => { 
                //Do something 
        }); 
        } 
}); 

For test purposes we created an events simulator that performs some event, like match starting, ending and score changings, before stopping them.

Events are sent on REST protocol to Web API:

private static void Send(string endpoint, StringContent content) 
{ 
        _client.PostAsync(endpoint, content).Wait(); 
} 

 static void Main(string[] args) 
{ 
        _client = new HttpClient { BaseAddress = new Uri("http://localhost:5000") }; 

        _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 

        Send("api/matches/1/start", ""); 
        Send("api/matches/2/start", ""); 

        var content = new StringContent(JsonConvert.SerializeObject(new 
        { 
                Team1Score = 1, 
                Team2Score = 0 
        }), Encoding.UTF8, "application/json"); 

        Send("api/matches/2/updatescore", content); 

        // other events… 

        Send("api/matches/2/stop", ""); 
        Send("api/matches/1/stop", ""); 

}	 

Project execution 

When running our test solution, we can get a dashboard with data refreshed each time an event from the simulator comes:

application running

I wish I have aroused your interest in the topic. On our blog, you can find other articles about the software architecture.

The source code used in this post is available here.

To the next article