簡體   English   中英

如何將 Promise.all() 限制為每秒 5 個承諾?

[英]How to throttle Promise.all() to 5 promises per second?

我有幾個項目需要查詢第 3 方 API,並且說 API 的調用限制為每秒 5 次調用。 我需要以某種方式將對 API 的調用限制為每秒最多 5 次調用。

到目前為止,我只是在承諾數組上使用了Promise.all() ,其中每個承諾向 API 發送請求,並在 API 以 HTTP 狀態代碼200響應時解析,並在以其他狀態代碼響應時拒絕。 但是,當我在數組中有超過 5 個項目時,我冒着被Promise.all()拒絕的風險。

如何將Promise.all()調用限制為每秒 5 次調用?

在沒有庫的情況下使用 ES6

export async function asyncForEach(array, callback) {
  for (let index = 0; index < array.length; index++) {
    await callback(array[index], index, array);
  }
}
export function split(arr, n) {
  var res = [];
  while (arr.length) {
    res.push(arr.splice(0, n));
  }
  return res;
}
export const delayMS = (t = 200) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(t);
    }, t);
  });
};
export const throttledPromises = (
  asyncFunction,
  items = [],
  batchSize = 1,
  delay = 0
) => {
  return new Promise(async (resolve, reject) => {
    const output = [];
    const batches= split(items, batchSize);
    await asyncForEach(batches, async (batch) => {
      const promises = batch.map(asyncFunction).map(p => p.catch(reject));
      const results = await Promise.all(promises);
      output.push(...results);
      await delayMS(delay);
    });
    resolve(output);
  });
};

我希望這會對你有所幫助。

還要說的是,這將使用Promise.all來解決所有請求,如果您有大量查詢,這將等待所有請求都解決,並可能導致您的代碼等待很多時間才能獲得所有響應。 而且,如果其中一個請求被拒絕, Promise.all也會拒絕。

我建議,如果您不需要所有結果,最好使用其他方法,例如 lodash debouncethrottle或處理此問題的框架。

let items = [
    {name: 'item1'}, 
    {name: 'item2'}, 
    {name: 'item3'}, 
    {name: 'item4'}, 
    {name: 'item5'}, 
    {name: 'item6'}
];

// This is the api request that you send and return a promise
function apiCall(item) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(item.name), 1000);
  })
}

new Promise((resolve) => {
  let results = [];

  function sendReq (itemsList, iterate, apiCall) {
    setTimeout(() => {
      // slice itemsList to send request according to the api limit
      let slicedArray = itemsList.slice(iterate * 5, (iterate * 5 + 5));
      result = slicedArray.map(item => apiCall(item));
      results = [...results, ...result];

      // This will resolve the promise when reaches to the last iteration
      if (iterate === Math.ceil(items.length / 5) - 1) {
          resolve(results);
      }
    }, (1000 * iterate)); // every 1000ms runs (api limit of one second)
  }

  // This will make iteration to split array (requests) to chunks of five items 
  for (i = 0; i < Math.ceil(items.length / 5); i++) {
    sendReq(items, i, apiCall);
  }
}).then(Promise.all.bind(Promise)).then(console.log);
// Use Promise.all to wait for all requests to resolve
// To use it this way binding is required

如果您不太擔心按順序解決承諾,則可以在 bluebird 中使用並發選項。

下面將一次只處理 5 個查詢。

const Promise = require('bluebird');

const buildQueries = (count) => {
  let queries = [];

  for(let i = 0; i < count; i++) {
    queries.push({user: i});
  };

  return queries;
};

const apiCall = (item) => {
  return new Promise(async (resolve, reject) => {
    await Promise.delay(1000);
    resolve(item.user);
  });
};

const queries = buildQueries(20);

Promise.map(queries, async query => {
  console.log( await apiCall(query) );
}, {concurrency: 5});

也許我頭腦簡單,但我寫的這個版本只是將傳入數組分成 5 個承諾的塊,並在每個塊上執行Promise.all()

utility.throttledPromiseAll = async (promises) => {
  const MAX_IN_PROCESS = 5;
  const results = new Array(promises.length);

  async function doBlock(startIndex) {
    // Shallow-copy a block of promises to work on
    const currBlock = promises.slice(startIndex, startIndex + MAX_IN_PROCESS);
    // Await the completion. If any fail, it will throw and that's good.
    const blockResults = await Promise.all(currBlock);
    // Assuming all succeeded, copy the results into the results array
    for (let ix = 0; ix < blockResults.length; ix++) {
      results[ix + startIndex] = blockResults[ix];
    }
  }

  for (let iBlock = 0; iBlock < promises.length; iBlock += MAX_IN_PROCESS) {
    await doBlock(iBlock);
  }
  return results;
};

我想你可以把你的問題分成兩部分:同時不超過 5 個呼叫,並確保最新的呼叫在最舊的 1 秒之后才發生。

第一部分很容易用一個驚人的p-limit庫來解決——它有迄今為止我見過的最簡單的界面。

對於第二部分,您需要實際跟蹤每次調用何時開始 - 即實現等待功能:基本偽代碼,尚未測試:

import pLimit from 'p-limit';
const apiLimit = pLimit(5);

const startTimes = [];

async function rateLimiter(item) {
  const lastSecond = (new Date().getTime()) - 1000;
  if (startTimes.filter(v => v > lastSecond).length >= 5) {
    await new Promise(r => setTimeout(r, 1000));
  }
  // TODO: cleanup startTimes to avoid memory leak
  startTimes.push(new Date().getTime());
  return apiCall(item);
}

await Promise.all(items.map(v => apiLimit(() => rateLimiter(v))))

我們可以使用生成器來發送組中的承諾列表。 一旦解決了第一個 yield,我們就可以進行另一個 yield。 我們將結果存儲在一個數組中。 一旦 promiseArray 長度等於結果長度,我們就可以解析包裝的 Promise。

const fetch = require("isomorphic-fetch");
const totalPromiseLength = 5;
const requestMethod = url => () => fetch(url).then(response => response.json());
let promiseArray = [...new Array(totalPromiseLength).keys()].map(index =>
  requestMethod("https://jsonplaceholder.typicode.com/todos/" + (index + 1))
);
function* chunks(arr, limit) {
  for (let i = 0; i < Math.ceil(arr.length / limit); ++i) {
    yield [...arr].slice(i * limit, i * limit + limit);
  }
}

new Promise(async resolve => {
  let generated = chunks(promiseArray, 2);
  let result = [];
  for (let bla of generated) {
    await Promise.all(bla.map(param => param())).then(response => {
      result = [...result, ...response];
      if (result.length === promiseArray.length) {
        resolve(result);
      }
    });
  }
}).then(response => {
  console.log(response);
});

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM