Skip to main content

Files de promesses et traitement par lots des tâches concurrentes dans Deno

Écrit par
snyk code featured

25 septembre 2024

0 minutes de lecture

Qu’est-ce que Deno ?

Deno est un environnement d’exécution sécurisé pour JavaScript et TypeScript, basé sur le même moteur JavaScript V8 que Node.js, mais avec des améliorations importantes. Il a été créé par Ryan Dahl, le créateur original de Node.js, pour remédier à certains des principaux problèmes et limites qu’il avait constatés au fil du temps dans Node.js. Deno est conçu pour renforcer la sécurité, simplifier la gestion des modules et prendre en charge TypeScript dès le départ.

js
import { serve } from "https://deno.land/std/http/server.ts";

serve((req) => new Response("Hello World\n"), { port: 8000 });
console.log("Server running on http://localhost:8000/");

Vous pouvez voir comment le serveur répond aux requêtes avec la chaîne « Hello World ». L’instruction import récupère la méthode serve directement depuis une URL, dans le module du serveur HTTP de la bibliothèque standard de Deno. C’est l’une des particularités de Deno : il récupère et met en cache les modules importés depuis des URL Web, ce qui évite d’avoir besoin d’un gestionnaire de paquets comme npm.

Gérer les tâches concurrentes dans Deno

La gestion des tâches concurrentes est essentielle à tout framework ou environnement d’exécution Web moderne, comme Deno. En JavaScript, les opérations asynchrones sont courantes : récupération de données dans une base de données, requêtes HTTP ou lecture de fichiers sur le disque.

JavaScript étant monothread, il s’appuie sur son modèle événementiel pour gérer ces tâches simultanément sans bloquer le thread principal.

js
let promises = [promise1, promise2, promise3];
let results = await Promise.all(promises);

L’exemple ci-dessus montre comment JavaScript gère les tâches concurrentes à l’aide de Promise.all(). Lorsqu’il exécute plusieurs promesses simultanément, Promise.all() attend qu’elles soient toutes terminées, puis renvoie leurs résultats. Cette méthode est très utile pour optimiser les performances, mais elle présente aussi des inconvénients. Si une promesse échoue, toutes les autres sont également rejetées, ce qui peut entraîner une consommation importante de mémoire lorsque le nombre de tâches est élevé.

Dans Deno, nous pouvons utiliser des files de promesses et le traitement par lots des tâches pour gérer la concurrence plus efficacement. Cette approche permet de traiter un grand nombre de tâches sans risquer de saturer la mémoire ou de tout faire échouer en cas d’erreur sur une seule tâche.

Voyons comment les files d’attente et le traitement par lots des tâches peuvent vous aider à écrire du code plus efficace, robuste et performant.

Comprendre les files de promesses et les tâches concurrentes


Les files de promesses sont essentielles à la programmation asynchrone en JavaScript. Comprendre les files de promesses et la concurrence peut considérablement améliorer l’efficacité et les performances de vos applications. 

Une approche séquentielle des tâches asynchrones

Une file de promesses est une liste d’objets Promise en attente de résolution ou de rejet. En JavaScript, une « Promise » est un objet qui représente l’achèvement ou l’échec éventuel d’une opération asynchrone.

Une file de promesses permet de gérer plusieurs promesses qui doivent être exécutées dans un ordre précis. Elle garantit leur résolution séquentielle et prévient les problèmes potentiels liés à l’exécution asynchrone du code. 

Voici un exemple de file de promesses en action :

javascript
let promiseQueue = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];

Promise.all(promiseQueue)
  .then(values => {
    console.log(values); // logs [1, 2, 3]
  });

Dans l’exemple ci-dessus, Promise.all accepte un itérable de promesses et renvoie une nouvelle promesse qui se résout avec un tableau contenant les valeurs résolues par les promesses transmises, dans le même ordre. Si l’une des promesses est rejetée, la promesse renvoyée est rejetée avec la raison du premier rejet.

Qu’est-ce que la concurrence en JavaScript ?

Dans le contexte de JavaScript, la concurrence désigne la capacité du moteur JavaScript à exécuter plusieurs blocs de code (tâches) sans bloquer l’exécution des autres. Il convient de noter que JavaScript est principalement monothread : il ne peut donc effectuer qu’une seule opération à la fois dans le code exécuté en espace utilisateur. Toutefois, grâce à la programmation asynchrone, notamment aux promesses, JavaScript peut planifier des tâches pour qu’elles s’exécutent simultanément.

La concurrence en JavaScript peut améliorer considérablement les performances, notamment pour les opérations d’E/S comme les requêtes réseau, les opérations sur le système de fichiers ou toute opération nécessitant un temps d’attente.

Voici un exemple de tâches concurrentes en JavaScript :

javascript
let promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, 'task1');
});

let promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'task2');
});

Promise.all([promise1, promise2])
  .then(values => {
    console.log(values); // logs ['task1', 'task2']
  });

Dans cet exemple, task1 et task2 s’exécutent simultanément. Même si task1 prend plus de temps à se terminer, elle ne bloque pas l’exécution de task2.

Cet exemple de code peut sembler similaire au précédent, mais dans les exemples JavaScript précédents, nous avons utilisé Promise.resolve(1), qui se résolvait immédiatement avec l’entier comme valeur de retour. Cet exemple d’exécution concurrente montre comment planifier l’exécution simultanée de tâches en arrière-plan.

Cas concrets où les files de promesses et la concurrence sont indispensables

Les files de promesses et la concurrence sont omniprésentes en JavaScript. Par exemple, dans une application Web, vous pouvez avoir besoin de récupérer simultanément des données depuis plusieurs points de terminaison, puis de les traiter séquentiellement dans un ordre précis. C’est une parfaite illustration de l’utilisation des files de promesses et de la concurrence.

Prenons l’exemple d’un système de réservation de billets de cinéma en ligne. Lorsqu’un utilisateur effectue une réservation, le système peut devoir accomplir plusieurs tâches : vérifier la disponibilité des places, réserver la place, procéder au paiement et envoyer un e-mail de confirmation. Les files de promesses permettent de gérer ces tâches dans le bon ordre. Certaines tâches, comme l’envoi d’e-mails de confirmation à plusieurs utilisateurs, peuvent quant à elles être effectuées simultanément pour gagner en efficacité.

Comprendre et appliquer les notions de files de promesses et de concurrence en JavaScript peut considérablement améliorer les performances et l’efficacité de vos applications. À mesure que vous approfondirez JavaScript, ces notions vous seront de plus en plus utiles pour créer des applications complexes et très performantes.

Créer une application Deno pour exécuter des requêtes HTTP fetch

Voyons comment créer une application Deno de base capable d’envoyer simultanément plusieurs requêtes HTTP fetch. Pour cela, nous utiliserons la méthode Promise.allSettled(). Notre application récupérera des données de paquets depuis le point de terminaison JSON du registre npm. 

Nous vous invitons à suivre ce tutoriel étape par étape. Si vous souhaitez consulter le code source complet pour reproduire les différents éléments de l’application Deno présentés dans cet article, vous pouvez accéder au dépôt GitHub suivant : https://github.com/snyk-snippets/deno-promise-queues-batch-queue

Configurer l’environnement Deno

La première étape de la création de notre application consiste à configurer l’environnement Deno. Deno est un environnement d’exécution sécurisé pour JavaScript et TypeScript, basé sur le moteur JavaScript V8 et le langage de programmation Rust. Pour installer Deno, utilisez la commande suivante :

bash
curl -fsSL https://deno.land/x/install/install.sh | sh

Une fois l’installation terminée, vérifiez-la en exécutant la commande suivante :

bash
deno --version

Cette commande affiche la version de Deno installée.

Écrire un programme Deno simple

Notre programme Deno enverra simultanément plusieurs requêtes fetch à l’aide de la méthode Promise.allSettled().

La méthode Promise.allSettled() renvoie une promesse qui se résout une fois que toutes les promesses fournies ont été résolues ou rejetées. Elle renvoie alors un tableau d’objets décrivant le résultat de chaque promesse. Cette méthode est particulièrement utile lorsque l’ordre de résolution des promesses vous importe peu et que vous souhaitez simplement savoir quand elles ont toutes été résolues ou rejetées.

Quelle est la différence entre Promise.all() et Promise.allSettled() ?


Promise.all() convient lorsque vous avez besoin que toutes les promesses réussissent et souhaitez que l’opération échoue dès qu’une promesse est rejetée. Promise.allSettled() est plutôt utile lorsque vous souhaitez attendre que toutes les promesses soient terminées, quel que soit leur résultat.

Récupérer les données des paquets depuis le point de terminaison JSON du registre npm

Notre application récupérera des données de paquets depuis le point de terminaison JSON du registre npm. Le registre npm est une vaste base de données de paquets JavaScript. Il fournit une API publique qui permet de récupérer les données des paquets au format JSON. C’est la solution idéale pour nos besoins, car elle facilite le traitement des données récupérées.

Exemple de code du programme Deno

Voici un exemple de programme Deno qui envoie simultanément plusieurs requêtes HTTP fetch à l’aide de Promise.allSettled().

javascript
const urls = [
  'https://registry.npmjs.org/package1',
  'https://registry.npmjs.org/package2',
  'https://registry.npmjs.org/package3'
];

async function fetchPackageData() {
  const promises = urls.map(url => fetch(url));

  const results = await Promise.allSettled(promises);

  for (const result of results) {
    if (result.status === 'fulfilled') {
      const data = await result.value.json();
      console.log(data);
    } else {
      console.error(result.reason);
    }
  }
}

fetchPackageData();

Dans cet exemple, nous créons un tableau d’URL à récupérer. Nous parcourons ensuite ce tableau pour envoyer une requête fetch à chaque URL. Nous utilisons Promise.allSettled() pour attendre que toutes les requêtes fetch soient terminées, puis nous traitons les résultats.

Voici une partie de la réponse renvoyée par le programme Deno ci-dessus :

json
{
  _id: "package1",
  _rev: "8-591c2789a67390636bd11cdacd69d197",
  name: "package1",
  "dist-tags": { latest: "0.0.0" },
  versions: {
    "0.0.0": {
      name: "package1",
      version: "0.0.0",
      main: "index.js",
      scripts: { test: 'echo "Error: no test specified" && exit 1' },
      author: "",
      license: "ISC",
      _id: "package1@0.0.0",
      dist: {
        shasum: "2c1e73fbc7351131b2f657417e50c2f1330450a8",
        tarball: "https://registry.npmjs.org/package1/-/package1-0.0.0.tgz",
        integrity: "sha512-U4a0VTa026SElK/J6JbW7/pzFZHddrAQ3OEbgCHFwXV/Ytt+Ece37GppLjP+GBRBqge7dZS9qlEOOou8m/1Fiw==",
        signatures: [ [Object] ]
      },
      _from: ".",
      _npmVersion: "1.4.3",
      _npmUser: { name: "zhoutj", email: "zhoutianjia@126.com" },
      maintainers: [ { name: "zhoutj", email: "zhoutianjia@126.com" } ]
    }
  },
  readme: "ERROR: No README data found!",
  maintainers: [ { name: "zhoutj", email: "zhoutianjia@126.com" } ],
  time: {
    modified: "2022-06-23T06:46:19.731Z",
    created: "2014-05-05T02:51:24.933Z",
    "0.0.0": "2014-05-05T02:51:24.933Z",
    "0.0.1": "2014-05-05T02:52:51.624Z"
  },
  license: "ISC",
  readmeFilename: ""
}

Améliorer l’application Deno avec le module BatchQueue

Le module BatchQueue de Deno permet de gérer et de contrôler l’exécution des tâches dans votre application Deno. Cet outil puissant permet de limiter le nombre de tâches exécutées simultanément. Il peut être utile lorsque vous travaillez avec des ressources soumises à des restrictions d’utilisation ou que vos tâches nécessitent beaucoup de processeur ou de mémoire.

Pour les requêtes HTTP en particulier, mieux vaut faire preuve de civisme et limiter le nombre de requêtes envoyées simultanément à un service distant comme le registre npm, afin d’éviter de le saturer. Limiter les requêtes concurrentes permet également à votre programme d’éviter d’être signalé comme une potentielle attaque par déni de service.

BatchQueue dans Deno permet de créer une file de promesses et de les exécuter par lots. Cette technique peut améliorer considérablement les performances de votre application Deno en évitant de surcharger les ressources de votre système et en augmentant le débit des tâches.

Améliorer le programme Deno en utilisant une file avec un faible niveau de concurrence

Prenons l’exemple d’un programme Deno qui envoie simultanément plusieurs requêtes HTTP. Par défaut, Deno tente d’exécuter toutes ces tâches en même temps. Cela peut toutefois poser problème si leur nombre est assez élevé pour épuiser les ressources système disponibles ou si elles impliquent des interactions avec des systèmes externes soumis à des limites d’utilisation.

En intégrant le module BatchQueue, nous pouvons contrôler le nombre de tâches exécutées simultanément. Voici un exemple simplifié d’utilisation de BatchQueue dans un programme Deno :

javascript
import { BatchQueue } from "https://deno.land/x/batch_queue/mod.ts";

// URLs to make HTTP requests to
const urls = [
  'https://registry.npmjs.org/package1',
  'https://registry.npmjs.org/package2',
  'https://registry.npmjs.org/package3'
];

// Initialize the queue with a concurrency of 2
const q = new BatchQueue(2);

// Function to make HTTP request
async function makeHttpRequest(url) {
  try {
    const response = await fetch(url);
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching URL:', url, error);
  }
}

for (const url of urls) {
  q.queue(() => makeHttpRequest(url));
}

await q.run()
await q.allSettled

Dans cet exemple, le niveau de concurrency de BatchQueue est défini sur 5 : seules cinq tâches peuvent donc s’exécuter simultanément.

Chaque tâche est ajoutée à la file BatchQueue, puis la fonction makeHttpRequest est appelée lorsque la tâche est retirée de la file pour être exécutée. BatchQueue veille à ce que cinq tâches au maximum s’exécutent à tout moment.

File de promesses et Promise.all() : quelles différences ?

En comparant la version améliorée du programme Deno, qui utilise BatchQueue, à la version initiale, vous constaterez une nette amélioration de l’utilisation des ressources système et des performances globales de votre application Deno. 

Le mécanisme BatchQueue garantit que le programme Deno n’épuise pas toutes les ressources système disponibles, un problème courant lors de l’exécution simultanée d’un grand nombre de tâches. 

Avec le module BatchQueue dans Deno, vous pouvez gérer efficacement l’exécution des tâches dans votre application et améliorer les performances et la réactivité de vos programmes Deno.

Ajouter ProgressBar à l’application Deno

Ajouter des retours d’information à votre application Deno peut considérablement améliorer l’expérience utilisateur. Pour en fournir de manière intuitive dans une interface en ligne de commande (CLI), vous pouvez utiliser une barre de progression.

Le module ProgressBar simplifie l’ajout d’une barre de progression à votre application Deno.

Le module ProgressBar est un paquet léger et hautement personnalisable qui permet d’intégrer facilement une barre de progression à vos applications. Son API simple facilite la mise en œuvre, tandis que ses options de personnalisation vous permettent d’adapter son apparence à votre application.

Pour installer le module ProgressBar, importez-le dans votre application comme suit :

javascript
import ProgressBar from "https://deno.land/x/progress/mod.ts";

Ajouter ProgressBar pour afficher la progression des requêtes terminées

Après avoir importé le module ProgressBar, vous pouvez l’utiliser pour afficher visuellement la progression des requêtes terminées dans votre application Deno. 

Vous pouvez créer une nouvelle instance de ProgressBar en indiquant le nombre total d’étapes qu’elle doit représenter. Ensuite, à chaque requête terminée, appelez la méthode .tick() pour faire avancer la barre de progression.

Voici un exemple simple d’utilisation de ProgressBar pour suivre et afficher la progression des requêtes terminées :

javascript
import ProgressBar from "https://deno.land/x/progress/mod.ts";

const totalRequests = 100;
const progressBar = new ProgressBar({
  total: totalRequests,
  complete: '=',
  incomplete: ' ',
  display: ':bar :percent',
});

// Simulate a loop that represents a batch of requests
for (let i = 0; i < totalRequests; i++) {
  // Simulate a completed request
  progressBar.render(i);
  await new Promise((resolve) => setTimeout(resolve, 100)); // simulate async delay
}

Dans cet exemple, la barre de progression avance d’une étape chaque fois qu’une requête est terminée. Vous pouvez personnaliser son apparence en ajustant les options complete et incomplete lors de la création de l’instance ProgressBar.

Exemple de code du programme Deno avec ProgressBar

Enfin, examinons un exemple plus détaillé de l’intégration de ProgressBar dans un scénario plus proche du monde réel. Imaginons que nous ayons un lot de requêtes API à envoyer et que nous souhaitions suivre leur progression.

javascript
import { BatchQueue } from "https://deno.land/x/batch_queue/mod.ts";
import ProgressBar from "https://deno.land/x/progress/mod.ts";

// URLs to make HTTP requests to
const urls = [
  'https://registry.npmjs.org/package1',
  'https://registry.npmjs.org/package2',
  'https://registry.npmjs.org/package3'
];

const totalRequests = urls.length;
let currentRequest = 0;
const progressBar = new ProgressBar({
  total: totalRequests,
  complete: '=',
  incomplete: ' ',
  display: ':bar :percent',
});

// Initialize the queue with a concurrency of 2
const q = new BatchQueue(2);

// Function to make HTTP request
async function makeHttpRequest(url) {
  progressBar.render(++currentRequest);
  const response = await fetch(url);
  const data = await response.json();
  // @TODO do something with the data...
}

for (const url of urls) {
	q.queue(() => makeHttpRequest(url));
}

await q.run()
await q.allSettled

Dans cet exemple, nous simulons l’envoi de requêtes API et utilisons ProgressBar pour suivre leur progression. Chaque appel à sendRequest() représente l’envoi d’une requête et l’attente d’une réponse. À chaque requête terminée, nous faisons avancer la barre de progression en appelant progressBar.tick().

L’exécution de notre programme Deno complet, qui utilise à la fois le module Promises Batch Queue et le module ProgressBar, produit la sortie suivante dans la ligne de commande :

bash
@lirantal ➜ /workspaces/project-app (main) $ deno run -A app.ts 
================================================== 100.00%

Et voilà ! Vous avez ajouté une ProgressBar à votre application Deno pour afficher la progression des requêtes terminées. Le module ProgressBar est un excellent ajout à vos applications Deno : il offre un moyen intuitif de suivre l’état des tâches de longue durée. Essayez-le dans votre prochain projet Deno !

Pour rappel, vous trouverez l’application Deno complète de cet exemple de file d’attente par lots de promesses à l’adresse suivante : https://github.com/snyk-snippets/deno-promise-queues-batch-queue

Sécurité des applications Deno : vulnérabilité SSRF

La cybersécurité est un enjeu fondamental dans tout projet de développement logiciel. Dans l’écosystème Deno, les développeurs doivent notamment rester vigilants face à un risque potentiel : la falsification de requête côté serveur (SSRF).

Cette section examine la vulnérabilité SSRF, les risques qu’elle présente pour les applications Deno et les moyens de l’atténuer. Nous présentons également un exemple pratique de code pour un programme Deno sécurisé.

Qu’est-ce qu’une vulnérabilité SSRF ?

SSRF est un type de vulnérabilité qui permet à un attaquant de manipuler une application côté serveur afin qu’elle effectue des interactions réseau non autorisées. Elle présente un risque, car elle permet à un attaquant d’interagir avec des systèmes internes, ce qui peut entraîner un accès non autorisé ou une fuite de données. En bref, une vulnérabilité SSRF peut transformer le serveur en proxy pour les activités malveillantes de l’attaquant.

Vulnérabilité SSRF dans les applications Deno

Deno est conçu pour être sécurisé par défaut : tous les scripts s’exécutent dans un environnement isolé sécurisé, sauf si l’utilisateur leur accorde explicitement des autorisations. Toutefois, lorsque les autorisations nécessaires sont accordées, les applications Deno peuvent être exposées à des vulnérabilités SSRF.

Par exemple, si une application Deno reçoit l’autorisation « allow-net », elle peut envoyer des requêtes réseau à n’importe quel hôte. Un attaquant pourrait exploiter cette possibilité en injectant un hôte malveillant dans la requête, ce qui entraînerait une vulnérabilité SSRF.

En général, les développeurs peuvent exécuter des applications Deno, comme le programme de file d’attente de promesses que nous avons écrit, de la manière suivante :


bash
$ deno run --allow-net app.ts

Atténuer les vulnérabilités SSRF dans les applications Deno

Pour atténuer les risques de vulnérabilités SSRF, les développeurs doivent adopter des pratiques de programmation sécurisées, notamment :

  1. Principe du moindre privilège : accordez uniquement les autorisations minimales nécessaires au bon fonctionnement de l’application Deno. Si l’application n’a pas besoin d’envoyer des requêtes réseau, n’accordez pas l’autorisation --allow-net (ou son raccourci -A dans la ligne de commande).

  2. Validation des entrées : validez correctement toutes les entrées utilisateur et rejetez celles qui sont suspectes ou mal formées. En règle générale, les utilisateurs ne sont pas autorisés à fournir des URL absolues. Limitez-les aux URL relatives hébergées et autorisées par votre application.

Exemple de code d’un programme Deno sécurisé

Examinons une application Deno élémentaire intégrant des mesures de sécurité pour contribuer à atténuer les vulnérabilités SSRF.

javascript
import { serve } from "https://deno.land/std/http/server.ts";

console.log("Server running on http://localhost:8000/");

serve((req) => {
  const url = new URL(req.url, `http://${req.headers.get('host')}`);
  const host = url.hostname;

  if (host !== 'localhost' && host !== '127.0.0.1') {
    return new Response("Unauthorized host\n");
  } else {
    return new Response("Hello World\n");
  }
}, { port: 8000 });

Dans cet exemple de code, nous exécutons un serveur HTTP sur le port 8000. Lorsqu’une requête est reçue, nous vérifions le nom d’hôte dans l’URL de la requête. S’il ne s’agit pas de « localhost » ou de « 127.0.0.1 », nous répondons par le message « Hôte non autorisé », empêchant ainsi toute requête vers des hôtes externes.

En adoptant des pratiques de programmation sécurisées et en accordant une attention particulière aux autorisations que nous octroyons à nos applications Deno, nous pouvons atténuer efficacement les risques de vulnérabilités SSRF.

Pourquoi gérer efficacement les tâches concurrentes dans Deno

La gestion efficace des tâches concurrentes est essentielle au développement d’applications performantes. C’est d’autant plus important dans Deno, conçu pour prendre en charge le code JavaScript et TypeScript moderne, qui implique souvent de nombreuses opérations asynchrones.

En utilisant des files d’attente de promesses et le traitement par lots, nous pouvons gérer efficacement les ressources et éviter des problèmes tels que les dépassements de mémoire ou la limitation du processeur. Cela améliore la fiabilité et la réactivité de l’application, ainsi que l’expérience utilisateur globale.

Explorez Deno et ses modules

Deno ne se limite pas aux files d’attente de promesses et au traitement par lots des tâches concurrentes. Cet environnement d’exécution puissant et flexible offre une multitude de fonctionnalités et de modules pour créer des applications robustes, sécurisées et efficaces.

Par exemple, la bibliothèque standard de Deno fournit un ensemble de modules de qualité, révisés et garantis compatibles avec Deno. Elle comprend notamment des modules de manipulation des dates, de hachage, de serveurs HTTP et de tests.

Grâce à l’importance qu’il accorde à la sécurité, Deno permet également de contrôler avec précision les ressources auxquelles une application peut accéder, un aspect essentiel à l’ère des cybermenaces. Cela dit, de nombreuses vulnérabilités, comme les vulnérabilités SSRF, nécessitent toujours une intervention au cas par cas et la mise en place de contrôles de sécurité par les développeurs.

Pour mieux comprendre Deno et apprendre à l’utiliser, consultez la documentation officielle de Deno. Vous y trouverez des informations sur les différentes fonctionnalités et les modules de Deno, qui vous aideront à comprendre les files d’attente de promesses et le traitement par lots des tâches concurrentes.

Pour garantir la sécurité de votre application, il est essentiel de comprendre les différentes menaces qui pèsent sur la sécurité des applications, notamment les vulnérabilités de falsification de requête côté serveur (SSRF). Voici des ressources complémentaires pour vous aider à comprendre et à atténuer les vulnérabilités SSRF :

Sécurisez votre code pendant le développement

Snyk analyse votre code pour détecter les problèmes de qualité et de sécurité, et vous conseille sur leur correction directement dans votre IDE.