Javascript Promise 重试设计模式
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/38213668/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Promise Retry Design Patterns
提问by user2727195
Edit
编辑
- Pattern that keep on retrying until the promise resolves (with delay and maxRetries).
- Pattern that keeps on retrying until the condition meets on the result (with delay and maxRetries).
- A memory efficient dynamic Pattern with unlimited retries (delay provided).
- 继续重试直到承诺解决的模式(使用延迟和 maxRetries)。
- 不断重试直到条件满足结果的模式(使用延迟和 maxRetries)。
- 具有无限重试(提供延迟)的内存高效动态模式。
Code for #1.Keeps on retrying until promise resolves (any improvements community for the language etc?)
#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();
});
};
Use
用
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);
Code for #2keep on retrying until a condition meets on the then
result in a reusable way (condition is what will vary).
#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);
采纳答案by Roamer-1888
Something a bit different ...
有点不同的东西......
Async retries can be achieved by building a .catch()
chain, as opposed to the more usual .then()
chain.
异步重试可以通过构建.catch()
链来实现,而不是更常见的.then()
链。
This approach is :
这种方法是:
- only possible with a specified maximum number of attempts. (The chain must be of finite length),
- only advisable with a low maximum. (Promise chains consume memory roughly proportional to their length).
- 只有在指定的最大尝试次数下才有可能。(链必须是有限长度的),
- 仅建议使用较低的最大值。(Promise 链消耗的内存大致与其长度成正比)。
Otherwise, use a recursive solution.
否则,请使用递归解决方案。
First, a utility function to be used as a .catch()
callback.
首先,要用作.catch()
回调的实用程序函数。
var t = 500;
function rejectDelay(reason) {
return new Promise(function(resolve, reject) {
setTimeout(reject.bind(null, reason), t);
});
}
Now you can build .catch chains very concisely :
现在您可以非常简洁地构建 .catch 链:
1. Retry until the promise resolves, with delay
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);
DEMO: https://jsfiddle.net/duL0qjqe/
演示:https: //jsfiddle.net/duL0qjqe/
2. Retry until result meets some condition, without delay
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);
DEMO: https://jsfiddle.net/duL0qjqe/1/
演示:https: //jsfiddle.net/duL0qjqe/1/
3. Retry until result meets some condition, with delay
3.重试直到结果满足某个条件,延迟
Having got your mind round (1) and (2), a combined test+delay is equally trivial.
考虑好 (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()
can be synchronous or asynchronous.
test()
可以是同步的或异步的。
It would also be trivial to add further tests. Simply sandwich a chain of thens between the two catches.
添加进一步的测试也很简单。只需将一串 then 夹在两个扣子之间即可。
p = p.catch(attempt).then(test1).then(test2).then(test3).catch(rejectDelay);
DEMO: https://jsfiddle.net/duL0qjqe/3/
演示:https: //jsfiddle.net/duL0qjqe/3/
All versions are designed for attempt
to be a promise-returning async function. It could also conceivably return a value, in which case the chain would follow its success path to the next/terminal .then()
.
所有版本都被设计为attempt
一个返回承诺的异步函数。也可以想象它返回一个值,在这种情况下,链将沿着它的成功路径到达 next/terminal .then()
。
回答by Yair Kukielka
2. Pattern that keeps on retrying until the condition meets on the result(with delay and maxRetries)
2. 不断重试直到结果满足条件的模式(带有延迟和maxRetries)
This is an nice way to do this with native promises in a recursive way:
这是一种以递归方式使用本机承诺执行此操作的好方法:
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);
});
});
This is how you call it, assuming that func
sometimes succeeds and sometimes fails, always returning a string that we can log:
这就是你怎么称呼它的,假设func
有时成功有时失败,总是返回一个我们可以记录的字符串:
retryOperation(func, 1000, 5)
.then(console.log)
.catch(console.log);
Here we're calling retryOperation asking it to retry every second and with max retries = 5.
在这里,我们调用 retryOperation 要求它每秒重试一次,最大重试次数 = 5。
If you want something simpler without promises, RxJs would help with that: https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/retrywhen.md
如果你想要更简单的没有承诺的东西,RxJs 会帮助你:https: //github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/retrywhen.md
回答by holmberd
There are many good solutions mentioned and now with async/await these problems can be solved without much effort.
提到了许多好的解决方案,现在使用 async/await 可以轻松解决这些问题。
If you don't mind a recursive approach then this is my solution.
如果您不介意递归方法,那么这就是我的解决方案。
function retry(fn, retries=3, err=null) {
if (!retries) {
return Promise.reject(err);
}
return fn().catch(err => {
return retry(fn, (retries - 1), err);
});
}
回答by jfriend00
You can chain a new promise onto the prior one, thus delaying its eventual resolution until you know the final answer. If the next answer still isn't known, then chain another promise on it and keep chaining checkStatus() to itself until eventually you know the answer and can return the final resolution. That could work like this:
您可以将新承诺链接到前一个承诺上,从而延迟其最终解决方案,直到您知道最终答案。如果仍然不知道下一个答案,则在其上链接另一个承诺并继续将 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);
Note, I also avoided creating the promise around your switch
statement. Since you're already in a .then()
handler, just returning a value is resolve, throwing an exception is reject and returning a promise is chaining a new promise onto the prior one. That covers the three branches of your switch
statement without creating a new promise in there. For convenience, I do use a delay()
function that is promise based.
请注意,我还避免在您的switch
声明周围创建承诺。由于您已经在.then()
处理程序中,因此只返回一个值是解决,抛出异常是拒绝,返回承诺是将新承诺链接到前一个承诺。这涵盖了您switch
声明的三个分支,而无需在其中创建新的承诺。为方便起见,我确实使用了一个delay()
基于 Promise的函数。
FYI, this assumes the work.requestStatus()
doesn't need any arguments. If it does need some specific arguments, you can pass those at the point of the function call.
仅供参考,这假设work.requestStatus()
不需要任何参数。如果它确实需要一些特定的参数,您可以在函数调用时传递这些参数。
It might also be a good idea to implement some sort of timeout value for how long you will loop waiting for completion so this never goes on forever. You could add the timeout functionality like this:
为循环等待完成的时间实现某种超时值也可能是一个好主意,这样就不会永远持续下去。您可以像这样添加超时功能:
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);
I'm not sure exactly what "design pattern" you're looking for. Since you seem to object to the externally declared checkStatus()
function, here's an inline version:
我不确定您正在寻找什么“设计模式”。由于您似乎反对外部声明的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);
A more reusable retry scheme that could be used in many circumstances would define some reusable external code, but you seem to object to that so I haven't made that version.
可以在许多情况下使用的更可重用的重试方案将定义一些可重用的外部代码,但您似乎反对这样做,所以我没有制作那个版本。
Here's one other approach that uses a .retryUntil()
method on the Promise.prototype
per your request. If you want to tweak implementation details of this, you should be able to modify this general approach:
这是另一种.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);
I still can't really tell what you want or what about all these approaches is not solving your issue. Since your approaches seem to all be all inline code and not using a resuable helper, here's one of those:
我仍然无法真正说出您想要什么,或者所有这些方法都没有解决您的问题。由于您的方法似乎都是内联代码而不是使用可重用的帮助程序,因此这里是其中之一:
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);
回答by Red Mercury
Here is a exponential backoff retry implementation using async/await that can wrap any promise API. It simulates a flaky endpoint with math random so try a few times to see both success and failure cases.
这是一个使用 async/await 的指数退避重试实现,可以包装任何承诺 API。它使用数学随机模拟一个片状端点,因此请尝试几次以查看成功和失败的情况。
/**
* 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))
回答by Seid Mehmedovic
Check @jsier/retrier. Tested, documented, lightweight, easy-to-use, without external dependencies and already in production for quite some time now.
检查@jsier/retrier。经过测试、记录、轻量级、易于使用、没有外部依赖,并且已经在生产环境中使用了很长时间。
Supports:
支持:
- First attempt delay
- Delay between attempts
- Limiting number of attempts
- Callback to stop retrying if some condition is met (e.g. specific error is encountered)
- Callback to keep retrying if some condition is met (e.g. resolved value is unsatisfactory)
- 首次尝试延迟
- 尝试之间的延迟
- 限制尝试次数
- 如果满足某些条件(例如遇到特定错误),则回调以停止重试
- 如果满足某些条件,回调以继续重试(例如解析的值不令人满意)
Installation:
安装:
npm install @jsier/retrier --save
Usage:
用法:
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!"
);
The package has no external dependencies.
该包没有外部依赖项。
回答by vlio20
If your code is placed in a class you could use a decorator for that. You have such decorator in the utils-decorators(npm install --save utils-decorators
):
如果您的代码放在一个类中,您可以为此使用装饰器。您在utils-decorators( npm install --save utils-decorators
) 中有这样的装饰器:
import {retry} from 'utils-decorators';
class SomeService {
@retry(3)
doSomeAsync(): Promise<any> {
....
}
}
回答by Jeff Tian
async-retry.tsis trying to implement the pattern, I'm using it in production for some projects.
async-retry.ts正在尝试实现该模式,我在一些项目的生产中使用它。
Installation:
安装:
npm install async-retry.ts --save
npm install async-retry.ts --save
Usage:
用法:
import Action from 'async-retry.ts'
const action = async()=>{}
const handlers = [{
error: 'error1',
handler: async yourHandler1()=>{}
}, {
error: 'error2',
handler: async yourHandler2()=>{}
}]
await Action.retryAsync(action, 3, handlers)
This package is quite new but it is derived from a long lived package co-retry
which implemented the retry pattern
in generator function fashion.
这个包很新,但它源自一个长期存在的包co-retry
,它实现了retry pattern
生成器功能时尚。
回答by Guido Dizioli
There are plenty answers here, but after some research i decided to go with a recursive approach. Im leaving my solution here for any one interested
这里有很多答案,但经过一些研究,我决定采用递归方法。我把我的解决方案留在这里给任何感兴趣的人
function retry(fn, retriesLeft = 2, interval = 1000) {
return new Promise((resolve, reject) => {
fn()
.then(resolve)
.catch((error) => {
if (retriesLeft === 0) {
reject(error);
return;
}
setTimeout(() => {
console.log('retrying...')
retry(fn, retriesLeft - 1, interval).then(resolve).catch(reject);
}, interval);
});
});
}
Here is a stackblitz with a nice playground where you can get the feel on how it works. Just play around the intent variable to see the promise resolve/reject
这是一个带有漂亮操场的 stackblitz,您可以在其中感受它的工作原理。只需玩转意图变量即可查看承诺解决/拒绝
回答by Sameer Girkar
function TryToSuccess(fun, reties) {
let attempt = 0;
let doTry = (...args) => {
attempt++;
return fun(...args)
.catch((err) => {
console.log("fail ", attempt);
if(attempt <= reties){
return doTry(...args);
} else {
return Promise.reject(err);
}
});
}
return doTry;
}
function asyncFunction(){
return new Promise((resolve, reject) => {
setTimeout(() => {
(window.findResult === true) ? resolve("Done") : reject("fail");
}, 2000);
});
}
var cloneFunc = TryToSuccess(asyncFunction, 3);
cloneFunc()
.then(res => {
console.log("Got Success. ", res)
})
.catch(err => {
console.log("Rejected with err ", err);
});
setTimeout(() => {
window.findResult = true;
}, 4000);