2

I have a data array of objects like [{number:1}, {number:2}, {number:3}... {number:100}]. And want to make parallel API calls in successive batches of 10 until the whole array has been processed.

How would I go about that?

Here's my code. It goes over the first 10, but then it stops.

const async = require("async");
const axios = require("axios");
const calls = require("../model/data"); // [{number:1}, {number:2},{number:3},...{number:100}]

let makeAPICall = function (request, callback) {
  axios
    .post("http://www.api.com/", {
      number: `${request.number}`,
      webhookURL: "http://localhost:8000/",
    })
    .then(function (response) {})
    .catch(function (err) {
      callback(err);
    });
};

const functionArray = calls.map((request) => {
  return (callback) => makeAPICall(request, callback);
});

exports.startCalls = (req, res, next) => {
  async.parallelLimit(functionArray, 10, (err, results) => {
    if (err) {
      console.error("Error: ", err);
    } else {
      console.log("Results: ", results.length, results);
    }
  });
};
  • For starters, your `makeAPICall()` function does not call the callback when the axios call is successful. – jfriend00 Sep 27 '20 at 23:35
  • Thanks! I fixed it. – Fernanda Alves Sep 27 '20 at 23:58
  • Are you saying that now the rest of it works? – jfriend00 Sep 28 '20 at 00:04
  • It's processing the whole thing almost (except for one), but not quite in order. The latest batches are finishing first. Also, the post requests made by the API to the server are out of order. – Fernanda Alves Sep 28 '20 at 00:43
  • Running requests in parallel means they can finish in an unknown order. A good function for managing parallel requests will gather the results in the order that you originally specified the requests, but when exactly each inidividual one finishes is blind luck (you're running asynchronous operations in parallel so they finish whenever they finish). If you absolutely want them to run and finish in order, then you have to sequence them one after another, not run them in parallel. – jfriend00 Sep 28 '20 at 00:55
  • It's fine if the actual calls I make in parallel are out of order, but I want the batches to run in order. Like [1,2,3] and then [4,5,6]... – Fernanda Alves Sep 28 '20 at 01:00
  • Well, that's likely not how `parallelLimit()` works. These kinds of solutions don't run in fixed sized batches. They start up the max requests and then each time one request finishes, they run another one until they've run them all. My `mapConcurrent()` in the answer below also does it that way because that's a faster way to get all the results. The results in `mapConcurrent()` are collected in order so when you're all done you have all the results in the original order. – jfriend00 Sep 28 '20 at 01:08
  • If you really need to run them in fixed sized batches of 10 requests where all 10 finish before you start any more, I'd have to write a different answer than the one below. I'm not sure why that would be better than keeping 10 in flight at all times though (and finishing faster) so I'll wait for your feedback. – jfriend00 Sep 28 '20 at 01:11
  • Alright. I'll try to make the MapConcurrent() solution work. I'll let you know if I have any questions. Thank you for all the help. – Fernanda Alves Sep 28 '20 at 02:27

2 Answers2

0

So, your approach is somewhat backwards as you have something that already returns a promise (Axios) which is the modern way to manage asynchronous operations and now you're trying to convert it back to a plain callback so you can use an old-fashioned library (the async library). It's also slower to run 10, wait for all 10 to finish before starting any more when that is typically not necessary.

Instead, I would suggest you use the Axios promise you already have. You can either use Bluebird's .map() which has a concurrency setting or you can use this bit of code that gives you a function for running promise-producing functions in parallel while controlling the max number that are in-flight at any given time:

// takes an array of items and a function that returns a promise
function mapConcurrent(items, maxConcurrent, fn) {
    let index = 0;
    let inFlightCntr = 0;
    let doneCntr = 0;
    let results = new Array(items.length);
    let stop = false;

    return new Promise(function(resolve, reject) {

        function runNext() {
            let i = index;
            ++inFlightCntr;
            fn(items[index], index++).then(function(val) {
                ++doneCntr;
                --inFlightCntr;
                results[i] = val;
                run();
            }, function(err) {
                // set flag so we don't launch any more requests
                stop = true;
                reject(err);
            });
        }

        function run() {
            // launch as many as we're allowed to
            while (!stop && inflightCntr < maxConcurrent && index < items.length) {
                runNext();
            }
            // if all are done, then resolve parent promise with results
            if (doneCntr === items.length) {
                resolve(results);
            }
        }

        run();
    });
}

You would then use it like this:

let makeAPICall = function(request) {
  return axios.post("http://www.api.com/", {
      number: `${request.number}`,
      webhookURL: "http://localhost:8000/",
    });
};

mapConcurrent(calls, 10, makeAPICall).then(results => {
   // all results in order here
   console.log(results);
}).catch(err => {
    console.log(err);
});

See another similar issue here: Promise.all consumes all my RAM

jfriend00
  • 683,504
  • 96
  • 985
  • 979
0

If you really want to run them in fixed batches where the whole batch finishes before you run any more requests, you could do something like this:

const axios = require("axios");
const calls = require("../model/data"); // [{number:1}, {number:2},{number:3},...{number:100}]

function makeAPICall(request) {
  return axios.post("http://www.api.com/", {
      number: `${request.number}`,
      webhookURL: "http://localhost:8000/",
  });
};

async function runBatches(array, batchSize, fn) {
    let index = 0;
    let results = [];
    while (index < array.length) {
        let promises = [];
        for (let num = 0; num < batchSize && index < array.length; ++num) {
            promises.push(makeAPICall(array[index++]));
        }
        let batchResults = await Promise.all(promises);
        results.push(...batchResults);
    }
    return results;
}

runBatches(calls, 10, makeAPICall).then(results => {
   // all results in order here
   console.log(results);
}).catch(err => {
    console.log(err);
});
jfriend00
  • 683,504
  • 96
  • 985
  • 979