En tant que développeur web, il est crucial que vos applications fonctionnent aussi rapidement que possible. Vous devez créer des applications Web qui répondent aux demandes dans les meilleurs délais.
L’une des nombreuses technologies qui peuvent vous aider est la mise en file d’attente des tâches.
Alors, qu’est-ce que la mise en file d’attente des tâches, et comment l’utiliser pour optimiser une application Node.js ?
Qu’est-ce que la mise en file d’attente des tâches ?
La mise en file d’attente des messages est un moyen de communication asynchrone entre deux applications ou services, généralement désigné sous le nom de producteur et consommateur. Il s’agit d’un concept bien connu employé dans les architectures sans serveur et les microservices.
Le concept de tâche ou emploi mise en file d’attente utilise la mise en file d’attente des messages pour améliorer les performances des applications. Elle abstrait les complexités de la gestion des messages et vous permet de définir des fonctions pour gérer les travaux ou les tâches de manière asynchrone en utilisant une file d’attente, réduisant ainsi le taux d’utilisation de la mémoire dans certaines parties d’une application.
L’exemple le plus courant de logiciel de file d’attente de messages est RabbitMQ. Les outils de file d’attente de tâches comprennent Celery et Bull. Vous pouvez également configurer RabbitMQ pour qu’il fonctionne comme une file d’attente de tâches. Lisez la suite pour en savoir plus sur la mise en file d’attente des tâches dans Node.js à l’aide de Bull.
Qu’est-ce que BullMQ ?
BullMQ (Bull.js) est une bibliothèque Node.js utilisée pour mettre en œuvre des files d’attente dans les applications Node. Bull est un système basé sur Redis (vous connaissez peut-être mieux Redis en tant qu’outil de stockage rapide de données) et c’est une option rapide et fiable à considérer pour la mise en file d’attente de tâches dans Node.js.
Vous pouvez utiliser Bull pour de nombreuses tâches telles que l’implémentation de tâches retardées, de tâches planifiées, de tâches répétables, de files d’attente prioritaires, et bien d’autres encore.
Alors, comment pouvez-vous utiliser Bull et Redis pour exécuter des tâches Node.js de manière asynchrone ?
Comment configurer Bull et Redis pour la mise en file d’attente des tâches dans Node.js ?
Pour commencer à utiliser la mise en file d’attente des tâches dans Node.js avec Bull, vous devez installer Node.js et Redis sur votre machine. Vous pouvez suivre le guide Redis labs pour installer Redis si vous ne l’avez pas encore installé.
La première étape pour implémenter Bull est de l’ajouter aux dépendances de votre projet en exécutant npm install bull ou yarn add bull dans le terminal à l’intérieur du dossier de votre projet. Il existe plusieurs façons d’initialiser une file d’attente dans Bull, comme indiqué ci-dessous :
const Queue = require('bull');
// different ways to initialize a queue
// - using redis URL string
const emailQueue = new Queue('Email Queue', 'redis://127.0.0.1:6379');
// - with a redis connection and queue options object
const videoQueue = new Queue('Video Queue', 'redis://127.0.0.1:6379', queueOptions);
// - without a redis connection but with queueOption
const docQueue = new Queue('Document Queue', queueOptions);
// - without a redis connection or queue options
const QueueClient = new Queue('My Queue');
Toutes ces méthodes utilisent une configuration minimale pour Bull dans Node.js. L’objet options supporte de nombreuses propriétés et vous pouvez les découvrir dans la section options de la file d’attente de la documentation de Bull.
Mise en œuvre d’une file d’attente de tâches de courrier électronique avec BullMQ
Pour implémenter une file d’attente pour l’envoi d’emails, vous pouvez définir une fonction de producteur qui ajoute des emails à la file d’attente, et une fonction de consommateur qui gère l’envoi des emails.
Tout d’abord, vous pouvez initialiser votre file d’attente dans une classe en utilisant une URL Redis et quelques options de file d’attente comme vu ci-dessous.
// queueHandler.js
const Queue = require('bull');
// use a real email handler module here - this is just an example
const emailHandler = require('./emailHandler.js');
// define constants, Redis URL, and queue options
const REDIS_URL = 'redis://127.0.0.1:6379';
const queueOpts = {
// rate limiter options to avoid overloading the queue
limiter: {
// maximum number of tasks queue can take
max: 100,
// time to wait in milliseconds before accepting new jobs after
// reaching limit
duration: 10000
},
prefix: 'EMAIL-TASK', // a prefix to be added to all queue keys
defaultJobOptions: { // default options for tasks in the queue
attempts: 3, // default number of times to retry a task
// to remove a task from the queue after completion
removeOnComplete: true
}
};
class EmailQueue {
constructor() {
this.queue = new Queue('Email Queue', REDIS_URL, queueOpts);
}
};
export default EmailQueue; // export the class
Maintenant que vous avez initialisé une file d’attente, vous pouvez définir votre fonction de producteur (en utilisant la fonction de Bull add() ) comme une méthode de la fonction EmailQueue pour ajouter des e-mails à la file d’attente des tâches. Le bloc de code suivant en fait la démonstration :
// queueHandler.js
class EmailQueue {
constructor () {
// ...
}
// producer function to add emails to queue
async addEmailToQueue(emailData) {
// add task with name 'email_notification' to queue
await this.queue.add('email_notification', emailData);
console.log('the email has been added to the queue...');
}
};
export default EmailQueue; // export the class
La fonction de producteur est prête, et vous pouvez maintenant définir une fonction de consommateur (en utilisant la fonction de Bull process() ) pour traiter toutes les tâches de courrier électronique dans la file d’attente, c’est-à-dire appeler la fonction pour envoyer un courrier électronique. Vous devez définir cette fonction consommateur dans le constructeur de la classe.
// queueHandler.js
class EmailQueue {
constructor () {
// ...
// consumer function that takes in the assigned name of the task and
// a callback function
this.queue.process('email_notification', async (emailJob, done) => {
console.log('processing email notification task');
await emailHandler.sendEmail(emailJob); // send the email
done(); // complete the task
})
}
// ...
};
export default EmailQueue; // export the class
Une tâche peut également avoir des options pour définir son comportement dans la file d’attente ou la façon dont la fonction consommateur la traite. Vous pouvez trouver plus d’informations à ce sujet dans la section des options du travail de la documentation de Bull.
Le site emailJob argument est un objet qui contient les propriétés de la tâche à traiter par la file d’attente. Il comprend également les principales données nécessaires à la construction de l’e-mail.. Pour faciliter la compréhension, le sendEmail() serait similaire à cet exemple :
// emailHandler.js
const sendgridMail = require('@sendgrid/mail');
const apiKey = process.env.SENDGRID_API_KEY
sendgridMail.setApiKey(apiKey); // set email transporter security credentials
const sendEmail = async (emailJob) => {
try {
// extract the email data from the job
const { name, email } = emailJob.data;
const message = {
from: 'me@example.com',
to: 'you@example.com',
subject: 'Hi! Welcome',
text: `Hello ${name}, welcome to MUO`
};
await sendgridMail.sendMail(message); // send email
// mark task as completed in the queue
await emailJob.moveToCompleted('done', true);
console.log('Email sent successfully...');
} catch (error) {
// move the task to failed jobs
await emailJob.moveToFailed({ message: 'task processing failed..' });
console.error(error); // log the error
}
}
export default sendEmail;
Maintenant que les fonctions de producteur et de consommateur sont définies et prêtes à être utilisées, vous pouvez appeler votre fonction de producteur n’importe où dans votre application pour ajouter un courriel à la file d’attente en vue de son traitement.
Un exemple de contrôleur ressemblerait à ceci :
// userController.js
const EmailQueue = require('../handlers/queueHandler.js')
const signUp = async (req, res) => {
const { name, email, password } = req.body;
// --
// a query to add the new user to Database...
// --
// add to Email queue
const emailData = { name, email };
await EmailQueue.addEmailToQueue(emailData);
res.status(200).json({
message: "Sign up successful, kindly check your email"
})
}
Votre queueHandler.js devrait maintenant être comme suit :
// queueHandler.js
const Queue = require('bull');
const emailHandler = require('../handlers/emailHandler.js');
const REDIS_URL = 'redis://127.0.0.1:6379';
const queueOpts = {
limiter: {
max: 100,
duration: 10000
},
prefix: 'EMAIL-TASK',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true
}
};
class EmailQueue {
constructor() {
this.queue = new Queue('Email Queue', REDIS_URL, queueOpts);
// consumer
this.queue.process('email_notification', async (emailJob, done) => {
console.log('processing email notification task');
await emailHandler.sendEmail(emailJob);
done();
})
}
// producer
async addEmailToQueue(emailData) {
// add task with name 'email_notification' to queue
await this.queue.add('email_notification', emailData);
console.log('the email has been added to the queue...');
}
};
export default EmailQueue;
Lorsque vous mettez en œuvre cette méthode dans une API REST Node.js, vous remarquerez une diminution du temps de réponse du point de terminaison de l’inscription et une accélération des délais de livraison des e-mails, par rapport à l’alternative.
Les files d’attente de tâches vous permettent également de gérer les erreurs d’inscription et d’envoi d’e-mails de manière indépendante.
Optimisation des applications à l’aide des files d’attente de tâches
Les files de messages et de tâches sont un excellent moyen d’améliorer les performances générales des applications. Elles sont également très bon marché et vous pouvez les utiliser dans autant de parties d’une application que vous le souhaitez.
Bien que ce tutoriel ait utilisé les emails comme scénario d’exemple pour gérer les tâches consommatrices de mémoire avec des files d’attente, il existe de nombreux autres cas où vous pouvez appliquer les mêmes concepts. Il s’agit notamment des opérations lourdes de lecture/écriture, du rendu d’images ou de documents de haute qualité et de l’envoi de notifications en masse.