Dans EventBridge, "API destinations" est une fonctionnalité permettant d'appeler n'importe quelle API HTTP en réponse à un événement.
Cette fonctionnalité est hyper-puissante, puisqu'elle permet de bénéficier de la gestion du retry par EventBridge et de la DLQ en cas d'échec, mais aussi de la transformation des événements à la volée (Input Transform), ainsi que l'authentification auprès de l'API tierce, le tout pour un prix ridiculement bas (1 $/million d'événements ingérés + 0,20 $/million d'événements transmis à une destination).
J'aime les utiliser (voir par exemple ici) pour envoyer des événements vers des systèmes tiers. Lorsqu'un traitement intermédiaire portée par une Lambda est nécessaire, passer par API Gateway permet d'invoquer la Lambda de manière synchrone et de bénéficier ainsi des fonctionnalités de retry évoquées ci-dessus.
Cependant, cette fonctionnalité comporte une limitation majeure : le temps de réponse de l'API appelée doit être inférieur à 5s ! Au-delà, EventBridge considère l'appel en échec.
Il est parfois déjà assez difficile de convaincre des développeurs que leur API doit respecter le délai d'expiration de 29 secondes imposé par le service API Gateway (et bien que ce soit une bonne pratique, c'est un problème si fréquent - et douloureux - qu'AWS a récemment rendu possible d'augmenter le timeout), alors essayer de leur vendre un timeout de 5 secondes est peine perdue !
Pour être juste envers les devs, bien souvent, nous devons également transmettre des événements à des systèmes tiers sur lesquels nous n’avons aucun contrôle et qui ne fournissent pas de SLA en termes de temps de réponse.
Un de mes clients avait besoin d'appeler un système qui répondrait en moins de 2 secondes la plupart du temps... mais en plus de 10 secondes 3 à 4 % du temps.
Contourner la limite d'EventBridge !
Même si EventBridge cesse de suivre la réponse à notre appel, la Lambda invoquée continuera à faire son travail jusqu'à ce qu'elle atteigne son propre délai d'exécution (maximum 15 minutes).
L'avantage est que ça nous permet de faire le job. L'inconvénient, c'est qu'EventBridge effectuera une nouvelle tentative (considérant que l'appel était un échec).
Le re-jeu d'un événément n'est pas un mal en soi, au contraire : on veut un re-jeu en cas de vrai échec, mais cela peut avoir de mauvaises conséquences si notre requête précédente est toujours en cours de traitement (nous pourrions surcharger l'API de destination) ou bien a terminé (le traitement de destination n'est peut-être pas idempotent et gèrera mal le re-jeu).
J'ai ainsi commencé à réfléchir à une manière de stocker l'état et la réponse des requêtes dans une table DynamoDB et d'interroger cette table avant de déclencher un nouvel appel. J'ai écrit du code, un peu au début puis cela a commencé à devenir non-trivial, voire même assez complexe, car nous devons gérer les différents statuts possibles d'une requête (non lancée, en cours de traitement, terminée avec succès, échouée avec des erreurs pour lesquelles un re-jeu peut être bénéfique (409, 429 et 5xx) ou pas) et nous assurer (i) qu'il n'y a pas d'exécution simultanée en cours, et (ii) que les erreurs sont bien remontées à EventBridge afin de pouvoir bénéficier de ses fonctionnalités de re-jeu ou de persistance en DLQ).
J'ai alors pensé que Lambda PowerTools pourrait être d'une grande aide. Powertools pour AWS Lambda est une boîte à outils de développement permettant de mettre en œuvre les meilleures pratiques Serverless et d'accélérer les développements logiciel. PowerTools contient une fonctionnalité géniale appelée Idempotency (disponible en Typescript, Python, Java or .NET) qui fait exactement ce dont j'avais besoin.
L'idempotence est une propriété-clé que les traitements doivent présenter dans une architecture orientée événements. Avoir des traitements idempotents permet de remédier au fait qu'en raison de la nature distribuée d'EventBridge, certains événements peuvent parfois être diffusés deux fois.
La fonctionnalité Idempotency protège les charges de travail en aval en enregistrant tous les appels dans une table DynamoDB, avec leur statut et leur réponse. Cela permet de bloquer des requêtes identiques concurrentes et de mettre en cache les réponses afin de pouvoir les récupérer sans ré-exécuter les traitements.
Mise en œuvre de l'idempotence de Powertools pour répondre à notre cas d'utilisation
PowerTools est très bien documenté et facile à utiliser. Dans l'exemple suivant,
- J'utilise l'ID d'événement EventBridge, encapsulé dans le corps de l'événement API Gateway, comme clé pour mettre en cache l'état et la réponse de l'appel API.
- Je stocke les données d'idempotence dans une table DynamoDB
(Si j'avais besoin de gérer plusieurs destinations, je pourrais utiliser une clé composite utilisant l'ID d'événement et l'ID de destination de l'API par exemple, pour traiter l'appel de chaque destination pour l'événement)
Pour tester cela, j'ai simplement ajouté une pause de 180 secondes dans ma section « logique métier ».
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
await delay(180000);
Lorsque je transmets un événement à Eventbridge, la première invocation du Lambda crée un nouvel élément dans la table DynamoDB ; au début, l'événement est d'abord « INPROGRESS » puis après la pause, il passe à « COMPLETED »
Dans les logs, je peux voir qu'EventBridge, confronté à un dépassement du temps de réponse de 5s, a tenté de re-jouer le traitement de l'événement plusieurs fois. Lorsque le statut de l'événement est INPROGRESS, les logs affichent une erreur IdempotencyAlreadyInProgressError.
Lors de la réception d'une telle réponse, EventBridge applique simplement la politique de re-jeu et tente de rejouer l'événement ultérieurement. Puis, lorsque le statut est COMPLETED, les logs montrent que Lambda a renvoyé la réponse à EventBridge, qui cesse alors de re-jouer l'événement.
Conclusion
La limite de temps de réponse de 5s toléré par EventBridge pour les API tierces pouvait sembler une limite infranchissable du service, mais ce n'est finalement pas si difficile de contourner cette limitation et de bénéficier ainsi de la puissance d'EventBridge.
Grâce à PowerTools pour Lambda, il a suffi de quelques lignes de code ! (et grâce à code open-source produit de façon communautaire et testé unitairement, on est plus certains de ne pas avoir de bug qu'avec son propre code !!)
Si vous avez aimé cet article, n'hésitez pas à commenter ici ou à vous connecter sur LinkedIn !