Search code examples
javascriptqueuepromisebluebirdrate-limiting

Timed promise queue / throttle


I have a request-promise function that makes a request to an API. I'm rate-limited by this API and I keep getting the error message:

Exceeded 2 calls per second for api client. Reduce request rates to resume uninterrupted service.

I'm running a couple of Promise.each loops in parallel which is causing the issue, if I run just one instance of Promise.each everything runs fine. Within these Promise.each calls they lead to the same function a with a request-promise call. I want to wrap this function with another queue function and set the interval to 500 milliseconds so that a request isn't made after one another, or parallel, but set to that time, on queue. The thing is I still need these promises to get their contents even if it takes a rather long time to get a response.

Is there anything that will do this for me? Something I can wrap a function in and it will respond at a set interval and not in parallel or fire functions one after another?

Update: Perhaps it does need to be promise specific, I tried to use underscore's throttle function

var debug = require("debug")("throttle")
var _ = require("underscore")
var request = require("request-promise")

function requestSite(){
  debug("request started")
  function throttleRequest(){
    return request({
      "url": "https://www.google.com"
    }).then(function(response){
      debug("request finished")
    })
  }
  return _.throttle(throttleRequest, 100)
}

requestSite()
requestSite()
requestSite()

And all I got back was this:

$ DEBUG=* node throttle.js 
throttle request started +0ms
throttle request started +2ms
throttle request started +0ms

Solution

  • Update

    The last answer was wrong, this works but I still think I can do better:

    // call fn at most count times per delay.
    const debounce = function (fn, delay, count) {
        let working = 0, queue = [];
        function work() {
            if ((queue.length === 0) || (working === count)) return;
            working++;
            Promise.delay(delay).tap(() => working--).then(work);
            let {context, args, resolve} = queue.shift();
            resolve(fn.apply(context, args));
        }
        return function debounced() {
            return new Promise(resolve => {
                queue.push({context: this, args: arguments, resolve});
                if (working < count) work();
            });
        };
    };
    
    function mockRequest() {
        console.log("making request");
        return Promise.delay(Math.random() * 100);
    }
    
    var bounced = debounce(mockRequest, 800, 5);
    for (var i = 0; i < 5; i++) bounced();
    setTimeout(function(){
        for (var i = 0; i < 20; i++) bounced();
    },2000);
    

    So you need to make the requests throttle function-wide - that's fine. Promises have queueing pretty much built in.

    var p = Promise.resolve(); // our queue
    
    function makeRequest(){
        p = p.then(function(){ // queue the promise, wait for the queue
            return request("http://www.google.com");
        });
        var p2 = p; // get a local reference to the promise
        // add 1000 ms delay to queue so the next caller has to wait 
        p = p.delay(1000); 
        return p2;
    };
    

    Now makeRequest calls will be at least 1000ms apart.

    jfriend has pointed out that you need two requests per second and not a single one - this is just as easily solvable with a second queue:

    var p = Promise.resolve(1); // our first queue
    var p2 = Promise.resolve(2); // our second queue
    
    function makeRequest(){
    
        var turn = Promise.any([p, p2]).then(function(val){ 
    
            // add 1000 ms delay to queue so the next caller has to wait 
            // here we wait for the request too although that's not really needed, 
            // check both options out and decide which works better in your case
            if(val === 1){
                p = p.return(turn).delay(1, 1000);
            } else {
                p2 = p2.return(turn).delay(1, 1000); 
            }
            return request("http://www.google.com");
        });
    
        return turn; // return the actual promise
    };
    

    This can be generalized to n promises using an array similarly