web-dev-qa-db-fra.com

Quel est le meilleur moyen de limiter les accès simultanés lors de l'utilisation de Promise.all () de ES6?

J'ai du code qui parcourt une liste qui a été interrogée dans une base de données et qui effectue une requête HTTP pour chaque élément de cette liste. Cette liste peut parfois être assez longue (sur des milliers) et je voudrais m'assurer que je ne frappe pas un serveur Web avec des milliers de demandes HTTP simultanées.

Une version abrégée de ce code ressemble actuellement à ceci ...

function getCounts() {
  return users.map(user => {
    return new Promise(resolve => {
      remoteServer.getCount(user) // makes an HTTP request
      .then(() => {
        /* snip */
        resolve();
      });
    });
  });
}

Promise.all(getCounts()).then(() => { /* snip */});

Ce code est exécuté sur le noeud 4.3.2. Pour rappel, Promise.all peut-il être géré de manière à ce que seul un certain nombre de promesses soient en cours à un moment donné?

36
Chris

Notez que Promise.all() ne déclenche pas les promesses de commencer leur travail, la création de la promesse elle-même.

Dans cet esprit, une solution serait de vérifier chaque fois qu'une promesse est résolue si une nouvelle promesse doit être commencée ou si vous êtes déjà à la limite.

Cependant, il n'est vraiment pas nécessaire de réinventer la roue ici. Une bibliothèque que vous pouvez utiliser à cette fin est es6-promise-pool . De leurs exemples:

// On the Web, leave out this line and use the script tag above instead. 
var PromisePool = require('es6-promise-pool')

var promiseProducer = function () {
  // Your code goes here. 
  // If there is work left to be done, return the next work item as a promise. 
  // Otherwise, return null to indicate that all promises have been created. 
  // Scroll down for an example. 
}

// The number of promises to process simultaneously. 
var concurrency = 3

// Create a pool. 
var pool = new PromisePool(promiseProducer, concurrency)

// Start the pool. 
var poolPromise = pool.start()

// Wait for the pool to settle. 
poolPromise.then(function () {
  console.log('All promises fulfilled')
}, function (error) {
  console.log('Some promise rejected: ' + error.message)
})
30
Timo

Promise.map de bluebird peut utiliser une option de concurrence pour contrôler le nombre de promesses exécutées en parallèle. Parfois, c'est plus facile que .all car vous n'avez pas besoin de créer le tableau de promesses.

const Promise = require('bluebird')

function getCounts() {
  return Promise.map(users, user => {
    return new Promise(resolve => {
      remoteServer.getCount(user) // makes an HTTP request
      .then(() => {
        /* snip */
        resolve();
       });
    });
  }, {concurrency: 10}); // <---- at most 10 http requests at a time
}
9
Jingshao Chen

P-Limit

J'ai comparé la limitation de la simultanéité des promesses à un script personnalisé, bluebird, es6-promise-pool et p-limit. Je crois que p-limit a la mise en œuvre la plus simple et la plus réduite possible pour ce besoin. Voir leur documentation .

Exigences

Être compatible avec async dans l'exemple

Mon exemple

Dans cet exemple, nous devons exécuter une fonction pour chaque URL du tableau (par exemple, une demande d'API). Ici, cela s'appelle fetchData(). Si nous avions un tableau de milliers d'éléments à traiter, la simultanéité serait certainement utile pour économiser sur le processeur et la mémoire. 

const pLimit = require('p-limit');

// Example Concurrency of 3 promise at once
const limit = pLimit(3);

let urls = [
    "http://www.exampleone.com/",
    "http://www.exampletwo.com/",
    "http://www.examplethree.com/",
    "http://www.examplefour.com/",
]

// Create an array of our promises using map (fetchData() returns a promise)
let promises = urls.map(url => {

    // wrap the function we are calling in the limit function we defined above
    return limit(() => fetchData(url));
});

(async () => {
    // Only three promises are run at once (as defined above)
    const result = await Promise.all(promises);
    console.log(result);
})();

Le résultat du journal de la console est un tableau des données de réponse de vos promesses résolues.

6
Matthew Rideout

Au lieu d'utiliser des promesses pour limiter les requêtes http, utilisez le paramètre http.Agent.maxSockets du noeud. Cela supprime la nécessité d'utiliser une bibliothèque ou d'écrire votre propre code de pooling et offre l'avantage supplémentaire de mieux contrôler ce que vous limitez.

agent.maxSockets

Par défaut, définissez sur Infinity. Détermine le nombre de sockets simultanés que l'agent peut ouvrir par origine. Origin est soit une combinaison "Host: port" ou "Host: port: localAddress".

Par exemple:

var http = require('http');
var agent = new http.Agent({maxSockets: 5}); // 5 concurrent connections per Origin
var request = http.request({..., agent: agent}, ...);

Si vous faites plusieurs demandes à la même origine, il peut également être avantageux de définir keepAlive sur true (voir la documentation ci-dessus pour plus d'informations).

5
tcooc

Si vous savez comment fonctionnent les itérateurs et comment ils sont utilisés, vous n’auriez pas besoin d’une bibliothèque supplémentaire, puisqu’il devient très facile de créer vous-même votre concurrence. Permettez-moi de démontrer: 

/* [Symbol.iterator]() is equivalent to .values()
const iterator = [1,2,3][Symbol.iterator]() */
const iterator = [1,2,3].values()


// loop over all items with for..of
for (const x of iterator) {
  console.log('x:', x)
  
  // notices how this loop continues the same iterator
  // and consumes the rest of the iterator, making the
  // outer loop not logging any more x's
  for (const y of iterator) {
    console.log('y:', y)
  }
}

Nous pouvons utiliser le même itérateur et le partager entre les travailleurs.
Si vous aviez utilisé .entries() au lieu de .values(), vous auriez obtenu un tableau 2D avec [index, value] que je montrerai ci-dessous avec une simultanéité de 2

const sleep = n => new Promise(rs => setTimeout(rs,n))

async function doWork(iterator) {
  for (let [index, item] of iterator) {
    await sleep(1000)
    console.log(index + ': ' + item)
  }
}

const arr = Array.from('abcdefghij')
const workers = new Array(2).fill(arr.entries()).map(doWork)
//    ^--- starts two workers sharing the same iterator

Promise.all(workers).then(() => console.log('done'))


Remarque: la différence par rapport à l'exemple async-pool est qu'elle génère deux travailleurs. Par conséquent, si un travailleur génère une erreur pour une raison quelconque, par exemple à l'index 5, il n'arrêtera pas l'autre travailleur de faire le reste. Donc, vous passez de 2 accès simultanés à 1 (pour que cela ne s'arrête pas là). Il sera alors plus difficile de savoir quand tous les travailleurs auront terminé, car Promise.all sera libéré plus tôt si l'un d'eux échoue. Donc, mon conseil est que vous récupériez toutes les erreurs dans la fonction doWork

4
Endless

C’est ce que j’ai fait avec Promise.race, dans mon code ici

const identifyTransactions = async function() {
  let promises = []
  let concurrency = 0
  for (let tx of this.transactions) {
    if (concurrency > 4)
      await Promise.race(promises).then(r => { promises = []; concurrency = 0 })
    promises.Push(tx.identifyTransaction())
    concurrency++
  }
  if (promises.length > 0)
    await Promise.race(promises) //resolve the rest
}

Si vous voulez voir un exemple: https://jsfiddle.net/thecodermarcelo/av2tp83o/5/

0
Marcelo Rafael

J'ai donc essayé de faire fonctionner quelques exemples illustrés pour mon code, mais comme il ne s'agissait que d'un script d'importation et non d'un code de production, utiliser le package npm batch-promises était sûrement le chemin le plus simple pour moi

NOTE: Exige que le moteur d'exécution prenne en charge Promise ou soit polyfilled.

Api BatchPromises (int: batchSize, tableau: Collection, i => Promesse: Iteratee) La promesse: Iteratee sera appelée après chaque lot.

Utilisation:

batch-promises
Easily batch promises

NOTE: Requires runtime to support Promise or to be polyfilled.

Api
batchPromises(int: batchSize, array: Collection, i => Promise: Iteratee)
The Promise: Iteratee will be called after each batch.

Use:
import batchPromises from 'batch-promises';
 
batchPromises(2, [1,2,3,4,5], i => new Promise((resolve, reject) => {
 
  // The iteratee will fire after each batch resulting in the following behaviour:
  // @ 100ms resolve items 1 and 2 (first batch of 2)
  // @ 200ms resolve items 3 and 4 (second batch of 2)
  // @ 300ms resolve remaining item 5 (last remaining batch)
  setTimeout(() => {
    resolve(i);
  }, 100);
}))
.then(results => {
  console.log(results); // [1,2,3,4,5]
});

0