Set minimum delay on bluebird.js promise resolutio

2019-01-20 12:34发布

问题:

I want to guarantee a minimum delay on the resolution of a bluebird.js promise.

As an example, let's say I'm making a request wrapped in a promise. The behaviour I want is that if the request takes less than 5 seconds, I want to artificially increase the delay of the promise resolution to 5 seconds. If the request were to take more than 5 seconds, I'd want no artificial delay added - so it's a little more complicated than just adding a static delay to every request. All of this should be completely hidden from the consumer of the promise - they should just see the promise being resolved in 5 seconds, or more.

To demonstrate, I've got a simple mock implementation example which hardcodes the mocked request latency at 3 seconds.

My first try went something like this - using a setTimeout to ensure the resolve callback isn't called before 5 seconds have passed.

fiddle here

function getTimestamp() {
  return new Date().getTime();   
}

function makeCallWith3SecondLatency(cb) {
  console.log('mocking a call with 3 second latency...');
  var mockResult = 'the result';
  setTimeout(function() { cb(mockResult); }, 3000);
}

function doSomethingAsync(minDelay) {
  return new Promise(function(resolve) {
    var calledAt = getTimestamp();
    makeCallWith3SecondLatency(function(arg) {
      var actualDelay = getTimestamp() - calledAt;
      if(actualDelay < minDelay) {
        var artificialDelay = minDelay - actualDelay;
        console.log('artificially delay another ' + artificialDelay + ' millis');
        setTimeout(function() { resolve(arg); }, artificialDelay);
      } else {
        resolve(arg);
      }
    });
  });
}

function printResult(result) {
  console.log('result: ' + result)   
}

var minDelay = 5000;
doSomethingAsync(minDelay).then(printResult);

A lot of boilerplate.

I then discovered through this answer that I could use the Promise.join function to join the promise wrapping the request with a Promise.delay of the minimum 5 second delay to achieve the same thing:

fiddle here

function makeCallWith3SecondLatency(cb) {
  console.log('mocking a call with 3 second latency...');
  var mockResult = 'the result';
  setTimeout(function() { cb(mockResult); }, 3000);
}

function doSomethingAsync(minDelay) {
  return Promise.join(
                new Promise(function(resolve) { makeCallWith3SecondLatency(resolve); }),
                Promise.delay(minDelay).then(function() { console.log('artificially delaying 5 seconds with Promise.delay') }),
                function(result) { return result; });
}

function printResult(result) {
  console.log('result: ' + result)   
}

var minDelay = 5000;
doSomethingAsync(minDelay).then(printResult);

This is cleaner, but still a little more boilerplate than I'd like - I've dug around the bluebird api reference and can't find a function which does this directly.

My question is simple - can anybody suggest a cleaner, more declarative way of achieving this behaviour with bluebird than the second example?

Any suggestions of other promise libraries where the api does offer this would also be appreciated.

回答1:

I believe that all you need to do is Promise.delay(value).return(promise):

You can wrap it in a utility function:

function stallPromise(promise, delay) {
    return Promise.delay(delay).return(promise);
}

function doSomethingAsync(minDelay) {
    var p = new Promise(makeCallWith3SecondLatency); 

    return stallPromise(p, minDelay);
}

var minDelay = 5000;
doSomethingAsync(minDelay).then(printResult);

http://jsfiddle.net/s572rg7y/1/

Note that one thing about this is that if the promise rejects, the delayed promise will not reject until the five seconds have elapsed. This may be the desired behavior (as @Benjamin Gruenbaum points out in the comments), but if you would prefer for it to reject immediately, two other options are:

With Promise.join:

function stallPromise(promise, delay) {
    // if you're using underscore/lodash, you can use _.identity for this
    function identity(val) { return val; }

    return Promise.join(promise, Promise.delay(delay), identity);
}

Or @Benjamin Gruenbaum's approach with Promise.all:

function minDelay(promise, delay) {
    Promise.all([promise, Promise.delay(delay)]).get(0);
}


回答2:

Your problem

First of all, the promisification of the other 3 second call is irrelevant here, it should not be part of the promise. While I'm flattered you liked my answer with .join it is also not a tool I'd actually use here.

First, the API call is just an arbitrary promise returning function.

function yourApiCall(){
    // your function that returns a promise AFTER promisificatin
}

Actually, we don't really care about it. It might as well just be:

var p = ... ; //p is a promise

Now we want to make sure at least 3 seconds pass before we resolve p.

function minDelay(p, ms){ // stealing name from JLRishe's answer
    return Promise.all([p, Promise.delay(ms)]).get(0);
}

Which takes an arbitrary promise and returns a promise that takes at least ms milliseconds to resolve.

minDelay(p, 300).then(function(el){
   // el is minDelay's return value and at least 300 ms have passed
});

You can also put it on Bluebird's prototype (if you're writing a library be sure to get your own isolated copy first):

Promise.prototype.minDelay = function minDelay(ms){
    // note that unlike the example above this will delay 
    // on rejections too 
    return Promise.delay(ms).return(this);
}

Which would let you declaratively do:

p.minDelay(300).then(function(res){
   // access result
}); 

A more general problem

Often when people ask about this problem what they actually care about is making a function return a result at most every number of milliseconds or make a function act as a monitor for how often calls are made. This is to throttle the number of calls that are made to a web service that rate-limits. This should be limited at the level of the function that returns the promise. For example:

var queue = Promise.resolve();
function throttle(fn, ms){
    var res = queue.then(function(){ // wait for queue
        return fn(); // call the function
    });
    queue = Promise.delay(ms).return(queue); // make the queue wait
    return res; // return the result
}

This would let you do:

function myApiCall(){
    // returns a promise
}
var api = throttle(myApiCall, 300); // make call at most every 300 ms;

api(); // calls will be sequenced and queued
api(); // calls will be made at most every 300 ms
api(); // just be sure to call this directly, return this to consumers


回答3:

Library spex was written specifically to deal with issues like data throttling and load balancing when using promises.

In your case we can use the following example:

var spex = require('spex')(Promise);

function source(index, data, delay) {
    var start = Date.now();
    return new Promise(function (resolve) {
        // request your data here;

        var end = Date.now();
        if (end - start < 5000) {
            setTimeout(function () {
                resolve();
            }, 5000 - end + start);
        } else {
            resolve();
        }
    });
}

function dest(index, data, delay) {
    // you can do additional load balancing here,
    // while processing the data;
}

spex.sequence(source, dest)
    .then(function (data) {
        console.log("DATA:", data);
    });

But that's just scratching the surface, because the library lets you implement much more flexible, and advanced (if you want) strategies for dealing with the promise requests.

For your case what might be interesting is parameter delay that's passed into both the source and destination functions, so the load balancing can be handled two-ways when needed.

In addition, you can employ method page with the same load balancing strategy, but processing requests in pages.