Comment mener des opérations de reconstruction d’un état du domaine dans une architecture Event-Driven ?
Context
Dans ce type d’architecture, la communication entre service passe par des brokers de message (bus-oriented ou topic-oriented) privilégiant le découplage. De plus, un Event (selon DDD) décrit un fait dans le passé et est contextualisé dans un domaine précis. Pour rappel, voici les 4 dimensions d’un event :
- Facts vs. delta events pertains to the event structure—the recording of a whole fact versus just the fields that have changed.
- Normalization vs. denormalization pertains to choices around event relationships, and the trade-offs between normalized and denormalized event streams.
- Single vs. multiple event types per topic focuses on the trade-offs between many topics with single event types, and a single topic containing multiple event types.
- Discrete vs. continuous event flows covers the relationships between events and their usage through workflows.
Il est donc très courant d’avoir une chaine de communication entre service où :
- Le
domaine A
consomme les Event un topic en amont - Le
domaine A
interprète le message pouvant entrainer une mutation, résultant à une modification en base - Le
domaine A
produit un Event décrivant la mutation effectuée - Le
domaine B
consomme l’Event, créant une chaine de consommation
The Challenge
Maintenant, dans l’éventualité où le domaine A
doit reconstruire son état en re-consommant le topic 1
(pour des raisons d’évolution d’une entité par exemple).
Dans notre cas, le fait de re-consommer des messages par le domaine A
publie systématiquement un Event consommé par le domaine B
.
C’est alors que plusieurs problématiques peuvent survenir :
- Event de maintenance : le
topic 2
contient des Event provoqués uniquement par une opération de maintenance - Chronologie des faits : le
topic 2
ne respecte plus la chronologie réelle des faits mais intègre le cycle de maintenance dudomaine A
- Augmentation de la volumétrie : le
topic 2
contient trop de message par rapport à la réalité des faits - Idempotence des consommateurs : le
domaine B
est dans l’obligation de traiter ces Event de manière idempotent, à savoir la maintien de son état peu importe le nombre de message déjà consommé de son côté.
Solution for Kafka
L’une des solutions est d’intégrer une capacité de rejeu par le consommateur du topic 1
dans le domaine A
et de préserver la cohérence du topic 2
.
La logique serait :
- Démarrer le consommateur en précisant l’ancien et le nouveau
group-id
(respectivementgroup-id-1
etgroup-id-2
) - Récupérer les offsets de toutes les partitions pour le topic de l’ancien
group-id
(icigroup-id-1
) - Connecter le consommateur sur le topic avec le nouveau
group-id
(icigroup-id-2
) - Récupérer les messages
- Si le message a déjà été consommé par l’ancien
group-id
(group-id-1
) avec la condition suivante : Message[partition/offset] < ancien group-id[partition/offset]- Si le message a déjà été lu par l’ancien
group-id
, traiter le message via une fonction du domaine silencieuse sans publication d’Event - Si le message n’a pas été lu par l’ancien
group-id
, traiter le message via une fonction du domaine publiant des Event
- Si le message a déjà été lu par l’ancien
- Confirmer la réception du message
- Boucler sur la consommation
Key concepts
Les points clés pour ce mécanisme de rejeu :
- Ne jamais muter manuellement l’offset d’un
group-id
car cela reviendrait à perdre de l’information et empêcherait un rollback en cas de problème - Pour re-consommer les messages d’un topic, créer un nouveau
group-id
- Récupérer le point de consommation du
domaine A
et afin de distinguer les messages déjà consommés et les nouveaux messages - Traiter les messages déjà consommés avec une fonction de maintenance, ne publiant aucun Event du domaine
- Traiter les nouveaux messages avec une fonction du domaine publiant des Event
De cette façon, nous garantissons que les topics d’un domaine contiennent uniquement des messages respectant la chronologie des faits, et ce, pour tous les consommateurs.