Сделайте несколько запросов к API, который может обрабатывать только 20 запросов в минуту

У меня есть метод, который возвращает обещание, и внутренне этот метод вызывает API, который может иметь только 20 запросов в минуту. Проблема в том, что у меня есть большой массив объектов (около 300), и я хотел бы сделать вызов API для каждого из них.

На данный момент у меня есть следующий код:

    const bigArray = [.....];

    Promise.all(bigArray.map(apiFetch)).then((data) => {
      ...
    });

Но это не обрабатывает временные ограничения. Я надеялся, что смогу использовать что-то вроде _.chunk и _.debounce от lodash но я не могу сосредоточиться на этом. Кто-нибудь может мне помочь?

2 ответа

Решение

Вы можете отправлять 1 блок из 20 запросов каждую минуту или распределять их по 1 запросу каждые 3 секунды (последний, вероятно, предпочитают владельцы API).

function rateLimitedRequests(array, chunkSize) {
  var delay = 3000 * chunkSize;
  var remaining = array.length;
  var promises = [];
  var addPromises = function(newPromises) {
    Array.prototype.push.apply(promises, newPromises);
    if (remaining -= newPromises.length == 0) {
      Promise.all(promises).then((data) => {
        ... // do your thing
      });
    }
  };
  (function request() {
    addPromises(array.splice(0, chunkSize).map(apiFetch));
    if (array.length) {
      setTimeout(request, delay);
    }
  })();
}

Для вызова 1 каждые 3 секунды:

rateLimitedRequests(bigArray, 1);

Или 20 каждую минуту:

rateLimitedRequests(bigArray, 20);

Если вы предпочитаете использовать _.chunk а также _.debounce 1 _.throttle:

function rateLimitedRequests(array, chunkSize) {
  var delay = 3000 * chunkSize;
  var remaining = array.length;
  var promises = [];
  var addPromises = function(newPromises) {
    Array.prototype.push.apply(promises, newPromises);
    if (remaining -= newPromises.length == 0) {
      Promise.all(promises).then((data) => {
        ... // do your thing
      });
    }
  };
  var chunks = _.chunk(array, chunkSize);  
  var throttledFn = _.throttle(function() {
    addPromises(chunks.pop().map(apiFetch));
  }, delay, {leading: true});
  for (var i = 0; i < chunks.length; i++) {
    throttledFn();
  }
}

1 Вы, вероятно, хотите _.throttle так как он выполняет каждый вызов функции после задержки, тогда как _.debounce группирует несколько звонков в один звонок. Смотрите эту статью по ссылкам из документов

Debounce: думайте об этом как о "группировании нескольких событий в одном". Представьте, что вы идете домой, входите в лифт, двери закрываются... и вдруг ваш сосед появляется в коридоре и пытается прыгнуть на лифте. Будьте вежливы! и откройте двери для него: вы отбрасываете лифт отъезда. Учтите, что та же самая ситуация может повториться с третьим лицом, и так далее... возможно, задержка отъезда на несколько минут.

Дроссель: Думайте об этом как о клапане, он регулирует поток казней. Мы можем определить максимальное количество раз, которое функция может быть вызвана в определенное время. Так что по аналогии с лифтом... вы достаточно вежливы, чтобы впустить людей на 10 секунд, но как только эта задержка пройдет, вы должны идти!

Если вы можете использовать библиотеку обещаний Bluebird, в нее встроена функция параллелизма, которая позволяет вам управлять группой асинхронных операций максимум для N в полете за раз.

var Promise = require('bluebird');
const bigArray = [....];

Promise.map(bigArray, apiFetch, {concurrency: 20}).then(function(data) {
    // all done here
});

Приятной особенностью этого интерфейса является то, что он будет хранить 20 запросов в полете. Это начнется 20, затем каждый раз, когда один заканчивается, он запускает другой. Таким образом, это потенциально более эффективно, чем отправка 20, ожидание завершения, отправка еще 20 и т. Д.

Это также обеспечивает результаты в том же порядке, что и bigArray так что вы можете определить, какой результат соответствует какому запросу.

Конечно, вы можете кодировать это самостоятельно с помощью общих обещаний, используя счетчик, но, поскольку он уже встроен в библиотеку Bluebird, я подумал, что рекомендую этот способ.

Библиотека Async также имеет аналогичный элемент управления параллелизмом, хотя, очевидно, она не основана на обещаниях.


Вот версия с ручным кодированием, использующая только обещания ES6, которые поддерживают порядок результатов и постоянно поддерживают 20 запросов (пока не осталось 20) для максимальной пропускной способности:

function pMap(array, fn, limit) {
    return new Promise(function(resolve, reject) {
        var index = 0, cnt = 0, stop = false, results = new Array(array.length);

        function run() {
            while (!stop && index < array.length && cnt < limit) {
                (function(i) {
                    ++cnt;
                    ++index;
                    fn(array[i]).then(function(data) {
                        results[i] = data;
                        --cnt;
                        // see if we are done or should run more requests
                        if (cnt === 0 && index === array.length) {
                            resolve(results);
                        } else {
                            run();
                        }
                    }, function(err) {
                        // set stop flag so no more requests will be sent
                        stop = true;
                        --cnt;
                        reject(err);
                    });
                })(index);
            }
        }
        run();
    });
}   

pMap(bigArray, apiFetch, 20).then(function(data) {
    // all done here
}, function(err) {
    // error here
});

Рабочая демонстрация здесь: http://jsfiddle.net/jfriend00/v98735uu/

Другие вопросы по тегам