Установите минимальную задержку для разрешения обещания bluebird.js

Я хочу гарантировать минимальную задержку выполнения обещания bluebird.js.

В качестве примера предположим, что я делаю запрос, заключенный в обещание. Поведение, которое я хочу, заключается в том, что если запрос занимает менее 5 секунд, я хочу искусственно увеличить задержку разрешения обещания до 5 секунд. Если бы запрос занимал более 5 секунд, я бы не хотел добавлять искусственную задержку, так что это немного сложнее, чем просто добавлять статическую задержку к каждому запросу. Все это должно быть полностью скрыто от потребителя промиса — он должен просто видеть, что промис решается за 5 секунд или больше.

Чтобы продемонстрировать, у меня есть простой пример фиктивной реализации, в котором задержка фиктивного запроса жестко задана в 3 секунды.

Моя первая попытка прошла примерно так: использование setTimeout для обеспечения того, чтобы обратный вызов разрешения не вызывался до истечения 5 секунд.

скрипка здесь

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);

Много шаблонов.

Затем с помощью этого ответа я обнаружил, что могу использовать функцию Promise.join для присоединения к упаковке обещания запрос с Promise.delay с минимальной 5-секундной задержкой для достижения того же результата:

скрипка здесь

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);

Это чище, но все же немного более шаблонно, чем хотелось бы - я копался в ссылка на API bluebird и не может найти функцию, которая делает это напрямую.

У меня простой вопрос: может ли кто-нибудь предложить более чистый, более декларативный способ достижения такого поведения с bluebird, чем во втором примере?

Любые предложения других библиотек обещаний, где API предлагает это, также будут оценены.


person davnicwil    schedule 29.01.2015    source источник


Ответы (3)


Я считаю, что все, что вам нужно сделать, это Promise.delay(value).return(promise):

Вы можете обернуть его в служебную функцию:

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/

Обратите внимание: если обещание отклонено, отложенное обещание не будет отклонено до тех пор, пока не истечет пять секунд. Это может быть желаемым поведением (как указывает @Benjamin Gruenbaum в комментариях), но если вы предпочитаете, чтобы оно было немедленно отклонено, есть два других варианта:

С 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);
}

Или подход @Benjamin Gruenbaum с Promise.all:

function minDelay(promise, delay) {
    Promise.all([promise, Promise.delay(delay)]).get(0);
}
person JLRishe    schedule 29.01.2015
comment
Если вы выполняете Promise.join с идентификатором, разве это не может быть просто Promise.all? - person Benjamin Gruenbaum; 29.01.2015
comment
@BenjaminGruenbaum Что ты имеешь в виду? Promise.all([promise, Promise.delay(delay)]) вернет обещание для массива из двух значений. - person JLRishe; 29.01.2015
comment
О, вы правы, так как delay в этом случае всегда больше или равно времени, которое мы хотим, чтобы оно потребовалось для разрешения промиса. Умный. - person Benjamin Gruenbaum; 29.01.2015
comment
Это .return — хорошая идея, но, к сожалению, имеет неблагоприятное поведение: если promise отклоняется в течение задержки, ошибка не будет немедленно всплывать, а также задерживается. - person Bergi; 29.01.2015
comment
Это именно то, что мне нужно! Когда я посмотрел на delay(x), я почему-то подумал, что он просто добавит статический x миллисекунд к любому времени, которое требуется для разрешения p. Конечно, это неправильно, потому что фактический запрос внутри p выделяется, как только p создается, поэтому p может быть готов к немедленному разрешению, когда он возвращается через x миллисекунд. Я все еще вхожу в образ мышления promises, это было очень полезно! - person davnicwil; 30.01.2015
comment
@davnicwil Promise.delay(promise, 5000) и promise.delay(5000) do имеют именно такое поведение (задержка promise еще на 5000 мс). Promise.delay(5000).return(promise) этого не делает, но у него есть неприятная ошибка, на которую указал Берги. Я вернулся к своему предыдущему ответу и добавил несколько дополнительных комментариев. - person JLRishe; 30.01.2015
comment
@Bergi Я думаю, что это на самом деле лучшее поведение - если вы дросселируете API при вызове за 1000 мс, а вызовы отклоняются через 1 мс, поскольку вы превысили лимит, вместо того, чтобы откладывать отклонение (исправление проблемы с дросселем), вы можете сделать больше и больше запросов подряд (повторные попытки), что усугубляет проблему. - person Benjamin Gruenbaum; 30.01.2015
comment
Хорошо, @Bergi действительно хороший момент, я этого не заметил, но на самом деле в моем конкретном случае это нормально - возможно, я смешиваю свою терминологию, но я хочу минимальную задержку при любом разрешении обещания, успеха или ошибки. - person davnicwil; 30.01.2015
comment
@BenjaminGruenbaum: Да, хорошо, это зависит от варианта использования. Однако для дросселирования я бы все равно попытался отделить обещание результата от обещания ожидания. - person Bergi; 30.01.2015

Твоя проблема

Прежде всего, обещание другого 3-х секундного звонка здесь не имеет значения, оно не должно быть частью обещания. Хотя я польщен, что вам понравился мой ответ с .join, это также не тот инструмент, который я бы использовал здесь.

Во-первых, вызов API — это просто произвольная функция, возвращающая обещание.

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

На самом деле, нас это мало волнует. Это может быть и просто:

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

Теперь мы хотим убедиться, что прошло не менее 3 секунд, прежде чем мы разрешим p.

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

Который принимает произвольное обещание и возвращает обещание, для разрешения которого требуется не менее ms миллисекунд.

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

Вы также можете поместить его в прототип Bluebird (если вы пишете библиотеку, обязательно сначала получите свою собственную изолированную копию):

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

Что позволит вам декларативно сделать:

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

Более общая проблема

Часто, когда люди спрашивают об этой проблеме, на самом деле их волнует то, чтобы функция возвращала результат не чаще, чем каждые миллисекунды, или чтобы функция действовала как монитор того, как часто выполняются вызовы. Это необходимо для ограничения количества вызовов, которые выполняются в веб-службе, которая ограничивает скорость. Это должно быть ограничено на уровне функции, возвращающей обещание. Например:

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
}

Это позволит вам сделать:

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
person Benjamin Gruenbaum    schedule 29.01.2015
comment
@JLRishe Я переключил все, кроме первого, на тот же шаблон, что и в вашем ответе, так как он все равно лучше. - person Benjamin Gruenbaum; 29.01.2015
comment
Но вы заметили, что он делает что-то другое? - person Bergi; 29.01.2015
comment
Спасибо за информативный ответ, использование all, а не join определенно более уместно. - person davnicwil; 30.01.2015
comment
@ Берги да, и мне это нравится намного больше. - person Benjamin Gruenbaum; 30.01.2015
comment
Хм, я считаю не немедленные ошибки (отказы) весьма нежелательными. По крайней мере, вы должны упомянуть разницу. - person Bergi; 30.01.2015

Библиотека spex была написана специально для решения таких проблем, как регулирование данных и балансировка нагрузки при использовании промисов.

В вашем случае мы можем использовать следующий пример:

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);
    });

Но это поверхностно, потому что библиотека позволяет вам реализовывать гораздо более гибкие и продвинутые (если хотите) стратегии для работы с запросами промисов.

В вашем случае может быть интересен параметр delay, который передается как в исходную, так и в целевую функции, поэтому при необходимости балансировка нагрузки может выполняться в двух направлениях.

Кроме того, вы можете использовать метод page с та же стратегия балансировки нагрузки, но обработка запросов на страницах.

person vitaly-t    schedule 04.10.2015