I have an array of objects, and for each object inside the array I want to call an asynchronous function.
However, I should only send 20 requests in a minute.
Example:
const myArray = ["a","b","aa","c","fd","w"...]
myArray length is 60.
I used to split myArray into sub-arrays of length 20 each and iterate over each sub-array and call the async function.
Then wait for a minute and then iterate over the next sub-array and so on. However I was doing that manually in a way such as
let promise;
let allPromises = [];
// Iterate over subArray1 and call the function for each element
for(let i = 0 ; i < subArray1.length ; i ){
promise = await myAsyncFunc(subArray1[i]);
allPromises.push(promise);
}
// wait for all the async functions to get resolved using Promise.all()
await Promise.all(allPromises);
allPromises = [];
await new Promise((resolve) => setTimeout(resolve, 60000));
if(subArray2){
// Iterate over subArray2 and call the function for each element
for(let i = 0 ; i < subArray2.length ; i ){
promise = await myAsyncFunc(subArray2[i]);
allPromises.push(promise);
}
// wait for all the async functions to get resolved using Promise.all()
await Promise.all(allPromises);
allPromises = [];
await new Promise((resolve) => setTimeout(resolve, 60000));
}
if(subArray3){
// Iterate over subArray3 and call the function for each element
for(let i = 0 ; i < subArray3.length ; i ){
promise = await myAsyncFunc(subArray3[i]);
allPromises.push(promise);
}
// wait for all the async functions to get resolved using Promise.all()
await Promise.all(allPromises);
allPromises = [];
await new Promise((resolve) => setTimeout(resolve, 60000));
}
If my array got more than 100 elements that would be an issue. Can anyone provide a practical way to handle this situation?
CodePudding user response:
A naive way to handle this would be to batch requests and wait for a batch to finish, wait a minute and launch the next batch.
Another way is to batch the first 20 requests, then everytime we get a response, we schedule the next fetch in 60s so we have always have 20 requests in a sliding window of 60 seconds.
Here is how both approaches could be implemented
const MAX_REQUESTS = 20;
// let's assume you have a fetchData function that takes dataRequestInfo as a param and builds the fetch from there
async function fetchData(dataRequestInfo) {
// do your fetch here, process the data and return your model
}
/****** naive batch approach ******/
async function batchRequests(dataRequestInfoList) {
const batchSize = MAX_REQUESTS;
const batches = splitToBatches(dataRequestInfoList, batchSize);
const results = [];
for (let batch of batches) {
const start = Date.now();
const batchResults = await Promise.all(batch.map(fetchData));
results.push(...batchResults);
// wait for 1 minutes before next batch
await new Promise((resolve) => setTimeout(resolve, 60 * 1000));
// alternatively wait for 1 minutes since the first fetch
// const duration = Date.now() - start;
// await new Promise(resolve => setTimeout(resolve, 60 * 1000 - duration));
// do something with batchResults if needed
}
return results;
}
function splitToBatches(arr, batchSize) {
const batches = [];
const lastIndex = arr.length - 1;
let index = 0;
while (index <= lastIndex) {
const nextIndex = index batchSize;
batches.push(arr.slice(index, nextIndex));
// in case the last element is in an isolated batch we exit the loop
if (index === lastIndex) {
break;
}
index = Math.min(lastIndex, nextIndex);
}
return batches;
}
/****** batch then queue requests ******/
async function batchThenQueueRequests(dataRequestInfoList) {
return new Promise(resolve => {
let currentIndex = MAX_REQUESTS;
let processedRequests = 0;
const results = [];
const fetchNext = async (result) => {
processedRequests ;
results.push(result)
if (processedRequests.length === dataRequestInfoList.length) {
// everything was processed let's resolve the promise
resolve(results);
}
if (currentIndex >= dataRequestInfoList.length) {
// no more data to fetch
return;
};
// wait for 1 minute before next request
await new Promise(resolve => setTimeout(resolve, 60 * 1000));
fetchData(dataRequestInfoList[currentIndex]).then(fetchNext);
currentIndex ;
}
for (let i = 0; i < MAX_REQUESTS; i ) {
fetchData(dataRequestInfoList[i]).then(fetchNext);
}
});
}
CodePudding user response:
Your call of Promise.all
is not necessary as by the time it executes, all the promises of the preceding loop have already resolved. promises
is not an array of promises, but an array of resolution values.
You should have your values in one array, like you presented myArray
, and then you can just perform a for..of
loop:
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const myArray = [...subArray1, ...subArray2, ...subArray3];
for (const value of myArray) {
await myAsyncFunc(value);
await delay(3000); // 3 second cooldown
}
There is no need to chunk a long array into subarrays of at most 20 elements, since this code inserts a cooldown of 3 seconds after each request, so it is guaranteed that you'll not have more than 20 requests per minute.
If the time to resolve myAsyncFunc()
is significant compared to those 3 seconds, then you would execute considerably fewer requests per minute. In that case you can start the timer for 3 seconds at the same time as you initiate the request:
for (const value of myArray) {
await Promise.all(myAsyncFunc(value), delay(3030));
}
The delay is here intentionally a bit bigger than 3 seconds, so to take no risk of being too close to 20/minute.