De boeiende, maar onbekende wereld van actors

Om bij te blijven met de nieuwste ontwikkelingen in .NET development, stuurden we onze .NET-developer Gaëtan op pad naar Antwerpen voor een training georganiseerd door Azug. Een Belgische gebruikersgroep van Microsoft Azure die zich focust op ontwikkeling en architectuur van het platform.

 

Die dag op het programma: Actor Based Services. Een term die onze developer nog nooit hoorde. En hij was blijkbaar niet alleen. Bijna niemand in de zaal had er al van gehoord. Maar na een boeiende uitleg was alles duidelijk. Actors zijn krachtig. Heel krachtig. En hier is waarom.

 

 

Schaalbaarheid

 

Wanneer je een applicatie ontwikkelt, denk je niet altijd aan de schaalbaarheid ervan. Een programma achteraf schaalbaar maken, kan snel heel complex worden: je gebruikt dan multithreading, cache en eventueel API’s.

Threading kan ervoor zorgen dat je na een tijd gaat locken. Wat later tot deadlocks kan leiden. Cache ga je delen om een bepaalde state bij te houden. En API’s ga je aanspreken, maar zijn nogal traag. Dit doordat ze stateless zijn en data opslaan in een database om deze later weer op te halen. 

Maar wat als je jouw applicatie wil draaien op verschillende processen? Of je state wenst te delen tussen verschillende servers? Wanneer je in deze fase van je ontwikkeling zit, schakel je misschien beter actors in. 

 

 

Maak kennis met Actors 

Een actor is een primitive die berichten zendt of ontvangt en zijn eigen state bijhoudt. Een actor kan dus een bericht zenden naar een ander actor. Deze kan dan aan de hand van dat bericht een stuk code uitvoeren en een bepaalde state bijhouden. Hiervoor maken we gebruik van een framework.

Het framework zelf maakt threads aan en deelt de state. Binnen .NET-development zelf, bestaan er twee soorten frameworks: 

 

Don’t kill Kenny

Om te verduidelijken wat Actors nu precies doen, gebruiken we in onderstaand voorbeeld het Orleans framework. Een actor framework gemaakt door Microsoft. Orleans werkt op .NET Core en maakt gebruik van Interfaces om met actors te communiceren. Er bestaan andere frameworks zoals Akka.NET. Dit is een port van het Java Akka framework.

Stel: je maakt een programma dat kan vragen aan Kenny of hij nog steeds leeft. Kenny moet dan kunnen antwoorden dat hij nog steeds leeft en aangeven hoeveel keer dit gevraagd werd. Hoe implementeer je actors? 

Orleans maakt gebruik van Grains en Silo’s. Grains zijn de primitive van Orleans (Actor). Ze beschikken dus over hun logica en state. Ze worden gehost en uitgevoerd door Silo’s. 

 

 

Ons programma bestaat uit een Client, Silo (Server) en een Interface library dat gedeeld wordt met de Client en Silo. 

 

 

Installeer volgende NugetPackages:

  • Microsoft.Orleans.Client – SouthParkClient
  • Microsoft.Orleans.Server – SouthParkSilo
  • Microsoft.Orleans.Core.Abstractions – SouthParkSilo; SouthParkInterfaces
  • Microsoft.Orleans.OrleansCodeGenerator.Build – SouthParkClient; SouthParkSilo; SouthParkInterfaces

De Silo bevat de CharacterGrain. Deze erft over van Grain. Een class uit Orleans die zorgt voor het bijhouden van references, lifecycle etc. Ook implementeren we de IcharacterGrain. Dit is een Interface dat wordt gedeeld over de Client en de Silo. Onze Interface beschikt over één methode namelijk,  AreYouAlive. De methode returned het aantal keer dat deze aangesproken werd.

 

 

In onze Program.cs builden we onze host als volgt. 

 

 

De Interface library beschikt over één Interface. Deze erft over van IGrainWithStringKey. Deze interface laat onze client toe om een Grain aan te spreken op basis van een Key die we kunnen meegeven. 

 

 

In onze Client Program.cs builden we onze Client. Door dezelfde ClusterId en ServiceId mee te geven als onze host weet Orleans hoe hij deze moet verbinden.

We vragen onze Grain op aan de hand van de GetGrain methode. Deze ontvangt de ICharacterGrain interface mee als type. We geven deze ook een Key mee. Aan de hand van deze Key houden we onze referentie naar de Grain. In dit geval Kenny. 

 

 

We kunnen nu de methode AreYouAlive aanspreken en wachten op antwoord van onze Silo. We starten de Silo en daarna de Client. In dit voorbeeld starten we drie Clients die elk vragen of Kenny nog leeft. Sluit je één van de Clients en start je een nieuwe op, blijft de counter stijgen. De state van Kenny wordt dus bijgehouden door de Silo. De Silo draait in een apart proces waardoor dat onze Clients en Silo onafhankelijk blijven.

 

Nu zitten we nog steeds met het “Don’t kill Kenny” probleem. Sluit je de Silo af? Dan zijn we onze state kwijt. 

 

 

Om dit op te lossen maken we gebruik van een Cluster. Om een Cluster op te zetten, moeten we gebruik maken van een database. 

Hiervoor installeren we de NuGet package Microsoft.Orleans.Clustering.AdoNet in onze Client en Silo project. Hierbij krijgen we een nieuwe folder namelijk ‘OrleansAdoNetContent’. In deze folder vinden we de nodige scripts per database provider. In ons voorbeeld maken we gebruik van Microsoft SQL Server. 

In zowel de Client als de Silo voegen we de UseAdoNetClustering opties toe aan onze builder. Daar voeg je je eigen ConnectionString aan toe. 

Bij de Silo voegen we ook AddAdoNetGrainStorage toe. Dit laat ons toe om onze state te gaan persisten naar onze database.

Nu we met meerdere Silo’s gaan werken moeten deze hun eigen IP, Poort en Gateway hebben. Dit kunnen we instellen via de EndpointOptions. 

 

 

Nu moeten we nog onze CharacterState opsplitsen en deze meegeven als type voor onze Grain. Zo weet Orleans welke Properties er bij de state horen en kan hij deze gaan opslaan in onze database. 

 

 

Nu kunnen we meerdere Silo’s gaan opstarten. Elke Silo deelt de State van de Grains. De state persisten we naar onze database. Deze wordt telkens opgehaald door onze Grains. Zonder extra code te moeten schrijven. Sluit je een Silo af, dan blijft onze applicatie werken. De andere Silo’s pikken de state weer op. Waardoor Kenny altijd leeft. De enige manier om Kenny te doden is door alle servers af te sluiten. 

Sluit je alle servers en start je daarna weer één op? Dan wordt de state terug opgehaald uit de database en kan je gewoon weer verder. 

 

 

 

Conclusie, Kenny never dies? 

Neen, Kenny kan nog steeds sterven maar hij maakt het ons veel moeilijker. Actor Frameworks zijn een efficiënte manier om een schaalbare applicatie te maken. Ze kunnen wel complex worden bij het gebruiken van veel actoren en vragen toch wel wat kennis over netwerken indien we deze services via TCP/IP willen aanspreken. Maar eenmaal opgezet is het deployen van nieuwe Silo’s in de cluster zeer gemakkelijk en beschikken we over een zeer schaalbare applicatie.

Natuurlijk kunnen we nog veel meer met actoren dan hier besproken. Elk framework beschikt over eigen implementaties van Timers, Observers, Persistence etc. Die laat ik aan jou om te onderzoeken. 

 

CodeSamples: GitHub

 

Met dank aan:

  • Azug: https://www.azug.be/
  • Wim Van Houts: http://www.xqting.com en http://www.splitvice.com.