承诺重试设计模式

use*_*195 53 javascript node.js promise

编辑

  1. 继续重试的模式,直到promise结算(使用delay和maxRetries).
  2. 继续重试的模式,直到条件满足结果(使用delay和maxRetries).
  3. 具有无限重试的内存高效动态模式(提供延迟).

代码为#1.继续重试,直到诺言解决(语言的任何改进社区等?)

Promise.retry = function(fn, times, delay) {
    return new Promise(function(resolve, reject){
        var error;
        var attempt = function() {
            if (times == 0) {
                reject(error);
            } else {
                fn().then(resolve)
                    .catch(function(e){
                        times--;
                        error = e;
                        setTimeout(function(){attempt()}, delay);
                    });
            }
        };
        attempt();
    });
};
Run Code Online (Sandbox Code Playgroud)

使用

work.getStatus()
    .then(function(result){ //retry, some glitch in the system
        return Promise.retry(work.unpublish.bind(work, result), 10, 2000);
    })
    .then(function(){console.log('done')})
    .catch(console.error);
Run Code Online (Sandbox Code Playgroud)

#2的代码继续重试,直到条件then以可重用的方式满足结果(条件是变化的).

work.publish()
    .then(function(result){
        return new Promise(function(resolve, reject){
            var intervalId = setInterval(function(){
                work.requestStatus(result).then(function(result2){
                    switch(result2.status) {
                        case "progress": break; //do nothing
                        case "success": clearInterval(intervalId); resolve(result2); break;
                        case "failure": clearInterval(intervalId); reject(result2); break;
                    }
                }).catch(function(error){clearInterval(intervalId); reject(error)});
            }, 1000);
        });
    })
    .then(function(){console.log('done')})
    .catch(console.error);
Run Code Online (Sandbox Code Playgroud)

Roa*_*888 53

有点不同......

异步重试可以通过构建.catch()链来实现,而不是更常见的.then()链.

这种方法是:

  • 只有指定的最大尝试次数才可能.(链必须是有限长度的),
  • 只有在最大值较低时才建议.(承诺链消耗大致与其长度成比例的内存).

否则,使用递归解决方案.

首先,一个实用函数用作.catch()回调.

var t = 500;

function rejectDelay(reason) {
    return new Promise(function(resolve, reject) {
        setTimeout(reject.bind(null, reason), t); 
    });
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以非常简洁地构建.catch链:

1.重试,直到承诺解决,拖延

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).catch(rejectDelay);
}
p = p.then(processResult).catch(errorHandler);
Run Code Online (Sandbox Code Playgroud)

演示:https://jsfiddle.net/duL0qjqe/

2.重试直到结果满足某些条件,没有延迟

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).then(test);
}
p = p.then(processResult).catch(errorHandler);
Run Code Online (Sandbox Code Playgroud)

演示:https://jsfiddle.net/duL0qjqe/1/

3.重试直到结果满足某些条件,并延迟

得到你的想法(1)和(2),综合测试+延迟同样微不足道.

var max = 5;
var p = Promise.reject();

for(var i=0; i<max; i++) {
    p = p.catch(attempt).then(test).catch(rejectDelay);
    // Don't be tempted to simplify this to `p.catch(attempt).then(test, rejectDelay)`. Test failures would not be caught.
}
p = p.then(processResult).catch(errorHandler);
Run Code Online (Sandbox Code Playgroud)

test() 可以是同步的或异步的.

添加进一步的测试也是微不足道的.只需在两个渔获物之间夹上一连串的thens.

p = p.catch(attempt).then(test1).then(test2).then(test3).catch(rejectDelay);
Run Code Online (Sandbox Code Playgroud)

演示:https://jsfiddle.net/duL0qjqe/3/


所有版本都旨在attempt成为一个承诺返回异步功能.它也可以设想返回一个值,在这种情况下,链将跟随其成功路径到下一个/终端.then().

  • 整个解决方案路径对我来说似乎很奇怪.因为它可能会重试N次,所以它会在需要时预先创建N个对象.如果实际操作在第一次尝试中成功,那么您不仅不必要地创建N-1个对象,而且必须将它们处理掉.它似乎在概念上效率低下,如果N只是一个很小的数字,它实际上效率很低.它也无法像递归链式解决方案一样多次重试.例如,它不知道如何实现"自动重试最多2分钟". (3认同)
  • @ user2727195 - 我也不确定`test()`函数如何能够回复故障与重试跟随之间的差异,或者将错误与错误进行通信以及所有重试中止.由于很多解决方案都有这种结构`p.catch(attempt).catch(rejectDelay);`,因此没有能力在`attempt()`中拒绝进一步处理.对于现实世界的情况来说,这似乎过于简单化了.通常有失败表明需要重试,失败表明不应该进行进一步的重试,事实上这似乎是OP代码中的情况. (3认同)

Yai*_*lka 21

2.继续重试的模式,直到条件满足结果(带延迟和maxRetries)

这是一种以递归方式使用本机promise进行此操作的好方法:

const wait = ms => new Promise(r => setTimeout(r, ms));

const retryOperation = (operation, delay, times) => new Promise((resolve, reject) => {
  return operation()
    .then(resolve)
    .catch((reason) => {
      if (times - 1 > 0) {
        return wait(delay)
          .then(retryOperation.bind(null, operation, delay, times - 1))
          .then(resolve)
          .catch(reject);
      }
      return reject(reason);
    });
});
Run Code Online (Sandbox Code Playgroud)

这就是你如何调用它,假设func有时成功但有时失败,总是返回一个我们可以记录的字符串:

retryOperation(func, 1000, 5)
  .then(console.log)
  .catch(console.log);
Run Code Online (Sandbox Code Playgroud)

这里我们调用retryOperation,要求它每秒重试一次,最大重试次数= 5.

如果你想要没有承诺的更简单的东西,RxJs会帮助你:https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/retrywhen.md


hol*_*erd 14

提到了许多好的解决方案,现在有了异步/等待这些问题可以毫不费力地解决.

如果你不介意递归方法,那么这是我的解决方案.

function retry(fn, retries=3, err=null) {
  if (!retries) {
    return Promise.reject(err);
  }
  return fn().catch(err => {
      return retry(fn, (retries - 1), err);
    });
}
Run Code Online (Sandbox Code Playgroud)


jfr*_*d00 11

您可以将新承诺链接到先前的承诺,从而推迟其最终解决方案,直到您知道最终答案.如果下一个答案仍然未知,则将另一个答案链接到其上并继续将checkStatus()链接到自身,直到最终您知道答案并返回最终解决方案.这可能是这样的:

function delay(t) {
    return new Promise(function(resolve) {
        setTimeout(resolve, t);
    });
}

function checkStatus() {
    return work.requestStatus().then(function(result) {
        switch(result.status) {
            case "success":
                return result;      // resolve
            case "failure":
                throw result;       // reject
            case default:
            case "inProgress": //check every second
                return delay(1000).then(checkStatus);
        }
    });
}

work.create()
    .then(work.publish) //remote work submission
    .then(checkStatus)
    .then(function(){console.log("work published"})
    .catch(console.error);
Run Code Online (Sandbox Code Playgroud)

请注意,我也避免在您的switch陈述中创建承诺.由于你已经在一个.then()处理程序中,只是返回一个值就是解决,抛出一个异常就是拒绝,并且返回一个promise就是将一个新的promise链接到前一个.这涵盖了你的switch陈述的三个分支,而没有在那里创造新的承诺.为方便起见,我确实使用了delay()基于promise 的函数.

仅供参考,这假设work.requestStatus()不需要任何参数.如果它确实需要一些特定的参数,你可以在函数调用点传递它们.


对于循环等待完成的时间长度来实现某种超时值也是一个好主意,所以这永远不会继续下去.您可以像这样添加超时功能:

function delay(t) {
    return new Promise(function(resolve) {
        setTimeout(resolve, t);
    });
}

function checkStatus(timeout) {
    var start = Date.now();

    function check() {
        var now = Date.now();
        if (now - start > timeout) {
            return Promise.reject(new Error("checkStatus() timeout"));
        }
        return work.requestStatus().then(function(result) {
            switch(result.status) {
                case "success":
                    return result;      // resolve
                case "failure":
                    throw result;       // reject
                case default:
                case "inProgress": //check every second
                    return delay(1000).then(check);
            }
        });
    }
    return check;
}

work.create()
    .then(work.publish) //remote work submission
    .then(checkStatus(120 * 1000))
    .then(function(){console.log("work published"})
    .catch(console.error);
Run Code Online (Sandbox Code Playgroud)

我不确定你正在寻找什么样的"设计模式".由于您似乎反对外部声明的checkStatus()函数,这里是一个内联版本:

work.create()
    .then(work.publish) //remote work submission
    .then(work.requestStatus)
    .then(function() {
        // retry until done
        var timeout = 10 * 1000;
        var start = Date.now();

        function check() {
            var now = Date.now();
            if (now - start > timeout) {
                return Promise.reject(new Error("checkStatus() timeout"));
            }
            return work.requestStatus().then(function(result) {
                switch(result.status) {
                    case "success":
                        return result;      // resolve
                    case "failure":
                        throw result;       // reject
                    case default:
                    case "inProgress": //check every second
                        return delay(1000).then(check);
                }
            });
        }
        return check();
    }).then(function(){console.log("work published"})
    .catch(console.error);
Run Code Online (Sandbox Code Playgroud)

可以在许多情况下使用的更可重用的重试方案将定义一些可重用的外部代码,但您似乎反对这样,所以我没有制作该版本.


这是另一种根据您的请求使用.retryUntil()方法的方法Promise.prototype.如果您想调整此实现细节,您应该能够修改这种通用方法:

// fn returns a promise that must be fulfilled with an object
//    with a .status property that is "success" if done.  Any
//    other value for that status means to continue retrying
//  Rejecting the returned promise means to abort processing 
//        and propagate the rejection
// delay is the number of ms to delay before trying again
//     no delay before the first call to the callback
// tries is the max number of times to call the callback before rejecting
Promise.prototype.retryUntil = function(fn, delay, tries) {
    var numTries = 0;
    function check() {
        if (numTries >= tries) {
            throw new Error("retryUntil exceeded max tries");
        }
        ++numTries;
        return fn().then(function(result) {
            if (result.status === "success") {
                return result;          // resolve
            } else {
                return Promise.delay(delay).then(check);
            }
        });
    }
    return this.then(check);
}

if (!Promise.delay) {
    Promise.delay = function(t) {
        return new Promise(function(resolve) {
            setTimeout(resolve, t);
        });
    }
}


work.create()
    .then(work.publish) //remote work submission
    .retryUntil(function() {
        return work.requestStatus().then(function(result) {
            // make this promise reject for failure
            if (result.status === "failure") {
                throw result;
            }
            return result;
        })
    }, 2000, 10).then(function() {
        console.log("work published");
    }).catch(console.error);
Run Code Online (Sandbox Code Playgroud)

我仍然无法真正告诉你想要什么,或者所有这些方法都没有解决你的问题.由于您的方法似乎都是内联代码而不是使用可恢复的帮助程序,因此以下是其中之一:

work.create()
    .then(work.publish) //remote work submission
    .then(function() {
        var tries = 0, maxTries = 20;
        function next() {
            if (tries > maxTries) {
                throw new Error("Too many retries in work.requestStatus");
            }
            ++tries;
            return work.requestStatus().then(function(result) {
                switch(result.status) {
                    case "success":
                        return result;
                    case "failure":
                        // if it failed, make this promise reject
                        throw result;
                    default:
                        // for anything else, try again after short delay
                        // chain to the previous promise
                        return Promise.delay(2000).then(next);
                }

            });
        }
        return next();
    }).then(function(){
        console.log("work published")
    }).catch(console.error);
Run Code Online (Sandbox Code Playgroud)


Bry*_*ane 9

这是我的尝试。我试图从上述所有答案中获取我喜欢的内容。没有外部依赖。Typescript + 异步/等待 (ES2017)

export async function retryOperation<T>(
  operation: () => (Promise<T> | T), delay: number, times: number): Promise<T> {
    try {
      return await operation();
    } catch (ex) {
      if (times > 1) {
        await new Promise((resolve) => setTimeout(resolve, delay));
        return retryOperation(operation, delay, times - 1);
      } else {
        throw ex;
      }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

function doSomething() {
  return Promise.resolve('I did something!');
}

const retryDelay = 1000; // 1 second
const retryAttempts = 10;


retryOperation(doSomething, retryDelay, retryAttempts)
    .then((something) => console.log('I DID SOMETHING'))
    .catch((err) => console.error(err));
Run Code Online (Sandbox Code Playgroud)


i4h*_*i4h 7

以 holmberd 的解决方案为基础,使用一些更简洁的代码和延迟

// Retry code

const wait = ms => new Promise((resolve) => {
  setTimeout(() => resolve(), ms)
})


const retryWithDelay = async (
  fn, retries = 3, interval = 50,
  finalErr = Error('Retry failed')
) => {
  try {
    await fn()
  } catch (err) {
    if (retries <= 0) {
      return Promise.reject(finalErr);
    }
    await wait(interval)
    return retryWithDelay(fn, (retries - 1), interval, finalErr);
  }
}

// Test

const getTestFunc = () => {
  let callCounter = 0
  return async () => {
    callCounter += 1
    if (callCounter < 5) {
      throw new Error('Not yet')
    }
  }
}

const test = async () => {
  await retryWithDelay(getTestFunc(), 10)
  console.log('success')
  await retryWithDelay(getTestFunc(), 3)
  console.log('will fail before getting here')  
}


test().catch(console.error)
Run Code Online (Sandbox Code Playgroud)


Sei*_*vic 7

检查@jsier/retrier。经过测试、记录、轻量级、易于使用、没有外部依赖,并且已经在生产环境中使用了很长时间。

支持:

  • 首次尝试延迟
  • 尝试之间的延迟
  • 限制尝试次数
  • 如果满足某些条件(例如遇到特定错误),则回调以停止重试
  • 如果满足某些条件,回调以继续重试(例如解析的值不令人满意)

安装:

npm install @jsier/retrier
Run Code Online (Sandbox Code Playgroud)

用法:

import { Retrier } from '@jsier/retrier';

const options = { limit: 5, delay: 2000 };
const retrier = new Retrier(options);
retrier
  .resolve(attempt => new Promise((resolve, reject) => reject('Dummy reject!')))
  .then(
    result => console.log(result),
    error => console.error(error) // After 5 attempts logs: "Dummy reject!"
  );
Run Code Online (Sandbox Code Playgroud)

该包没有外部依赖项。


Red*_*ury 6

这是一个“指数退避”重试实现,使用async/await它可以包装任何承诺 API。

注意:出于演示原因,snippet 用 模拟了一个片状端点Math.random,因此请尝试几次以查看成功和失败的情况。

/**
 * Wrap a promise API with a function that will attempt the promise over and over again
 * with exponential backoff until it resolves or reaches the maximum number of retries.
 *   - First retry: 500 ms + <random> ms
 *   - Second retry: 1000 ms + <random> ms
 *   - Third retry: 2000 ms + <random> ms
 * and so forth until maximum retries are met, or the promise resolves.
 */
const withRetries = ({ attempt, maxRetries }) => async (...args) => {
  const slotTime = 500;
  let retryCount = 0;
  do {
    try {
      console.log('Attempting...', Date.now());
      return await attempt(...args);
    } catch (error) {
      const isLastAttempt = retryCount === maxRetries;
      if (isLastAttempt) {
        // Stack Overflow console doesn't show unhandled
        // promise rejections so lets log the error.
        console.error(error);
        return Promise.reject(error);
      }
    }
    const randomTime = Math.floor(Math.random() * slotTime);
    const delay = 2 ** retryCount * slotTime + randomTime;
    // Wait for the exponentially increasing delay period before retrying again.
    await new Promise(resolve => setTimeout(resolve, delay));
  } while (retryCount++ < maxRetries);
}

const fakeAPI = (arg1, arg2) => Math.random() < 0.25 ? Promise.resolve(arg1) : Promise.reject(new Error(arg2))
const fakeAPIWithRetries = withRetries({ attempt: fakeAPI, maxRetries: 3 });
fakeAPIWithRetries('arg1', 'arg2').then(results => console.log(results))
Run Code Online (Sandbox Code Playgroud)


vli*_*o20 5

如果您的代码放在一个类中,您可以为此使用装饰器。您在utils-decorators ( npm install --save utils-decorators) 库中有这样的装饰

import {retry} from 'utils-decorators';

class SomeService {

   @retry(3)
   doSomeAsync(): Promise<any> {
    ....
   }
}
Run Code Online (Sandbox Code Playgroud)

或者您可以使用包装函数:

import {retryfy} from 'utils-decorators';

const withRetry = retryfy(originalFunc, 3);
Run Code Online (Sandbox Code Playgroud)

注意:这个库是可摇树的,所以你不会为这个库中其他可用的装饰器支付额外的字节。

https://github.com/vlio20/utils-decorators#retry-method