Promise 重试设计模式

IT技术 javascript node.js promise
2021-01-12 12:05:56

编辑

  1. 不断重试直到Promise解决的模式(使用delaymaxRetries)。
  2. 不断重试直到条件满足结果(使用delaymaxRetries)的模式。
  3. 具有无限重试的内存高效动态模式(delay提供)。

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

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

利用

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

#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);
6个回答

有点不同的东西......

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

这种方法是:

  • 只有在指定的最大尝试次数下才有可能。(链条必须是有限长度的),
  • 仅建议使用较低的最大值。(Promise 链消耗的内存大致与其长度成正比)。

否则,请使用递归解决方案。

首先,要用作.catch()回调的实用程序函数

var t = 500;

function rejectDelay(reason) {
    return new Promise(function(resolve, reject) {
        setTimeout(reject.bind(null, reason), t); 
    });
}

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

1. 重试直到Promise解决,延迟

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

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

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

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

添加进一步的测试也很简单。只需将一串 thens 夹在两个扣子之间即可。

p = p.catch(attempt).then(test1).then(test2).then(test3).catch(rejectDelay);

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


所有版本都被设计为attempt一个返回Promise的异步函数。也可以想象它返回一个值,在这种情况下,链将沿着它的成功路径到达 next/terminal .then()

我已将 #3 编辑为无限重试,并且我希望有一些内存高效的动态连接/链接。
2021-03-12 12:05:56
@ user2727195 - 我也不确定该test()函数如何传达回重试失败或失败并返回错误并中止所有重试之间的差异。由于很多解决方案都具有这种结构p.catch(attempt).catch(rejectDelay);,因此无法拒绝attempt()中止进一步处理。这对于现实世界的情况似乎过于简单。通常有表明需要重试的失败和表明不应进行进一步重试的失败,实际上在 OP 的代码中似乎就是这种情况。
2021-03-17 12:05:56
Promise chains consume memory roughly proportional to their length 但他们会在结算结束时被释放吗?
2021-03-27 12:05:56
整个解决方案对我来说似乎很奇怪。因为它可能会重试最多 N 次,所以它会预先创建 N 个对象以备不时之需。如果实际操作在第一次尝试中成功,那么您不仅不必要地创建了 N-1 个对象,而且必须随后处理它们。如果 N 不是一个小数,它只是在概念上看起来效率低下,有时实际上效率低下。它也不能像递归链接解决方案那样武断地决定重试多少次。例如,它不知道如何实现“自动重试最多 2 分钟”。
2021-03-28 12:05:56
当您说,only possible with a specified maximum number of attempts我可以恢复到setInterval基于无限的方法并在该setInterval方法中连接Promise时,如果您不介意的话,很高兴看到您的无限尝试示例。当我知道有关结果是否会被使用resolvereject但这过程(发布)需要在基于文件的大小倍太多时间。
2021-03-29 12:05:56

2. 不断重试直到结果满足条件的模式(带延迟和最大重试次数)

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

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

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

这就是你怎么称呼它的,假设func有时成功有时失败,总是返回一个我们可以记录的字符串:

retryOperation(func, 1000, 5)
  .then(console.log)
  .catch(console.log);

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

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

我希望第三个参数是时间或重试.. 尝试执行 retryOperation(func, 1000, 1) - func 不会在失败时执行(重试)。调用 retryOperation(func, 1000, 0) 并期望该操作根本不会被调用是否有意义?- “0”参数应该意味着:不重试 - 不是 0 次操作调用。
2021-03-17 12:05:56
实际上,retryOperation(func, 1000, 0) 和 retryOperation(func, 1000, 1) 具有相同的效果 - 不重试
2021-03-31 12:05:56
我喜欢这种方法,但是在检查剩余的重试时间时存在一个错误。应该是 if (times > 0) {...
2021-04-10 12:05:56
我想这取决于您如何解释要重试操作的“时间”。但是如果考虑到该操作在catch之前已经运行过一次,则是正确的。
2021-04-11 12:05:56

提到了许多好的解决方案,现在使用 async/await 可以轻松解决这些问题。

如果您不介意递归方法,那么这就是我的解决方案。

function retry(fn, retries=3, err=null) {
  if (!retries) {
    return Promise.reject(err);
  }
  return fn().catch(err => {
      return retry(fn, (retries - 1), err);
    });
}
@DavidLemon 是的catch(),您很可能会retry()为您提供最新的重试错误以及所有重试失败的信息。
2021-03-18 12:05:56
我喜欢这种经得起时间考验的简单方法。我只会添加一个错误处理程序。
2021-03-20 12:05:56
这非常有帮助。我想一直运行fn直到获得所需的输出,或者直到重试次数用完,所以我.then().catch(). 在我的用例中,.then()如果需要,它会进行递归调用,并.catch()拒绝错误。
2021-04-08 12:05:56

您可以将一个新的Promise链接到前一个Promise上,从而延迟其最终解决方案,直到您知道最终答案。如果仍然不知道下一个答案,则在其上链接另一个Promise并继续将 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);

请注意,我还避免在您的switch声明周围创建Promise由于您已经在.then()处理程序中,因此仅返回一个值是解决方案,抛出异常是拒绝,而返回Promise是将新Promise链接到前一个Promise。这涵盖了您switch声明的三个分支,而无需在其中创建新的Promise。为方便起见,我确实使用了一个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);

我不确定您正在寻找什么“设计模式”。由于您似乎反对外部声明的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);

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


这是另一种.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);

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

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);
@ user2727195 - 之前的实现不起作用 - 这就是我改变它的原因。在找到结果之前它没有循环。它只是再调用work.requestStatus()一次,无法循环。我相信这符合您的问题的要求。如果您正在寻找超出此范围的内容,请编辑您的问题以指定您正在寻找的其他内容,并给我留言告诉我您已经这样做了。
2021-03-16 12:05:56
@ user2727195 - 这可能会变得更通用。请编辑您的问题以准确描述您正在寻找的标准。您含糊地提到了“设计模式”,但对于实际满足您的要求的内容并不是很具体。
2021-03-31 12:05:56
我喜欢以前的实现,我也以某种方式解决了它,但不喜欢它,与您的情况相同,我喜欢您删除的以前的实现,我喜欢它,因为它是 then 处理程序中的内联实现,目的是想出一个优雅的设计模式,它可以与 then 处理程序一起使用,而不需要外部函数,而不仅仅是解决问题,我们可以重新工作吗?
2021-04-09 12:05:56
@ user2727195 -checkStatus()如果这让您感到困扰,可以在没有单独命名的函数的情况下内联完成实现,但我认为这是一个更清晰的实现,可以像我一样打破它,因为它使.then().then().then()链更容易遵循并查看到底是什么正在那里进行。此外,我需要为超时管理创建一个闭包,这是一种方便的方法,无需在顶级范围级别引入任何变量。
2021-04-09 12:05:56
@ user2727195 - 如果没有一些可以重复调用的函数,你就不能无限地异步重复代码。这就是为什么check()定义为函数的原因这就是您制作可以异步重复调用的代码体的方式。
2021-04-10 12:05:56

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

支持:

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

安装:

npm install @jsier/retrier

用法:

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

该包没有外部依赖项。