-2

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?

AliOz
  • 335
  • 2
  • 10
  • 1
    It is confusing that you start with an example of `myArray`, but then show code where you have **multiple** arrays, each apparently with just a value at index `i` which is not defined... – trincot Sep 26 '22 at 19:40
  • Uh, just write a loop to do this instead of repeating the code many times? – Bergi Sep 26 '22 at 19:45
  • Please post your complete, actual code if you need help with simplifying it – Bergi Sep 26 '22 at 19:46
  • @trincot I fixed my description, please check it again – AliOz Sep 26 '22 at 19:49
  • @Bergi I fixed the description, please check it again – AliOz Sep 26 '22 at 19:49
  • That call to `Promise.all` makes little sense. It will resolve immediately as all promises in `allPromises` have all resolved already. – trincot Sep 26 '22 at 19:51
  • @trincot I need ```Promise.all```, to finish executing the first 20 async calls before navigating into the other sub-array – AliOz Sep 26 '22 at 19:52
  • 1
    Sure, but they **are** already resovled by the time you execute `Promise.all`. What else is `await` doing with each of them, you think? – trincot Sep 26 '22 at 19:53
  • @AliOz Where do the `subArray1`, `subArray2`, `subArray3` etc come from? – Bergi Sep 26 '22 at 19:55
  • You actually want a `Promise.any` and each time a promise resolves, schedule more work and try to keep your queue of promises full at a length of 20. – Wyck Sep 26 '22 at 19:56
  • @trincot More concretely, the `promise` variable does not hold a promise, since the promise has already been `await`ed - it will hold the result value. Consequently, `allPromises` is an array of values, not an array of promises; and indeed, the `Promise.all` call is superfluous. Or probably the `await` was not meant to be part of `await myAsyncFunc(subArray1[i]);`. – Bergi Sep 26 '22 at 19:58
  • That's what I wrote in my latest edit to my answer. – trincot Sep 26 '22 at 19:58
  • @trincot Ah, right, I only read your comment "*all promises in allPromises have all resolved already*" – Bergi Sep 26 '22 at 19:59
  • @Bergi ```subArray1```, ```subArray2```, ```subArray3``` are sub arrays of the original array ```myArray```, each one of the subarrays has length 20 – AliOz Sep 26 '22 at 19:59
  • Yes, that was a misnomer. – trincot Sep 26 '22 at 19:59
  • @trincot my issue is not with ```Promise.all```, my issue is how to wait 60 seconds after each 20 async call – AliOz Sep 26 '22 at 20:02
  • 1
    @AliOz Then it sounds like a duplicate of https://stackoverflow.com/questions/37213316/execute-batch-of-promises-in-series-once-promise-all-is-done-go-to-the-next-bat or https://stackoverflow.com/questions/72923347/dealing-with-rate-limiting-apis-in-nodejs/72923433#72923433 – Bergi Sep 26 '22 at 20:02

2 Answers2

1

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.

trincot
  • 317,000
  • 35
  • 244
  • 286
1

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);
        }
    });
}
user3252327
  • 617
  • 5
  • 9