Akka.Net est un framework qui vous permettra de faire de la programmation distribuée en appliquant le pattern « Actor Models ». Comme son nom l’indique, c’est un portage de la librairie java Akka <troll> c’est la preuve qu’il reste encore des choses sympa à pomper côté Java </troll> et nous vous proposons donc une petite présentation du pattern, du Framework et d’un exemple.

 

Le pattern Actor Models

Selon Wikipédia, « le modèle d’acteur est un modèle mathématique qui considère des acteurs comme les seules fonctions primitives nécessaires pour la programmation concurrente. Les acteurs communiquent par échange de messages. En réponse à un message, un acteur peut effectuer un traitement local, créer d’autres acteurs, ou envoyer d’autres messages. »

Le modèle considère donc que tout est acteur (de la même manière qu’en programmation orienté objet, on considère que tout est objet) et qu’il doit être en mesure de :

  • Recevoir des messages
  • Envoyer des messages à d’autres acteurs
  • Créer de nouveaux acteurs
  • Modifier son état (et donc potentiellement son comportement pour les messages suivants)

Les deux informations importantes sont donc à mon avis :

  • Qu’il y a une distinction forte entre le message et le traitement (contenu dans les acteurs)
  • Les acteurs traitent les messages les uns après les autres sans se soucier de l’état des autres acteurs (ils sont complètement indépendants dans leurs tâches).

 

Akka.net

Akka.Net est une implémentation de ce pattern, et il distingue trois types d’objets :

  • Les « Messages » : chargés de transporter l’information d’un acteur à l’autre.
  • Les « Actors »: chargés de traiter les messages, ils sont constitués d’un état » (state) et d’un « comportement» (behaviour). Chaque acteur possède sa propre file d’attente qui est dépilée selon le principe FIFO.
  • Les « Actor System » : c’est le host qui va gérer une hiérarchie d’acteurs (une sorte d’espace de travail).

A partir de là, nous allons modéliser nos traitements avec quelques règles à respecter :

  • Nous allons éviter, d’avoir de gros traitements contenus dans un unique acteur mais nous tenterons de le découper en petites fonctionnalités contenues dans des acteurs spécialisés (on retrouve ici un concept proche du principe de responsabilité unique de SOLID) formant ainsi une hiérarchie d’acteurs.
  • Nos messages devront être immutables pour rendre le modèle plus robuste (on évite de retomber dans les problèmes classiques de la gestion concurrentielle des données)
  • Un Acteur est une file d’attente, un état, un comportement, un statut et un ensemble d’Acteurs enfants

image

  • Un Acteur doit être codé de manière indépendante des autres acteurs et de leur emplacement (il est potentiellement sur un autre process/machine et ne partage donc pas forcément de la mémoire avec ses petits amis).

 

 

clip_image002

 

Un peu de code

Je vous propose un exemple simple de transformation (un mini ETL) d’une donnée qui passe par deux traitements distincts : une phase d’extraction et une phase de transformation, à l’extraction si les données en entrée (de simple chaines de caractères) sont fausses (la ligne commence par la lettre « a ») on l’envoie au traitement de rejet.

1. Le message

Ici, tous les messages seront du même type mais rien n’empêche d’en créer plus :

 

​
public class ETLMessage
{
    public ETLMessage(string message, int dataId)
    {
        Message = message;
        DataId = dataId;
    }

    public string Message { get; private set; }

    public int DataId { get; private set; }
}

C’est une classe standard chargée uniquement de porter des messages d’un acteur à l’autre. Dans notre exemple, nous n’avons pas besoin de stocker le contenu de la data et son identifiant.

2. La phase d’extraction

​
public class ExtractActor : TypedActor, IHandle<ETLMessage>
{
    IActorRef childActorTransform;
    IActorRef childActorError;

    public ExtractActor()
    {
        childActorTransform = Context.ActorOf(Props.Create<TransformActor>());
        childActorError = Context.ActorOf(Props.Create<ErrorActor>());
    }

    public void Handle(ETLMessage message)
    {
        if (message.Message.StartsWith("a"))
        {
            childActorError.Tell(message);
            return;
        }

        Thread.Sleep(3000);
        var newMessage = string.Format("> EXTRACT :#{0} -- {1}", message.DataId, message.Message);
        Console.WriteLine(newMessage);
        childActorTransform.Tell(message);
    }
}

Nous avons ici deux acteurs enfants :

· childActorTransform  chargé de la phase de transformation de la donnée à effectuer si tout se passe bien

· childActorError chargé de gérer les données en erreurs (qui commencent par « a »)

Ces deux acteurs sont « chargés » dans le constructeur en utilisant la méthode « Context.ActorOf » et non pas un « new » pour la simple et bonne raison c’est qu’ici vous allez manipuler une sorte de proxy (ou adresse) sur l’acteur fils. C’est très important car cela permet de déployer les acteurs dans des process/machines différents sans impacter le code (nous vous présenterons comment faire dans un autre article).

Une méthode « Handle » (imposé par l’interface IHandle) chargée de traiter le message. Son contenu est très simple : si le message est en erreur, on le renvoie à l’acteur chargé du traitement des erreurs, sinon on simule un traitement de 3 secondes et on passe le message à la transformation.

3. Gestion des erreurs et de la transformation

public class TransformActor : TypedActor, IHandle<ETLMessage>
{
    public void Handle(ETLMessage message)
    {
        Thread.Sleep(6000);
        var newMessage = string.Format(">>>>> TRANSFORM :#{0} -- {1}", message.DataId, message.Message);
        Console.WriteLine(newMessage);
    }
}

public class ErrorActor : TypedActor, IHandle<ETLMessage>
{
    public void Handle(ETLMessage message)
    {
        var newMessage = string.Format(">>>>>> IN ERROR :#{0} -- {1}", message.DataId, message.Message);
        Console.WriteLine(newMessage);
    }
}

Les acteurs sont très simples, et ne nécessitent pas d’explication. Ce qu’il faut néanmoins pointer c’est qu’à aucun moment nous n’avons produit du code pour la gestion de la file d’attente des messages (mécanisme inclus dans la classe TypedActor) ou encore liée à l’asynchronisme/parallèlisme des échanges (comme le mot clés async/await ou encore l’utilisation des Task).

4. Exécution du code

​
static void Main(string[] args)
{
    using (var system = ActorSystem.Create("ETL"))
    {
        IActorRef extractActor = system.ActorOf(Props.Create<ExtractActor>());
        var count = 0;
        Console.WriteLine("Messages à traiter:");

        while (true)
        {
            count++;
            string line = Console.ReadLine();
            var message = new ETLMessage(line, count);
            extractActor.Tell(message);
        }
    }
}

La première chose à faire est de déclarer un espace d’exécution (ActorSystem) pour hoster nos acteurs.

Ensuite, on instancie un acteur de type extract, et on lui passe des messages via la console :

clip_image004.jpg

Ici, j’ai saisi les données de manière aléatoire et rapproché ce que l’on peut constater :

· L’ordre de traitement des messages est respecté

· La saisie « aaa » ne prend pas de temps mais elle a dû attendre que le message #3 soit traité.

· Nous avons directement la sortie du traitement de l’erreur sans être bloqués par les autres traitements

· La transformation du message #2 a eu lieu pendant que je saisissais un nouveau message : la console n’est jamais bloquée (freeze).

Conclusion

Nous pouvons donc voir que les files d’attentes et l’asynchronisme sont respectés et que contrairement à un traitement classique (je finis une étape et j’envoie à la suivante) on a un comportement orienté « flux » . Lorsqu’un message est traité, il passe à l’étape suivante sans attendre la fin de tous les traitements. En cela, le fonctionnement est très proche de TPL dataflow, mais je suis un grand fan de la simplicité et l’élégance de la solution proposée par Akka. De plus, la possibilité de déployer les acteurs de manière indépendante les uns des autres offre des perspectives intéressante en terme de scalabilité et pour l’implémentation d’architecture orientée micro-services.

Je ne suis pas un grand spécialiste du F# mais mon intuition me dit que cette approche est particulièrement adaptée au langage F# (j’ai l’impression que la philosophie du F# colle bien avec celle d’Akka.Net)