Événements et commandes : démêler l'architecture orientée événements dans un système Rails
Deux contrats très différents se cachent derrière l'étiquette « orienté événements ». Bien choisir les noms détermine le comportement du reste de l'architecture.
Publié le 13 octobre 2024 · 5 min de lecture
« Orienté événements » sert d'étiquette à deux architectures qui se comportent très différemment sous charge et en cas de panne. Avant de nommer une queue ou un topic, il vaut la peine de préciser laquelle des deux vous construisez réellement.
Une commande ordonne, un événement informe
Une commande nomme une action que vous voulez voir exécutée et s'attend à ce qu'elle le soit : CreateInvoice, CancelSubscription. Elle est adressée à un seul destinataire, qui a le droit de la refuser. Un événement décrit quelque chose qui s'est déjà produit, au passé : OrderPlaced, InvoicePaid. Il n'est adressé à personne en particulier ; l'émetteur ne sait pas, et ne devrait pas se soucier de savoir, qui écoute.
Là où les équipes se brûlent
L'erreur la plus courante consiste à publier quelque chose qui porte un nom d'événement mais qui a la forme d'une commande, « SendWelcomeEmail » par exemple, puis à s'étonner que le service d'une autre équipe agisse dessus d'une manière qui couple les calendriers de livraison des deux équipes. Si le comportement d'un consumer est nécessaire pour que votre processus métier soit correct, vous émettez une commande et devez traiter la garantie de livraison en conséquence. Si le processus est correct que ce consumer réagisse ou non, c'est véritablement un événement.
Rails, ActiveSupport::Notifications et leurs limites
À l'intérieur d'un seul process Rails, ActiveSupport::Notifications vous offre le pattern événementiel sans effort : on publie une fois, zéro ou plusieurs abonnés réagissent, et aucun d'eux ne bloque les autres ni l'émetteur.
ActiveSupport::Notifications.instrument("order.placed", order_id: order.id)
ActiveSupport::Notifications.subscribe("order.placed") do |*, payload|
LoyaltyPoints.credit(payload[:order_id])
end
Ce pattern fonctionne très bien dans un même process et s'effondre dès qu'un abonné doit survivre à un redémarrage ou vivre dans un autre service. C'est le moment de recourir à un transport durable, et celui où le pattern outbox prend tout son sens.
Le nommage est la décision de conception
Dans la pratique, la revue d'architecture qui compte le plus n'est pas « Kafka ou RabbitMQ ». C'est d'obliger chaque équipe à écrire, pour chaque message, si elle émet une commande ou annonce un fait. Tout le reste, retries, ordre, rejeu, découle de cette réponse.
Articles liés
Le pattern outbox dans Rails : publier des événements sans perdre de données
L'écart entre l'enregistrement d'une donnée et la publication du message qui l'annonce est l'endroit où les événements disparaissent discrètement. Voici comment le combler.
Idempotence par conception : rendre vos jobs Sidekiq sûrs à rejouer
Sidekiq garantit une livraison au moins une fois (at-least-once), pas exactement une fois. Tout job qui n'est pas sûr à exécuter deux fois est un bug qui attend le bon chevauchement.