Carl Hewitt a défini le modèle Acteur en 1973 comme :
La théorie mathématique qui fait des "Acteurs" la primitive universelle de la programmation concurrente.
Les acteurs ont été au centre de tendances récentes dans le domaine des architectures distribuées. Dernièrement, on a entendu beaucoup de buzz autour de ce sujet et de la place des acteurs dans les traitements concurrents sur le Cloud et dans le monde du Big Data. Si le modèle acteur a historiquement été principalement utilisé pour modéliser et mettre en place des traitements parallèles dans le cadre de processus uniques, exécutés sur une seule machine, il est à présent un modèle de choix pour les traitements hautement parallèles des environnements Cloud.
Dans cet article, nous définirons d'abord trois types d'acteurs et explorerons leurs rôles et responsabilités. Nous examinerons ensuite un paradigme consistant à découper les processus métier en un "réseau évolutif d'événements" au sein duquel toute étape "significative" de la logique métier génère un événement durable pouvant être traité plus avant, de façon indépendante, par un ou plusieurs acteurs. Nous jetterons aussi un oeil à quelques exemples d'utilisation réelle du modèle acteur. Nous finirons en étudiant les implications de l'adoption de ce pattern et en faisant la revue d'une implémentation s'appuyant sur la librairie BeeHive.
Le modèle acteur
Carl Hewitt, Peter Bishop et Richard Steiger, ont publié en 1973 un article qui proposait un formalisme identifiant une classe d'objets unique, les Acteurs, en tant que brique de base permettant de construire des systèmes faits pour l'implémentation d'algorithmes d'intelligence artificielle.
Un pattern similaire apparut dans le domaine des réseaux de neurones, où des noeuds font office de briques de base de ces réseaux, chacun de ces noeuds ayant un biais, un ou plusieurs inputs (avec leurs poids correspondants) et un ou plusieurs outputs (avec leurs poids correspondants).
La différence est qu'un acteur, contrairement au noeud du réseau de neurones, n'est pas juste une simple unité de traitement. Les acteurs sont de façon inhérente des unités autonomes au sein d'un réseau plus large qui sont déclenchées à la reception de messages.
Les Acteurs
D'après Hewitt, un acteur peut, en réponse à un message :
-
Envoyer un nombre fini de messages à d'autres acteurs
-
Créer un nombre fini d'autres acteurs
-
Décider du comportement à adopter à la reception du prochain message
Toute combinaison de ces actions peut se passer simultanément et en réponse à des messages arrivant dans n'importe quel ordre. Ainsi, il n'y a pas de contrainte sur l'ordonnancement et l'implémentation d'un acteur doit pouvoir traiter des messages arrivant de façon inattendue.
Les Acteurs "Processor"
Dans une description plus tardive du modèle acteur, la première contrainte est redéfinie comme la possibilité d'"envoyer un nombre fini de messages à l'adresse d'autres acteurs". L'adressage est une part intégrante du modèle qui découple les acteurs en limitant la connaissance qu'a un acteur des autres acteurs à un simple token (c'est-à-dire à une adresse). Les points de terminaison des Web Services, des files Publish/Subscribe et les adresses email représentent ce même concept classique d'adressage. Les acteurs qui répondent à un message en utilisant cette première contrainte peuvent être désignés par le nom de Processor Actor.
Les Acteurs "Factory"
La seconde contrainte permet aux acteurs de créer d'autres d'acteurs, ils sont donc appelés Factory Actors. Les acteurs de type Factory sont des éléments importants d'un système orienté-message où un acteur consomme depuis une file de message et crée des handlers en fonction du type de message. Les acteurs de type Factory contrôlent le cycle de vie des acteurs qu'ils créent. En comparaison, les Processors ne connaissent que des adresses. Il est utile de séparer les Factories des Processors, en accord avec le principe de responsabilité unique.
Les Acteurs "Stateful"
La troisième contrainte correspond au Stateful Actor. Les acteurs qui implémentent la troisième contrainte ont une mémoire qui leur permet de réagir différemment aux messages subséquents. De tels acteurs peuvent être sujets à une myriade d'effets de bord. D'abord, quand on parle de "messages subséquents", on admet un ordonnancement, alors que, comme cela a été dit, il n'y a pas de contrainte d'ordonnancement dans le modèle : l'arrivée d'un message inattendu peut conduire à des complications. Ensuite, tous les aspects du CAP theorem s'appliquent à cette mémoire, ainsi, avoir un état consistant et hautement disponible et tolérant aux partitions est impossible à atteindre. Pour résumer, il est préférable d'éviter les acteurs Stateful.
Comme cela a été proposé, un acteur peut agir en fonction de toute combinaison de ces trois contraintes, mais il faut souligner qu'un système d'acteurs avec des responsabilités uniques est préférable et conduit à une meilleure conception. Dans cet article, nous nous appuyons fortement sur cette séparation et proposons des solutions qui en tirent avantage. Nous montrerons un exemple d'une telle implémentation.
Les dépendances entre acteurs
Le modèle acteur, en contraignant les interactions d'un acteur, réduit la connaissance qu'un acteur a de son environnement. Pour un Processor Actor, il est suffisant de connaître l'adresse des acteurs se trouvant à sa suite dans la chaîne. On pourra même supprimer complètement cette connaissance en faisant générer par les acteurs un événement qui décrit (via le nom de l'événement) et qui documente (via l'état de l'événement), ce qui s'est passé. Le réseau d'acteurs peut récupérer l'événement à partir de son type. Un Factory Actor ne sait que créer des acteurs et leur passer les messages qu'il reçoit. Celui-ci contrôle le cycle de vie des acteurs qu'il crée mais il n'a pas conscience des détails du traitement effectué par les Processing Actors. En réduisant les dépendances, le couplage entre les éléments du système diminue, ce qui permet une meilleure évolutivité du système. Le comportement de chaque acteur peut être modifié indépendamment et en fait, chaque acteur peut être déployé indépendamment des autres. Réduire les dépendances est une des vertus les plus importantes d'un système à base d'acteurs. Ceci conduit à une simplification du design et réduit le couplage entre éléments d'une application. Tous les acteurs, en plus de tout cela, dépendent d'un ensemble non modifiable de paramètres de configuration qui leur donnent la connaissance nécessaire du milieu dans lequel ils s'exécutent. Ces paramètres peuvent comprendre divers aspects de l'environnement (des adresses d'endpoints) ou des comportements par défaut (comme le nombre de tentatives, les tailles de buffers, le niveau de verbosité des logs).
Les Acteurs "Réactifs"
Les acteurs peuvent être implémentés de façon impérative ou de façon réactive. Dans le cas des acteurs impératifs, un message envoyé par un acteur est un message RPC destiné à un acteur dont l'interface, le type et l'endpoint sont connus par l'envoyeur. Le message est typiquement délivré à travers des bus de messages durables. Ceci est très similaire à l'implémentation d'un Enterprise Service Bus où les artefacts déployables sont réduits à leur plus simple forme.
Dans le cas d'un acteur Réactif, l'envoyeur publie simplement un événement signifiant que le traitement métier est effectué (OrderAccepted, FraudDetected, ItemOutOfStock, ItemBackInStock, etc.) et les autres acteurs choisissent de souscrire à ces événements et d'effectuer leurs actions. Dans ce cas, les acteurs peuvent évoluer indépendamment et les processus métier être modifiés en ne changeant qu'un seul ou un petit nombre d'acteurs. Ceci est particulièrement adapté au développement de systèmes analytiques ou transactionnels sur les modèles de scaling horizontal existants sur le Cloud.
Des implémentations d'acteurs Réactifs existent déjà dans l'industrie. Le travail de Fred George sur les MicroServices réactifs en est un très bon exemple. Amazon Kinesis peut être vu comme un Framework d'acteurs réactifs à grosse granularité.
Modéliser un système basé sur des acteurs
Des systèmes exploitant le modèle acteur sont utilisés depuis les années 1970. Ces systèmes en ont tiré profit pour créer des systèmes hautement parallèles. Erlang est un des langages ayant incorporé ce modèle. Des applications développées en Erlang ont été utilisées avec succès dans le domaine des télécommunications depuis de nombreuses années.
De nos jours, avec l'informatique de commodité, le traitement et le stockage à bas coûts, il est possible de déployer un système sur des centaines ou des milliers de noeuds en appuyant simplement sur un bouton. Le challenge est de concevoir des systèmes qui peuvent tirer avantage de cette puissance. Une erreur courante dans l'adoption du Cloud est de suivre une approche lift and shift, c'est-à-dire de déployer sur le Cloud une architecture identique à l'architecture en tiers à demeure, avec une base de données RDBMS traditionnelle. Dans la plupart des cas, on perd simplement l'intérêt de réaliser une telle opération. Construire des systèmes qui peuvent accepter un scaling horizontal linéaire requiert de nouveaux paradigmes de modélisation des structures de données, des traitements et des dépendances.
Dans notre cas, nous voulons un modèle scalable horizontalement et linéairement, robuste et hautement disponible et restant facile à faire évoluer. Examinons divers aspects d'un tel système.
Le messaging
Les systèmes de messaging existent depuis longtemps dans l'industrie. Ils ont été un pilier de l'adoption d'SOA, avec les Enterprise Service Bus. D'autre part, HTTP définit une communication en termes de message de type Response, intervenant en réponse à un message de type Request. Un service SOAP répond à une requête SOAP en retournant un message SOAP. Même à plus bas niveau, avec le paradigme RPC, un appel de méthode peut être modélisé comme un message de requête et un message de réponse. Donc, qu'est-ce qui peut être différent ici, par rapport au messaging ?
Il suffit aux acteurs Processors de connaître le nom du sujet des messages qu'ils souhaitent lire et le nom du sujet à utiliser pour l'envoi de leurs messages. Les détails, tels que l'adressage, la distribution, le fail-over, etc, peuvent être confiés au broker de messages sous-jacent.
Pour construire un système d'acteurs robuste, il faut utiliser un mécanisme de messaging utilisant des sujets (topics), qui soit durable, hautement disponible, tolérant à la panne et scalable horizontalement. Apache Kafka et Windows Azure Service Bus sont des exemples de tels systèmes sur le Cloud. EventStore ou RabbitMQ sont des alternatives à demeure (ou sur le Cloud).
Messages, événements et faits
Un message est un élément d'information, destiné à un acteur. Traditionnellement, les messages sont constitués d'un en-tête et d'un corps. Cette même définition est applicable aux messages des acteurs. D'autre part, un événement est habituellement défini comme "quelque chose qui est arrivé dans le passé".
Les événements et les messages ont de nombreuses similarités. En fait, le livre Enterprise Integration Patterns définit un événement comme un sous-ensemble de messages, c'est à dire un message d'événement (EventMessage). Cependant, on y lit plus loin que la différence entre un Document Message (ce qui est habituellement désigné par le terme Message) et un Event Message est lié au temps et au contenu. Autrement dit, le parti pris est que la garantie de Delivery n'est pas importante pour un événement et les événements peuvent principalement être utilisés pour la Business Intelligence (c'est-à-dire à la mesure des métriques métier) où une précision absolue n'est pas nécessaire.
Il y a, à l'autre bout du spectre, des événements utilisés comme source unique de vérité d'un système. Les systèmes basés sur l'Event Sourcing sont les principaux représentants d'une telle approche. Toutes les autres données sont générées à partir de ces événements, qui sont soit stockés dans des modèles taillés pour la lecture et durables, soit dans des entrepôts de données volatiles, en mémoire.
Nous définissions un Event comme un élément d'information horodaté, immuable, unique et pour toujours vrai. Cette définition est similaire à celle d'un Fait, décrit par Nathan Marz. Nous constatons que tous les événements n'ont pas la même importance pour tous les domaines métier, c'est pourquoi les besoins relatifs au stockage et à la disponibilité sont différents. Par exemple, dans un scénario e-Commerce (amazon.com par exemple), un événement OrderSubmitted qui contient les détails d'une commande (variantes des produits et leurs quantités) est de la plus haute importance pour le métier, alors que les états transitoires de la commande (produits ajoutés, supprimés, quantités changées, etc.), bien qu'utiles pour les analyses, ne sont pas aussi critiques pour le métier.
De même, il n'est pas forcément utile de capturer toutes les données : stocker la position précise de la souris de l'utilisateur chaque milliseconde n'a pas d'utilité. Le métier a donc à faire des choix sur les données à capturer. D'un autre côté, stocker des données hautement disponibles coûte cher et il faut faire des distinctions sur les SLA en fonction des différents types de données. Ainsi, nous identifions deux types d'événements : les Business Events et les Log Events.
Les Business Events sont les briques de base du workflow métier et requièrent une cohérence et une durabilité au niveau transactionnel. Les Log Events, eux, sont principalement utilisés pour de la métrique métier et de l'analyse. Ils ne requièrent pas les mêmes niveaux de disponibilité. Les Business Events sont fondamentaux pour le coeur de métier (par exemple, les ventes en ligne dans le domaine de l'e-Commerce) alors que les Log Events correspondent aux données qui supportent la prise de décision, la relation client, le dépannage technique. Perdre un Business Event est considéré comme un incident sérieux, alors que la perte de Log Events, même si cela a son importance pour le business, n'aura pas d'impact catastrophique sur la fonction métier.
Les événements comme moyen d'inversion de contrôle
L'injection de dépendances (DI), en tant que pattern permettant de réaliser l'inversion de contrôle, est un pilier du développement applicatif traditionnel. Les événements, au sein d'une architecture faiblement couplée, permettent d'atteindre le même objectif que la DI au sein d'une application. En supprimant la connaissance qu'ont les consommateurs du producteur d'un événement, il est possible d'atteindre un même degré de découplage.
Ceci correspond également au Hollywood Principle, "Don't call us, we'll call you" utilisé dans le cadre de l'injection de dépendances et contraste avec l'utilisation d'une Commande, destinée à un handler unique.
S'appuyer sur des événements est lié aux principes de la programmation réactive, qui a nettement gagné en popularité ces dernières années. L'utilisation du Reactive Programming (comme les Reactive Extensions - Rx) simplifie les réseaux de dépendances au sein des systèmes applicatifs complexes. Les architectures réactives et event-driven (telles que décrites dans le Reactive Manifesto) aident à la mise en oeuvre d'architectures hautement découplées et évolutives.
Remplacer les commandes par des événements est le changement le plus fondamental par rapport aux approches traditionnelles des architectures SOA basées sur un ESB
Modéliser un événement
Comme nous l'avons décrit plus tôt, un événement est un élément d'information horodaté, immuable, unique et pour toujours vrai. Des exemples métier sont : CustomerCreated, OrderDispatched, BlogTagAdded, etc.
Il est pratique d'attribuer des types aux événements. Le type d'un événement décrit le genre d'information qu'il contient. Un type d'événement est normalement unique au sein du système.
Un événement se réfère habituellement à un ou plusieurs agrégats du système (voir plus loin). Par exemple, CustomerCreated contiendra naturellement un CustomerId, alors que BlogTagAdded contiendra le PostId et le tag (si le tag est modélisé comme Value Object ou le TagId (si c'est une entité).
Chaque événement, quelque soit son contenu, a besoin d'une identité à part entière. Il est possible que deux événements contiennent la même information, par exemple StaffTakenOffsick. Il est utile d'avoir recours à des GUID pour l'identité des événements. Dans certains cas, il est possible d'utiliser le type en ajoutant l'Id des identités (par exemple, il pourrait n'y avoir qu'un seul événement CustomerCreated pour un CustomerId donné), cependant, l'identité d'un événement devrait toujours être définie indépendamment de son contenu.
Les événements doivent capturer de façon "économe" toute l'information qui leur est liée. Lorsque l'on modélise pour de l'Event Sourcing, l'événement devrait capturer toute l'information nécessaire permettant de reproduire, à partir du flux d'événements pour une entité, l'ensemble des états par lesquels l'entité est passée. L'Event Sourcing est particulièrement important pour les analyses Big Data. Ici, toutes les informations sont intéressantes. ItemAddedToBasket est un événement important pour l'analyse, bien que cela ne soit pas le cas dans un système transactionnel tant que celui-ci n'est pas utilisé pour constituer une commande. D'un autre côté, un événement CustomerCreated, contenant l'ensemble des informations relatives à un Customer, est nécessaire aux systèmes Big Data où la donnée doit être stockée de façon indépendante, mais pas à un service Credit Check qui n'a besoin que de l'état courant du Customer.
Fondamentalement, pour un système transactionnel, on favorise la publication d'événements plus petits et on compte sur les structures de données classiques pour stocker le reste de l'état. Dans ce cas, les événements sont utilisés comme indicateurs de transition d'état à travers le système et l'état lui-même est stocké dans des entrepôts de données que nous aborderons rapidement plus bas. Cette approche nécessite que tout le système ait accès à ces entrepôts de données, hypothèse réaliste dans un scénario Cloud.
Le problème avec cette approche est que vous allez être amenés à écraser l'état des entités et donc, vos systèmes analytiques vont perdre des données de valeur. Vos systèmes analytiques ont aussi besoin de s'appuyer sur leurs propres entrepôts de données et n'accèderont donc pas aux bases transactionnelles. Il existe ici trois solutions :
- Vous disposez d'acteurs de type EventEnricher qui souscrivent aux événements, les enrichissent et publient des événements avec l'état enrichi à destination des systèmes analytiques. Dans ce cas, vous n'éliminez pas le risque de passer à côté de certaines transitions d'états, étant donné que l'enrichissement peut n'entrer en jeu qu'après plusieurs changements d'état. Dans la plupart des cas, ce n'est pas un problème pour les analyses.
- Vos acteurs publient deux événements : un destiné à être consommé par le transactionnel, un autre pour les analyses Big Data
- Vous abandonnez complètement vos entrepôts de données et optez pour l'Event Sourcing. À cause de la complexité accrue et des raisons données plus haut, nous ne recommandons pas cette option pour les systèmes principalement à vocation transactionnelle. Cette approche n'est pas non plus adaptée à certains types de structures de données, comme les compteurs, et peut résulter en une forte contention. Ce sujet mériterait un article à part entière.
Les queues
Les fonctionnalités de queuing requises par les Reactive Cloud Actors sont les suivantes :
- Haute disponibilité et tolérance de panne
- Support de souscriptions basées sur les sujets (topics)
- Capacité de réserver (lease) un message et ensuite de pouvoir l'abandonner ou le valider
- Capacité d'implémenter un délai
- Support du Long Polling
- Support optionnel du traitement en masse
Il existe des technologies, telles que Kafka, Azure Service Bus, RabbitMQ et ActionMQ qui fournissent toutes ou un sous-ensemble de ces fonctionnalités.
Autres structures de données
Les données des systèmes applicatifs traditionnels sont souvent modélisées en lignes et colonnes, comme dans un RDBMS. En fait, il n'est pas rare de voir des bases de données utilisées comme queues : lorsque tout ce dont on dispose est un marteau, tout ressemble à un clou. Pour les systèmes purement en Event Source, toutes les données sont des événements et sont stockées dans un Event Store, optionnellement avec snapshots. Dans une base de données orientée documents, les données sont stockées sous forme de documents, éventuellement accompagnés d'index. Quelque soit l'outil, choisir une structure de données adéquate est essentiel. Chaque structure de données est optimisée pour certaines opérations et essayer de faire entrer de force une donnée dans un modèle non adapté impliquera de faire des compromis.
Nous pensons que BigTable peut être utilisé pour exprimer toutes les structures de données séminales (autres que les queues qui sont implémentées par les Service Bus) permettant de construire un système Cloud transactionnel, horizontalement scalable tout en restant suffisamment performant. Il est à noter que Redis propose aussi toutes les structures de données séminales, il est dommageable cependant que celui-ci ne soit pas conçu pour le niveau de cohérence requis par les systèmes transactionnels.
Toutes les données stockées dans différents entrepôts ont besoin d'une identité. Pour cela, on associe un Id à chaque entité. Ainsi, chaque entité doit implémenter l'interface ci-dessous :
public interface IHaveIdentity { string Id { get; } }
Afin de répondre aux contraintes de la concurrence, les entités devraient être rendues conscientes de ces contraintes (être concurrency-aware) en implémentant l'interface suivante :
public interface IConcurrencyAware { string ETag { get; } DateTimeOffset? LastModified { get; } }
Les entrepôts de structures de données pourront vérifier si une entité implémente une telle interface et, si c'est le cas, vérifieront les conflits de concurrence en présence d'update ou de delete.
Nous allons maintenant passer en revue ces structures clés.
Clefs-Valeurs
La structure la plus basique est la structure clef-valeur, où une donnée arbitraire (généralement un tableau d'octets non-interprétés) est stockée en regard d'une clef. Les clefs peuvent être combinées pour créer des collections. Une alternative est de préfixer les noms de clefs. Habituellement, il n'est pas possible d'itérer sur les clefs, ce qui en fait un sérieux inconvénient. Les opérations sur les bases clef-valeur sont atomiques et peuvent parfois être (mais souvent ne sont pas) concurrency-aware. Cependant, des mécanismes de verrouillage devraient être fournis, avec timeout et nettoyage automatique.
Listes à clefs
Les listes à clefs sont similaires aux listes clef-valeur mais proposent de stocker une liste de clés-valeurs pour une clef donnée. La liste en regard d'une clef peut grossir de façon importante et peut être parcourue et mise à jour sans avoir à charger toutes les données.
Collections
Les collections sont des clefs-valeurs stockées par type logique de collection. La différence avec la simple clef-valeur est que la collection peut être parcourue, bien que cela puisse prendre du temps. Les clefs peuvent être utilisées pour des itérations de sous-ensembles de données. Les collections devraient proposer des mécanismes pour la concurrence (ETag et/ou date de dernière modification ou TimeStamp).
Compteurs
Les compteurs, comme le nom l'indique, portent un nombre pouvant être incrémenté ou décrémenté de façon atomique. C'est souvent la structure de données manquante de beaucoup de bases de données. À l'heure où cet article est écrit, il n'existe pas de compteur sur Windows Azure par exemple.
Modéliser des acteurs
Comme nous l'avons dit plus haut, nous avons besoin de concevoir des Processor Actors, des Factory Actors et nous laisserons de côté les Stateful Actors. Examinons chacun d'eux de plus près.
Processing Actor
Conception
Un Processor Actor est l'endroit où le processus métier se réalise, c'est pourquoi leur conception est de la plus haute importance. Ceux-ci doivent être conçus pour satisfaire tous les scénarios tout en gérant de façon inhérente la tolérance de panne requise par les environnements Cloud, où les défaillances sont relativement courantes.
Nous proposons l'interface simple ci-dessous pour les Processor Actor, que nous pensons couvrir tous les scénarios (code C#) :
public interface IProcessorActor : IDisposable { Task<IEnumerable<Event>> ProcessAsync(Event message); }
La méthode ci-dessus prend un événement et retourne une promesse de liste énumérable de 0 à plusieurs événements. Un acteur devrait généralement retourner 0 ou 1 événement. Dans certaines circonstances, il peut retourner plus de messages. Une Task est une promesse en C# et est utilisée pour l'implémentation de tâches asynchrones, avec le pattern async/await. Les processus Cloud sont généralement IO-bound (notamment à cause des lectures/écritures vers les queues ou les bases) et on ne soulignera pas assez l'importance des implémentations asynchrones basées sur les IO Completion Ports.
Les types d'événements et queues
Les Processor Actors souscrivent à un ou plusieurs types d'événements. Ceci est généralement fait en créant une souscription dédiée à un sujet (topic). Parfois, une simple queue est suffisante mais la nature des architectures de type event-driven fait que les événements crées pour un objectif deviennent utiles à d'autres objectifs. C'est pourquoi il est bon d'avoir un sujet et une souscription par défaut. Ceci dit, il est parfois nécessaire d'être explicite au sujet d'un événement lorsque celui-ci est utilisé en tant que décomposition d'un processus à multiples étapes. Un acteur s'enregistre généralement à un seul type d'événement. Toutefois, si l'étape correspondante possède des actions de compensation en cas d'erreur dans les étapes suivantes, c'est le meilleur endroit pour implémenter ces actions de compensation. Dans ce cas, l'acteur s'enregistre aux événements associés aux actions de compensation.
Les processus
Les acteurs doivent être conçus pour réaliser une seule chose et bien la faire. Dans le cas d'un crash, le message retournera dans la queue et sera à disposition des autres acteurs après la période de timeout. Les processus de reprise vont contrôler le nombre de tentatives pour le traitement d'un message et le délai permis après un échec. Un processus de Scatter-Gather peut parfois être découpé et des processus parallèles transformés en processus séquentiels et, pour chaque processus, de multiples événements et acteurs peuvent être définis. Cependant, ce n'est pas toujours possible à cause du délai introduit dans le processus. Ainsi, un processus de Scatter-Gather sera mieux conçu en faisant en sorte que chaque état du workflow soit implémenté en tant qu'entité (stockée dans une base de type Collection) contenant plusieurs flags. L'acteur responsable du Scatter lève plusieurs événements (un pour chaque étape), quand les processus éparpillés sont terminés, un événement est levé et un acteur positionne le flag approprié. Quand tous les flags sont positionnés, on peut avancer dans le processus. Il est très important d'implémenter l'état de ce workflow avec les conditions de concurrence en tête.
Découper un processus métier en plusieurs étapes apporte :
- De l'agilité et de la flexibilité pour changer le processus. Les étapes peuvent facilement être changées et des branches parallèles ajoutées ou supprimées.
- Des déploiements isolés. Chaque acteur peut être déployé indépendamment.
- Pour chaque tâche, un modèle de programmation et de test plus simple pour les développeurs.
- Un modèle de tolérance de panne plus simple pour les acteurs.
- Une meilleure maintenabilité, une meilleure traçabilité du code.
- La centralisation des aspects transverses comme le logging autour d'un seul appel de méthode.
Tout ceci conduit à une diminution du coût global du projet. Voici les inconvénients :
- Une latence augmentée à cause de passages plus fréquents dans les queues. Les Reactive Cloud Actors échangent la latence contre la simplicité et l'Eventual Consistency. Dans la plupart des configurations, la latence pour qu'un message traverse l'ensemble du processus sera aux alentours de quelques secondes.
- Le code sera dispersé dans de multiples implémentations, ce qui rend le suivi du processus dans le code plus difficile. Des diagrammes du workflow à jour sont obligatoires pour maintenir une bonne visibilité du processus, une tâche qui devrait incomber à une équipe de gouvernance et d'architecture.
- Des logs détaillés et des outils pouvant faire apparaître l'état des éléments traversant le système sont nécessaires. Lorsque vous construisez un système Cloud, vous auriez de toute façon à mettre ces éléments en place, cependant, l'effort pour le faire ne doit pas être sous-estimé.
Les Acteurs Factory
Un Factory Actor est responsable de :
- créer des Processor Actors et de gérer leur cycle de vie
- configurer du polling sur les queues pour recevoir des messages pour des souscriptions
- passer les messages reçus aux acteurs pour traitement
- au traitement réussi du message, publier en retour des événements vers la queue et marquer le message comme traité
- en cas d'échec, abandonner le message reçu ou relâcher la réservation. Les nouvelles tentatives peuvent être prises en charge par le Factory Actor ou configurées dans la queue.
Les Factory Actors font normalement partie de la librairie ou du Framework que vous utilisez et n'ont pas à être implémentés. Un Factory Actor utilise généralement l'injection de dépendances pour initialiser le Processor Actor avec ses dépendances. BeeHive est notre mini-Framework d'Acteurs réactifs pour le Cloud, nous vous proposons maintenant de vous en donner un aperçu.
BeeHive
BeeHive est une implémentation des Reactive Cloud Actors en C#. Le projet se concentre sur la définition d'interfaces et de patterns pour l'implémentation de solutions d'acteurs réactifs sur le Cloud, avec pour les queues et les structures de données basiques une implémentation Azure. L'adaptation pour les autres plate-formes Cloud représente un effort minime.
Les événements
Un événement est composé d'un corps (BeeHive utilise la serialisation JSON et bien que cela soit faisable, le besoin de changer cela ne s'est pas fait sentir) avec ses méta-données :
{ Task<IEnumerable<Event>> ProcessAsync(Event message); }
public sealed class Event : ICloneable { public DateTimeOffset Timestamp { get; set; } public string Id { get; private set; } public string Url { get; set; } public string ContentType { get; set; } public string Body { get; set; } public string EventType { get; set; } public object UnderlyingMessage { get; set; } public string QueueName { get; set; } public T GetBody<T>() { ... } public object Clone() { ... } }
L'implémentation des Acteurs
Afin de construire un acteur réactif, l'interface IProcessorActor doit être implémentée en prenant un événement en entrée et en retournant une promesse (Task) d'IEnumerable. L'acteur n'a pas à prendre en charge les erreurs inattendues puisque toutes les exceptions non attrapées seront prises en charge par le Factory Actor responsable de l'alimentation et du cycle de vie de l'acteur.
L'interface IProcessorActor inclut l'interface IDisposable qui offre à l'acteur l'opportunité de faire le ménage.
Puisqu'un acteur peut souscrire à différents types d'événements, il peut utiliser la propriété EventType de l'événement pour découvrir le type de l'événement.
La configuration des Acteurs
La configuration indique au Factory Actor comment créer, alimenter et gérer un Processor Actor. Étant donné que la configuration des acteurs peut être une tâche délicate et laborieuse, BeeHive fournit une configuration basée sur les attributs.
[ActorDescription("OrderShipped-Email")] public class OrderShippedEmailActor : IProcessorActor { public async Task<IEnumerable<Event>> ProcessAsync(Event evnt) { ... } }
Le nom de l'événement inclut en général deux chaînes de caractères séparées par un tiret où la première correspond au nom du type d'événement (qui est le même que le nom de la queue) et la seconde le nom de la souscription. Par exemple, si on définit "OrderAccepted-PaymentProcessing" pour le PaymentActor, cet acteur s'abonnera à la souscription PaymentProcessing du sujet OrderAccepted. Si la queue est une simple queue (n'utilise pas les sujets), seul le nom de l'événement est utilisé.
En implémentant l'interface IActorConfiguration, vous pouvez charger la configuration de l'acteur depuis un fichier ou une base de données.
public interface IActorConfiguration { IEnumerable<ActorDescriptor> GetDescriptors(); }
Si vous utilisez les attributs (c'est le défaut), vous pouvez créer une instance de la configuration comme suit :
var config = ActorDescriptors.FromAssemblyContaining<OrderAccepted>() .ToConfiguration();
L'exemple de Prismo e-Commerce
Prismo e-Commerce est un exemple d'implémentation non-triviale d'un processus métier avec BeeHive. Le processus métier commence avec la réception d'une commande et suit toutes les étapes de la commande jusqu'à ce que celle-ci soit, soit annulée, soit expédiée. Ce processus inclut l'autorisation de paiement, la détection de fraude, la vérification de l'inventaire, le réapprovisionnement en cas de besoin, etc.
Deux implémentations sont incluses dans les exemples de BeeHive, une en mémoire, une pour Azure.
Conclusion
Les Reactive Cloud Actors sont un paradigme centré sur les événements pour construire des applications Cloud faites d'acteurs indépendants qui fonctionnement purement en consommant et en publiant des événements. Contrairement aux acteurs impératifs qui envoient des messages RPC à des points de terminaison connus, un acteur réactif ne sait pas quels acteurs vont consommer ses messages. Un acteur réactif s'intéresse à un ou plusieurs types d'événements et réalise une activité unique atomique pour simplifier la tolérance de panne et les actions compensatrices. Tous les événements sont stockés dans des brokers de messages scalables horizontalement, généralement de façon durable.
Ce paradigme prend le parti des acteurs d'Hewitt et définit une triade distinguant les responsabilités de chaque acteur : acteur "Processor" (responsable du traitement des événements), acteur "Factory" (gère le cycle de vie des acteurs Processors, alimente les acteurs et retourne des événements) et l'acteur "Stateful". Les acteurs Processors sont spécifiques à l'application et réalisent les activités métier alors que les acteurs Factory sont fournis par le Framework. Les acteurs Stateful sont à éviter. Tout l'état est stocké dans des bases hautement disponibles sur le Cloud.
Les Reactive Cloud Actors sont associés à quatre structures de données de base pour le stockage de l'état : clef-valeur, collections, compteurs et listes à clefs. Les clef-valeur peuvent être implémentées avec de nombreuses technologies mais l'implémentation classique est S3. Les implémentations de type BigTable de Google (comme DynamoDB) sont capables d'implémenter les trois autres structures de données.
BeeHive est une implémentation en C# des Reactive Cloud Actors, proposée avec un exemple e-Commerce non trivial.
Cet article est inspiré par le travail de Fred George sur MicroSerives et les Reactive Event-Driven Architectures et lui est humblement dédicacé.
Au sujet de l'Auteur
Ali Kheyrollahi est un architecte de solutions, auteur, blogger, auteur et contributeur open source qui travaille actuellement pour un gros acteur de l'e-Commerce à Londres. Il est passionné par HTTP, les Web APIs, REST, DDD, la modélisation conceptuelle tout en restant pragmatique et en résolvant de vrais problèmes métier. Il a plus de 12 ans d'expérience dans l'industrie et a travaillé pour des entreprises "blue chip". Ali a un fort intérêt pour la vision par ordinateur, le machine learning et a publié quelques papiers sur le sujet. Dans sa vie précédente, il fut médecin et a travaillé pendant 5 ans en tant que médecin généraliste. Ali tient un blog et est un utilisateur avide de twitter @aliostad.