web-dev-qa-db-fra.com

Limiter et mettre en file d'attente les demandes d'API en raison d'un plafond par seconde

J'utilise mikeal/request pour effectuer des appels d'API. L'une des API que j'utilise le plus souvent (l'API Shopify). Récemment mis en place une nouvelle limite call , je vois des erreurs comme: 

Exceeded 6.0 calls per second for api client. Slow your requests or contact support for higher limits.

J'ai déjà eu une mise à jour, mais peu importe la quantité de bande passante dont je dispose, je dois en tenir compte. Une grande majorité des demandes adressées à l'API Shopify se trouvent dans les fonctions async.map () , qui mettent en boucle les demandes asynchrones et rassemblent les corps.

Je cherche de l'aide, peut-être une bibliothèque existante, qui engloberait le module de requête et bloquerait, mettrait en veille, limiter, allouer, gérer, les nombreuses requêtes simultanées qui se déclenchent de manière asynchrone et les limiteraient à des requêtes 6 à la fois. Je n'ai aucun problème à travailler sur un tel projet s'il n'existe pas. Je ne sais tout simplement pas comment gérer ce genre de situation et j'espère une sorte de standard.

J'ai fait un ticket avec mikeal/request .

44
ThomasReggi

J'ai rencontré le même problème avec différentes API. AWS est également célèbre pour son étranglement.

Quelques approches peuvent être utilisées. Vous avez mentionné la fonction async.map (). Avez-vous essayé async.queue () ? La méthode de file d'attente devrait vous permettre de définir une limite solide (telle que 6) et tout dépassement de cette quantité sera placé dans la file d'attente.

Un autre outil utile est oibackoff . Cette bibliothèque vous permettra d’annuler votre demande si vous recevez une erreur du serveur et essayez à nouveau.

Il peut être utile d’emballer les deux bibliothèques pour s’assurer que vos deux bases sont couvertes: async.queue pour vous assurer de ne pas dépasser la limite et oibackoff pour vous assurer que votre demande soit traitée à nouveau si le serveur vous le demande. Il y avait une erreur.

13
Dan

Pour une solution alternative, j'ai utilisé le node-rate-limit pour envelopper la fonction de requête comme ceci:

var request = require('request');
var RateLimiter = require('limiter').RateLimiter;

var limiter = new RateLimiter(1, 100); // at most 1 request every 100 ms
var throttledRequest = function() {
    var requestArgs = arguments;
    limiter.removeTokens(1, function() {
        request.apply(this, requestArgs);
    });
};
31
Dmitry Chornyi

Le paquet npmsimple-rate-limit semble être une très bonne solution à ce problème.

De plus, il est plus facile à utiliser que node-rate-limiter et async.queue.

Voici un extrait qui montre comment limiter toutes les demandes à dix par seconde.

var limit = require("simple-rate-limiter");
var request = limit(require("request")).to(10).per(1000);
20
Camilo Sanchez

Dans le module async, cette fonctionnalité demandée est fermée en tant que "ne sera pas corrigé" 

Il existe une solution utilisant le modèle leakybucket ou token bucket, elle est implémentée dans le module "limit" npm en tant que RateLimiter. 

RateLimiter, voir l'exemple ici: https://github.com/caolan/async/issues/1314#issuecomment-263715550

Une autre méthode consiste à utiliser PromiseThrottle. Voici un exemple pratique:

var PromiseThrottle = require('promise-throttle');
let RATE_PER_SECOND = 5; // 5 = 5 per second, 0.5 = 1 per every 2 seconds

var pto = new PromiseThrottle({
    requestsPerSecond: RATE_PER_SECOND, // up to 1 request per second
    promiseImplementation: Promise  // the Promise library you are using
});

let timeStart = Date.now();
var myPromiseFunction = function (arg) {
    return new Promise(function (resolve, reject) {
        console.log("myPromiseFunction: " + arg + ", " + (Date.now() - timeStart) / 1000);
        let response = arg;
        return resolve(response);
    });
};

let NUMBER_OF_REQUESTS = 15;
let promiseArray = [];
for (let i = 1; i <= NUMBER_OF_REQUESTS; i++) {
    promiseArray.Push(
            pto
            .add(myPromiseFunction.bind(this, i)) // passing am argument using bind()
            );
}

Promise
        .all(promiseArray)
        .then(function (allResponsesArray) { // [1 .. 100]
            console.log("All results: " + allResponsesArray);
        });

Sortie:

myPromiseFunction: 1, 0.031
myPromiseFunction: 2, 0.201
myPromiseFunction: 3, 0.401
myPromiseFunction: 4, 0.602
myPromiseFunction: 5, 0.803
myPromiseFunction: 6, 1.003
myPromiseFunction: 7, 1.204
myPromiseFunction: 8, 1.404
myPromiseFunction: 9, 1.605
myPromiseFunction: 10, 1.806
myPromiseFunction: 11, 2.007
myPromiseFunction: 12, 2.208
myPromiseFunction: 13, 2.409
myPromiseFunction: 14, 2.61
myPromiseFunction: 15, 2.811
All results: 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15

Nous pouvons clairement voir le taux de sortie, c’est-à-dire 5 appels par seconde.

6

Voici ma solution utiliser une bibliothèque request-promise ou axios et encapsuler l'appel dans cette promesse.

var Promise = require("bluebird")

// http://stackoverflow.com/questions/28459812/way-to-provide-this-to-the-global-scope#28459875
// http://stackoverflow.com/questions/27561158/timed-promise-queue-throttle

module.exports = promiseDebounce

function promiseDebounce(fn, delay, count) {
  var working = 0, queue = [];
  function work() {
    if ((queue.length === 0) || (working === count)) return;
    working++;
    Promise.delay(delay).tap(function () { working--; }).then(work);
    var next = queue.shift();
    next[2](fn.apply(next[0], next[1]));
  }
  return function debounced() {
    var args = arguments;
    return new Promise(function(resolve){
      queue.Push([this, args, resolve]);
      if (working < count) work();
    }.bind(this));
  }
2
ThomasReggi

Les autres solutions ne correspondaient pas à mes goûts. En recherchant plus loin, j'ai trouvé promise-ratelimit qui vous donne une api que vous pouvez simplement await:

var rate = 2000 // in milliseconds
var throttle = require('promise-ratelimit')(rate)

async function queryExampleApi () {
  await throttle()
  var response = await get('https://api.example.com/stuff')
  return response.body.things
}

L'exemple ci-dessus garantira que vous ne ferez que des requêtes sur api.example.com toutes les 2000 ms au plus. En d’autres termes, la toute première demande ne le sera pas attendra 2000ms.

1
mindeavor

Ma solution en utilisant moderne Vanilla JS:

function throttleAsync(fn, wait) {
  let lastRun = 0;

  async function throttled(...args) {
    const currentWait = lastRun + wait - Date.now();
    const shouldRun   = currentWait <= 0;

    if (shouldRun) {
      lastRun = Date.now();
      return await fn(...args);
    } else {
      return await new Promise(function(resolve) {
        setTimeout(function() {
          resolve(throttled());
        }, currentWait);
      });
    }
  }

  return throttled;
}

Usage:

const throttledRun = throttleAsync(run, 1000);
0
djanowski