Le Cloud Microsoft est aujourd’hui l’un des cloud publics incontournables avec AWS, et propose une panoplie déjà bien complète de service Serverless au service du développeur. Pour rappel, le Serverless est une technique permettant de pousser du code instantanément sur une infrastructure totalement gérée de A à Z par le prestataire externe et mise à l’échelle automatiquement selon la demande. Le développeur n’a ainsi plus besoin de se soucier de la puissance des serveurs ou même du système sous-jacent. Ce genre de service proposé optimise également les coûts des applicatifs puisque les entreprises paient selon la consommation via un plan de facturation ‘Pay-as-you-go’. Si le service ne fait rien, aucune facturation ne sera émise.
Azure propose ce type de service via les Azure Functions. En C#, Javascript, Java ou encore Python, le développeur est en mesure d’héberger du code fonctionnel en quelques minutes sans se préoccuper de l’infrastructure sous-jacente. Microsoft a poussé le concept plus loin encore en proposant un SDK permettant d’orchestrer des fonctions afin de concevoir des applications Serverless plus complètes et avec plus de maîtrises : les Durable Functions. Cet article présentera d’abord les concepts généraux sur lesquels reposent les Durable Functions, puis nous décortiquerons le SDK afin d’explorer ses possibilités. Enfin, nous expliquerons divers patrons de conception relatifs à ces concepts.
La pattern Actor Model
Le principe d’un acteur est simple, il faut le voir comme étant une unité logique de votre application. Ensuite, le pattern repose sur la notion de messages. Cette notion de message est très importante, car c’est ce qui permet d’avoir des acteurs suffisamment indépendants pour fonctionner tout seul. On peut voir les acteurs comme étant des fonctions ou encore des unités de traitement type processus système. Les acteurs ont la capacité de :
-
Stocker des données propre à chaque acteur ;
-
Recevoir des messages depuis d’autres acteurs ;
-
Envoyer des messages vers d’autres acteurs ;
-
Créer de nouveaux acteurs ;
C’est avec ces règles simples que l’on construit un cadre d’exécution sain et terriblement efficace. L’autre force de ce patron de conception est que les messages sont envoyés et livrés de manière asynchrone, c’est-à-dire que deux acteurs différents peuvent tout-à-fait traiter des messages de manière parallèle sans s’attendre l’un et l’autre. Cependant, chaque acteur ne peut traiter qu’un seul message à la fois.
Le principe de l’acteur permet ainsi une mise à l’échelle efficace, puisque le parent va ainsi créer autant d’acteur enfant qu’il a besoin pour traiter sa requête. Le parent va ensuite gérer sa propre exécution en fonction des retours des différents enfants.
Attention cependant, le pattern Actor model n’est pas adapté à tous les cas d’usages, par exemple si vous avez besoin de traitement synchrone dans votre application. Si tel est le cas, cela veut probablement dire que vous n’avez tout simplement pas besoin de ce genre de pattern. Ensuite, ce cadre d’exécution n’est pas particulièrement adapté pour l’exécution via transaction, et notamment pour les rollbacks. En effet, imaginons une transaction bancaire avec retour en arrière si une erreur se produit, il devient alors difficile et fastidieux avec de simple retour de message de traiter ce cas d’usage si plusieurs acteurs ont déjà été mis en œuvre lors de la transaction. Heureusement, pour ces points manquants, divers Framework et SDK intègre des mécanismes permettant de pallier à ces manques (par exemple Akka).
Nous venons de voir les concepts généraux autour du pattern Actor model. Ces bases sont essentielles à la compréhension du fonctionnement des Durable Functions. Nous allons maintenant explorer d’autres patrons de conception, reposant sur le principe des acteurs, et pouvant être mis en œuvre via les Durable Functions dans Azure.
Dans les profondeurs du SDK
Au sein du SDK Durable Functions, il existe plusieurs types de fonctions que le développeur va devoir créer afin de les orchestrer correctement :
-
Client function : c’est le point d’entrée de votre solution d’orchestration. C’est elle qui va lancer le premier orchestrateur et ainsi lancer tous vos autres processus ;
-
Orchestrator function : cette fonction représente le cœur de l’orchestrateur de votre système. C’est cette dernière qui s’occupe de lancer les fonctions les unes après les autres, ou en parallèle, ou de conditionner leurs lancements et ainsi de suite… Les orchestrateurs sont des fonctions C# simple, mais un certain nombre de bonnes pratiques doivent être respectés. Par exemple, aucun appel HTTP doit être effectué dans cette fonction. L’ensemble des préconisations se trouve ici : https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-code-constraints ;
-
Activity function : plus petite unité de traitement et indépendante du système orchestré. Ces fonctions sont les tâches qui sont orchestrés les fonctions d’orchestrations. Ces fonctions ne sont pas limitées comme le peuvent être les fonctions d’orchestrations. C’est donc elles qui doivent effectuer les traitements HTTP ou les calculs nécessitant plus de mémoire ou de CPU ;
Entity function : opération de lecteur / écriture de l’état durable de l’orchestration. Lors du traitement de l’orchestrateur, l’application a besoin de stocker de l’état en mémoire de manière transverse à tous les acteurs. Ces fonctions sont là pour assurer l’atomicité de l’opération vers un stockage sécurisé et prévu pour les Durable Functions.
Chacune des fonctions sont activables selon un binding Azure Functions bien précis.
Depuis l’orchestrateur, on retrouve la classe DurableOrchestrationContext (ou IDurableOrchestrationContext pour la V2) et on y retrouve les méthodes suivantes :
-
CallActivityAsync : permet d’appeler une Activity Function avec son nom et des paramètres si besoin ;
-
CallActivityWithRetry : appel une Activity Function avec possibilité de relancer si jamais la fonction à échoué ;
-
CallSubOrchestrator[WithRetry] : appel un autre orchestrateur lui permettant lui aussi d’orchestrer des fonctions à sa guise. Cette méthode possède également sa version avec les options de relances ;
-
WaitForExternalEvent : attends qu’un événement externe à l’orchestrateur se produise. Ces événements sont notamment lancés via la méthode RaiseEventAsync dans les fonctions sous-jacentes.
Ces méthodes sont la base de l’orchestration avec les Azure Functions. Avec ceci, on retrouve plusieurs binding Azure Functions nous permettant d’activer ces fonctions :
-
[OrchestrationClient] ou [DurableClient] : binding de démarrage permettant d’initier une fonction d’orchestration. Le nouveau trigger [DurableClient] (Azure Function 2.x) permet également d’activer des fonctions d’entité ;
-
[OrchestrationTrigger] : binding d’orchestration permettant à la fonction de démarrer des acteurs ;
-
[ActivityTrigger] : binding d’activation d’une fonction d’acteur appelé par un orchestrateur ;
[EntityTrigger] : binding d’activation d’une fonction entité permettant de modifier de l’état persistent dans l’application ;
Un peu de code
La première fonction à créer est le client d’orchestration. Ce dernier va s’activer selon un événement externe (appel HTTP, Service Bus …) et va lancer le premier orchestrateur.
L’Azure Function ci-dessus est déclenché suivant un appel HTTP de type GET sans authentification particulière. Elle accepte également un binding [DurableClient] permettant d’injecter un objet de type IDurableOrchestrationClient afin de lancer l’orchestration. La fonction StartNewAsync permet de lancer la fonction d’orchestration HelloWorld.
Nous allons ensuite développer notre fonction d’orchestration. Cette dernière va nous permettre de lancer plusieurs fonctions d’activité selon notre cas métier.
La fonction effectue 2 actions principales :
-
Récupération du paramètre d’entrée via GetInput ;
-
Lancement d’un acteur appelé SayHello via la méthode CallActivityAsync ;
C’est ici que le développeur va intégrer sa logique d’orchestration avec des conditions et des boucles selon ses cas métiers. On peut notamment imaginer de lancer plusieurs acteurs en parallèle si besoin : chaque acteur devenant une Azure Function, le développeur profite alors d’une puissance de calcul et de traitement quasi infini.
Une fois l’acteur lancé, ce dernier va récupérer le paramètre et renvoyer un résultat sous forme de chaîne de caractère modifié.
Depuis un client orchestrateur, il est également possible d’appeler une fonction d’entité pour modifier l’état de l’application. La fonction ci-dessous modifie l’état d’une valeur placé sous l’effigie d’une entité Counter.
Ensuite, la fonction modifiant l’état peut prendre plusieurs formes :
Une fonction analysant l’opération souhaité et utilisant des méthodes GetState et SetState pour modifier l’état ;
-
Une classe définissant une propriété représentant l’état et les opérations comme des méthodes.
La version avec la classe paraît la plus robuste en termes de typage, mais la première version permet des scénarios plus génériques si besoin.
Avec l’ensemble de ces fonctions et leurs particularités, le développeur est en capacité de proposer une solution entièrement Serverless et orchestré avec finesse afin de répondre aux besoins métier. La puisse du Cloud vient ensuite s’ajouter à ce type d’architecture pour proposer une solution totalement scalable et encaissant la charge de manière très réactive.
Durable Functions et patterns
Le premier type d’architecture possible est le Function chaining. Ce dernier permet de lancer des fonctions dans un ordre bien précis afin d’orchestrer leurs exécutions. La sortie d’une fonction sera le paramètre d’entrée de la suivante, et ainsi de suite.
A noter : tous les schémas de cette section sont également disponibles sur la documentation officielle de Microsoft
Le code est ainsi très simple et enchaîne simplement les fonctions les unes après les autres.
Le deuxième type d’architecture est le l’opposé de celui vu à l’instant : la parallélisation. Avec Les Durable Function, il est aisé de lancer plusieurs acteurs de manière parallèle afin de profiter de toute la puissance du Cloud pour accélérer le traitement. Le SDK permet ensuite d’attendre que toutes les tâches aient terminés avant de continuer.
Le code ci-dessous illustre le schéma.
La clé ici est l’attente de tous les acteurs via la méthode Task.WhenAll. Par la suite, le code récupère le résultat de chaque tâche via la propriété Result de chaque Task.
Lors d’un traitement long, il est commun de développer un mécanisme permettant de suivre la progression du traitement afin de savoir précisément s’il est encore en cours de fonctionnement ou s’il a terminé.
Les Durable Functions intègrent déjà ce genre de fonctionnement. En effet, lorsque chaque orchestrateur démarre, il est possible de récupérer plusieurs points d’entrées, sous la forme d’URL, et ainsi en les appelant on récupère des informations relatives à l’état de l’orchestrateur. Une fois l’orchestrateur lancé, il faut utiliser la méthode CreateCheckStatusResponse afin de récupérer ces informations. Par exemple, l’URL suivante permet de récupérer le statut d’un orchestrateur avec l’ID b79baf67f717453ca9e86c5da21e03ec lancer dans l’application myfunc.
https://myfunc.azurewebsites.net/runtime/webhooks/durabletask/b79baf67f717453ca9e86c5da21e03ec
Dans le même esprit que la surveillance de l’orchestration, il est possible de créer une fonction qui vient scruter l’état d’un orchestrateur afin de lancer les actions en conséquence. Par exemple, si l’orchestrateur tombe en erreur, la fonction de surveillance pourrait lancer une alerte et ainsi notifier une administrateur qu’une erreur s’est produite
Ce pattern permet d’automatiser la surveillance des processus longs et de réagir en conséquence.
Ensuite, il est également possible d’attendre une notification externe avant de continuer le processus d’orchestration. En effet, le SDK intègre un mécanisme permettant de notifier l’orchestrateur d’un événement externe. On peut imaginer une interaction humaine via une interface Web ou autre.
Cette notification s’effectue via un appel HTTP vers l’Azure Function. Par exemple, l’URL suivante lance un événement intitulé ApprovalEvent, et permet ainsi à l’orchestrateur de continuer son processus.
http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}/raiseEvent/ApprovalEvent
L’attente de ce type de notification se fait via la méthode WaitForExternalEvent depuis l’objet IDurableOrchestrationContext. On lui passe alors en paramètre le nom de l’événement attendu, ici ApprovalEvent.
Task<bool> approvalEvent = context.WaitForExternalEvent<bool>("ApprovalEvent");
Enfin, le dernier type d’architecture possible est l’agrégation de données entrantes dans l’application. Une Azure Function peut récupérer des données en entrées de manière massive puis peut utiliser une Durable Entities pour mettre à jour l’état de l’application au fur et à mesure que les données arrivent.
Les Durable Entities vont s’occuper de gérer l’accès concurrentiel de manière efficace afin de na pas perturber le flux de traitement. L’Azure Function ci-dessous écoute les événements d’un Event Hub puis agrège le flux dans une Durable Entites intitulé Counter.
Les Durable Function sont construites à partir du Durable Task Framework disponible en open-source sur GitHub : https://github.com/Azure/durabletask.
Conclusion
Les Durables Functions permettent au développeur de construire des applications avec traitement long et de les gérer de manière efficace au travers du SDK. Ce dernier élimine la majorité des difficultés rencontrés et permet au développeur de se concentrer uniquement sur son code afin d’apporter au mieux la valeur ajouté métier dont il a besoin.
Ce paradigme de programmation se fond totalement dans l’infrastructure Cloud Azure via les Azure Function. Cette parfaite intégration permet aux applications de disposer d’une puissance de calcul très grande et ainsi de répondre à de fortes charges. Cette scalabilité est un atout majeur en faveur de ce type d’architecture. Cependant, il n’enlève en rien la responsabilité du développeur de construire un algorithme prêt à être mis à l’échelle, car dans le cas contraire l’utilisation de ce type de SDK peut s’avérer être une vraie plaie.