Ambiance de panique lors de la recherche pour la résolution d'un bug sur NodeJS en rapport avec l'event loop.
Plus ou moins représentatif de la situation 😅

L’Event Loop en NodeJs — Piqûre de rappel 💉

Jphiboris
TEAMSTARTER/Tech
Published in
4 min readJan 15, 2024

--

Une fois embauché dans une start-up, l’école s’éloigne peu à peu et la connaissance théorique de Nodejs (Event Loop, Memory Heap, Promise) ainsi que celle du Javascript (scope de variables …) s’estompe progressivement. Parfois, la pratique (qui contribuera à forger notre expérience) vient nous rappeler de temps en temps que l’apprentissage théorique n’est pas superflu.

Chez Teamstarter, l’équipe tech a été mise à rude épreuve en découvrant un jour que le même email était envoyé plusieurs fois à la même personne. Rien de bien problématique si ce n’est pour l’impact négatif sur la notoriété de notre nom de domaine et la possible mise en quarantaine de nos communications dans les spams 😷.

Un développeur a essayé de “tacler” alors le problème afin de relancer le worker chargé de l’envoi des emails le plus rapidement possible !

15 minutes plus tard un deuxième développeur est venu lui prêter main-forte…

… finalement, il n’a suffit qu’une demi-heure pour que toute l’équipe technique se réunisse afin de remettre en marche le service le plus rapidement possible.

Notre problème 😤

Tout est parti d’un appel à une fonction SendEmail pour l’envoi d’email à une certaine liste de destinataires :

const lstEmails = [
'email1@email.com',
'email2@email.com',
'email3@email.com'
]

async function init() {
const promises = []
for (let email of lstEmails) {
promises.push(sendEmail(email))
}
await Promise.all(promises)
}

init()

Le code de la fonction sendEmail utilisait un objet global SendSmtpEmail pour pouvoir envoyer l’email et appelait une fonction asynchrone pour ajouter la pièce jointe.

const sendSmtpEmail = new SendSmtpEmail()

module.exports = async function sendEmail(
email
) {

sendSmtpEmail.email = email

await addAttachments(sendSmtpEmail, 'PJ - '+email)

console.log('send_email', sendSmtpEmail.email, 'attachments', sendSmtpEmail.attachment)

return sendSmtp.send(smtpEmail)
}

Résultats :

send_email email3@email.com attachments PJ - email3@email.com
send_email email3@email.com attachments PJ - email3@email.com
send_email email3@email.com attachments PJ - email3@email.com

L’équipe ne voyait pas d’où pouvait venir l’erreur 🤔. Un Git Blame ayant révélé qu’une seule ligne de code avait été modifiée dans les derniers mois, la décision fut prise de repartir du code d’origine :

const sendSmtpEmail = new SendSmtpEmail()

// We remove the async
module.exports = function sendEmail(email) {
sendSmtpEmail.email = email

// Remove the async part
//await addAttachments(sendSmtpEmail, 'PJ - '+email)
sendSmtpEmail.attachment = 'PJ - '+email

console.log('send_email', sendSmtpEmail.email, 'attachments', sendSmtpEmail.attachment)

return sendSmtp.send(smtpEmail)
}

Les résultats obtenus :

send_email email1@email.com attachments PJ - email1@email.com
send_email email2@email.com attachments PJ - email2@email.com
send_email email3@email.com attachments PJ - email3@email.com

A ce moment-là, l’équipe était rouge-panique 👹. Elle était capable de résoudre le bug mais ne comprenait plus rien…

Erreur(s) 😥

Pourquoi avions-nous créé un objet SendSmtpEmail au niveau global ? Cet objet métier était vraiment très ambigu.

  • Est-ce un objet représentant un email ?
  • Est-ce l’objet responsable de l’envoi de chaque e-mail ?
  • Si c’est un email, il doit être instancié à chaque envoi, et si c’est le service qui envoie les emails, un singleton suffit.

Pourquoi l’effort de rationalisation du code visant à améliorer la lisibilité de notre code (en déplaçant l’opération AddAttachments dans une fonction asynchrone spécifique) a-t-il conduit à un code défaillant ?

Si la stack ne se comportait pas comme prévu, alors il était sûr que nous ne maîtrisions pas quelque chose. C’était donc le moment de ré-ouvrir nos livres d’école.

1 — le traitement asynchrone de Nodejs.

  • L’Event Loop de Nodejs va suspendre toutes les fonctions sendEmail après avoir exécuté la fonction addAttachments (à cause du await) et les placer dans la queue MicroTasks.
  • Le dernier sendEmail viendra assigner l’email de l’objet global avec le dernier email traité et l’attachment avec le dernier attachment.
  • Une fois la stack vide, l’Event Loop dépile la file des MicroTasks. L’exécution des fonctions sendEmail reprend avec le mauvais email 🥶. CQFD.

🔥Largement inspiré par le Javascript Visualized de Lydia Hallie (ref ci-dessous)

2 — la nécessité d’être plus vigilant dans l’appellation des objets métier. SendSmtpEmail contient 2 notions métier. Avec un nom moins ambigu, l’erreur aurait été moins probable. Nous appellerons donc:

  • SmtpEmail l’email qui sera envoyé,
  • sendSmtp le service responsable de l’envoi de l’email.

💫 Solution(s)

Bien que le bug soit difficilement explicable à première vue, notre solution est centré sur "rendre le code plus lisible". Comme souvent du code avec des variables bien nommées évitera des problèmes. Il ressemblera au final à :

module.exports = async function sendEmail(email) {
const smtpEmail = new SmtpEmail(email)

await addAttachments(smtpEmail, 'PJ - '+email)

await sendSmtp.send(smtpEmail)
}

Références

L’Event Loop expliqué par Lydia Hallie :

https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif

Les promesses et Async/Await expliqués par Lydia Hallie :

https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke

Conclusion 📚

Laisser des “blackboxes”, et donc ne pas maitriser les concepts fondamentaux de sa stack, ce n’est pas dans la culture de Teamstarter. Nous espérons qu’en partageant cet article, comme nous, vous aurez revu vos bases ! Ou simplement découvert le super travail de Lydia !

--

--