Implementing exponential backoff with Node.js and Promises

Viewed 11551

I'm making about 70 requests to an API in my code. I'm getting an error response telling me that I'm making requests too quickly one after the other and I have decided to use the idea of exponential backoff to get through this problem.

Currently, this is what my code looks like:

  let backoffTime = 1;

  for (let i = 0; i < fileIds.length; i++) {
    let fileId = fileIds[i];
    getFileName(fileId, auth)
    .then((name) => {
      // do some work
    })
    .catch((err) => {
      // assumes that the error is "request made too soon"
      backoff(backoffTime);
      backoffTime *= 2;
      i--;
      console.log(err);
    });
  }

function backoff(time) {
  let milliseconds = time * 1000;
  let start = (new Date()).getTime();
  while (((new Date()).getTime() - start) < milliseconds) {
    // do nothing
  }
}

My getFileName function makes the request to the API and returns a Promise.

Currently this does not work because Promises are async (kinda). My for loop runs really fast and calls the getFileName function which makes those API requests really fast. Then, it gets the error for some of the API calls in which case it updates the backoffTime. This implementation doesn't work.

Any idea how I can implement this correctly?

3 Answers

First of all blocking the browser with a nearly infinite loop is a very very bad idea, just use promises:

 const delay = ms => new Promise(res => setTimeout(res, ms));

Then just await the promise before continuing the loop and use the delay:

 (async function() {
   for (let i = 0; i < fileIds.length; i++) {
     let fileId = fileIds[i];
     await getFileName(fileId, auth)
      .then((name) => {
        // do some work
      })
      .catch((err) => {
        // assumes that the error is "request made too soon"
        backoffTime *= 2;
        i--;
        console.log(err);
        return delay(backoffTime);
      });
   }
})();

The easiest way is to use async/await and then either await each request, or if it is too slow for you, then create chunks with i.e. 15 requests and Promise.all such chunks.

You can also use this: https://caolan.github.io/async/parallelLimit.js.html

It will require some extra work to transition promises into callbacks and vice versa, but it will do the best job.

This is the function: parallelLimit(tasks, limit, callback)

const tasks = [];
// This for-cycle will just prepare the tasks into array of functions
for (let i = 0; i < fileIds.length; i++) {
  tasks.push(callback => doWorkThatReturnsPromise(fileIds[i])
     .then(val => callback(null, val))
     .catch(callback));
}

async.parallelLimit(tasks, 15, (err, values) => {
  // do something after it finishes
})

You can fix that using closures, here is an example:

for (let i = 0; i < fileIds.length; i++) {
  let fileId = fileIds[i];
  doCall(fileId);
}

function doCall(fileId, backoffTime = 1){
  getFileName(fileId, auth)
    .then((name) => {
      // do some work
    })
    .catch((err) => {
      setTimeout(() => {
         doCall(fileId, (backoffTime * 2));
      }, backoffTime * 1000);
    });
}

I've replaced the backoff function which pauses the execution using a while loop with a setTimeout call.

This code can cause an infinite recursion, you should probably add some checks to prevent that.

Related