Démo
FrançaisEnglishEspañolDeutschItaliano

Une architecture pilotée par les événements - Partie 2

6 min de lecture

Romuald Texier-MarcadéIngénieur R&D chez Allo-Media / uh!ive

Dans l’article précédent de cette série, nous avons expliqué pourquoi nous avons abandonné notre ancienne architecture, basée sur des services REST synchrones, au profit d’une architecture event driven entièrement asynchrone.

Aujourd’hui, nous abordons les principes de conception fondamentaux qui ont été déterminants dans la réussite de ce chantier.

Business services et data processing services

Nous distinguons les business services des data processing services (ou services utilitaires), pour séparer proprement la logique métier de la complexité du traitement des données.

Data processing services

Les data processing services doivent être des services purs et stateless qui réalisent un traitement algorithmique des données (calculs, transformations…). Ils sont aussi indépendants du contexte : ils ne doivent dépendre ni de règles métier, ni d’hypothèses, ni de sources de données externes. Tout ce dont ils ont besoin pour leur traitement doit se trouver dans le message qu’ils reçoivent. Ils ne doivent pas avoir à interroger un tiers pour obtenir des données supplémentaires. Les data processing services sont en quelque sorte des bibliothèques universelles, et peuvent même être fournis par des tiers.

Exemples de data processing services :

  • Un service de speech to text a de nombreuses applications. Il n’a besoin en entrée que de l’audio et d’une référence de langue. Il n’a aucune donnée à persister.
  • Un service de génération de vignettes d’images. Il ne prend en entrée qu’une image et des dimensions cibles. Il n’a pas d’effet de bord et peut servir dans de nombreux métiers.

Business services

Les business services implémentent les workflows des clients et se concentrent uniquement sur les règles et les exigences métier, pour orchestrer et réaliser la valeur ajoutée sur l’audio et les données de nos clients. Pour cela, ils utilisent les data processing services comme une bibliothèque. Ils nous sont très spécifiques : on ne voudrait jamais externaliser ses business services.

Les business services persistent les données qu’ils produisent et en sont l’unique source de vérité.

Les business services construisent et maintiennent leur propre configuration client à partir des événements du bus.

Exemples de business services :

  • Chez Allo-Media, nous avons un business service qui tague les appels entrants. Il écoute les transcriptions d’appels et publie des tags de qualification. Il connaît les besoins de nos clients et tague les appels en conséquence. Il persiste les tags et en est l’unique source de vérité.
  • Un service de panier pour une boutique en ligne. Pour chaque utilisateur connecté, il maintient l’état de son panier en écoutant des événements d’UI comme ItemSelected, ItemRemoved ou des événements de stock comme ItemStockLeft…

Tous les services doivent être idempotents : s’ils reçoivent deux fois exactement le même message, ils doivent se comporter de la même façon et produire les mêmes sorties.

Événements, commandes et résultats

De la même façon que nous avons deux types de services, nous avons deux types de messages : les commandes (et leurs résultats) et les événements.

Événements

Les événements sont des messages métier publiés sur le bus par les business services pour annoncer au reste du monde ce qui s’est passé.

Un business service est propriétaire des types d’événements qu’il émet. Il ne sait rien des services qui vont les traiter. Il s’abonne aux types d’événements dont il a besoin, mais ne sait rien de leur origine.

Un type d’événement définit le sens des événements de ce type et leur schéma de données. Il doit être documenté.

Le type d’un message d’événement est donné par son nom (aussi appelé routing key, parce qu’il sert au routage des abonnements). Le nom du type d’événement doit être de la forme SubjectPastParticiple. Par exemple ConversationStarted, CustomerCreated, ShoppingCartValidated… Si vous n’arrivez pas à nommer immédiatement votre type d’événement, c’est qu’il est mal défini, ou que ce n’est pas un événement. Peut-être faut-il affiner votre service ou le découper, faute d’avoir analysé votre chaîne de valeur assez en profondeur ?

Commandes et résultats

Les commandes sont des messages utilitaires consommés par les data processing services. Imaginez une commande passée à un fournisseur : vous ne savez pas qui va la traiter, ni comment, ni quand, mais vous recevrez ce que vous avez demandé dans votre boîte aux lettres un peu plus tard.

Un data processing service est propriétaire des types de commandes qu’il consomme et de leurs résultats. Une commande est toujours adressée au service logique qui en est propriétaire.

Un type de commande définit le sens de la commande, son schéma de données et le schéma de données de son résultat. Il doit être documenté.

Le type d’un message de commande est donné par son nom, de la forme VerbObject. Par exemple : AnnotateText, TranscribeAudio… Comme les commandes sont adressées à un service logique précis, la routing key d’une commande est de la forme logical_service_name.commandname. Par exemple : asr.TranscribeAudio.

La commande contient l’« adresse » de retour à laquelle envoyer le résultat, et une référence fixée par l’émetteur, renvoyée telle quelle avec le résultat de la commande. Cette référence s’appelle l’identifiant de corrélation (correlation ID), et elle est essentielle pour l’émetteur : comme toutes les communications sont asynchrones, le service qui a envoyé la commande a besoin d’un moyen de rapprocher le résultat reçu de sa requête initiale.

Un résultat est un message associé et propre à chaque commande, qui contient le résultat du traitement (succès ou erreur) et l’identifiant de corrélation. Un message de résultat ne peut pas exister sans commande préalable.

Les résultats d’erreur sont prévus et documentés : ce sont des erreurs « normales », pas des rapports de bug. Les exceptions dues à des bugs ne doivent pas renvoyer de résultat d’erreur. En cas d’erreur inattendue, le service remet la commande reçue en queue pour la retenter une fois, et si la deuxième tentative lève encore une erreur inattendue, le message est rejeté et part dans la dead letter queue pour investigation. Les exceptions sont toujours loguées.

Logs

Nous pouvons aussi avoir des messages de log, pour collecter facilement les logs applicatifs.

Tous les messages qui découlent « en cascade » d’un même événement source partagent un identifiant commun, appelé identifiant de conversation, qui a les propriétés suivantes :

  • il est unique dans le temps ;
  • il est créé par un événement (jamais par une commande) publié pour des raisons extérieures au bus, et non en réaction à d’autres événements ; nous appelons cet événement l’événement initial.
  • Tout message (événement, commande ou résultat) créé en réaction à un autre message M reprend tel quel l’identifiant de conversation de M.

Tous les messages issus d’un même événement initial partagent le même identifiant de conversation, et aucun autre événement ne le porte. Nous pouvons ainsi facilement tracer et débugger le pipeline réel de chaque appel entrant, par exemple.

Enfin, les schémas de messages doivent être forward compatible :

  • une nouvelle version du schéma d’un message pour une application peut ajouter des champs, mais ne doit ni supprimer ni redéfinir les champs existants ;
  • l’implémentation d’un décodeur de messages doit ignorer les champs inconnus sans planter.

La documentation détaillée des messages doit être tenue à jour, dans un endroit facilement accessible aux développeurs.

Dans le prochain article de cette série, nous verrons comment nous avons mis en œuvre ces principes et ces comportements dans l’architecture réelle.

Mots-clésAI algorithmsAllo-MediaanonymizeAPIaudiobusinessesconversationscustomersindexnamed entitiesNLPphonepipelinequalifysemantic tagstag transcriptstranscribeUIvalue chain

Partager

Articles similaires

Tous les articles