如何实现 Promise 重试和撤消 [英] How to implement a Promise retry and undo
问题描述
我很好奇应该如何实现 API 重试和超时.有时,仅仅等待 api 调用然后捕获出现的任何错误是不够的.如果我需要发出一系列异步请求,如下所示:
I'm curious on how API retry and timeouts should be implemented. Sometimes simply awaiting an api call then catching any error that come up isn't sufficient. If there's a chain of async requests I need to make, like so:
await client
.callA()
.then(async () => await callB())
.then(async () => await callC())
.catch(err => console.error(err));
如果其中一个承诺在链中失败,我想在几秒钟后再次尝试请求,直到尝试用完为止.
and if one of the promise fails midchain, I want to attempt to the request again after a few seconds until attempts run out.
这是我尝试制作重试包装器.
This is my attempt at making a retry wrapper.
async function retry (fn, undo, attempts, wait = 5000) {
await fn().catch(async (err) => {
console.error(err.message + `\n retrying in ${wait/1000} seconds...`);
if (attempts !== 0) {
// async timeout
await new Promise((resolve) => {
setTimeout(() => resolve(retry(fn, undo, attempts - 1)), wait);
})
} else {
await undo()
}
})
}
await retry(calls, undoCalls, 10)
callA ->呼叫B ->调用C
说 callA()
成功,但 callB()
失败,我希望包装器每隔一段时间重试 callB()
而不是重新开始.然后:
Say callA()
succeeds, but callB()
fails, I want the wrapper to retry callB()
at an interval instead of starting over again.
Then either:
callB()
最终在允许的尝试范围内成功,转到callC()
.callB()
尝试次数用完,调用undoCallA()
以恢复之前所做的更改.
callB()
eventually succeeds within the allowed attempts, move ontocallC()
.callB()
runs out of attempts, callundoCallA()
to revert the changes previously made.
重复上述直到链结束.
我想了解一下这是如何实现的,或者是否有一个库可以做类似的事情.谢谢!
I'd like some insight on how this is implemented or if there's a library that does something similar. Thanks!
推荐答案
函数应该很简单,只做一件事.我会从一个通用的 sleep
-
Functions should be simple and do just one thing. I would start with a generic sleep
-
const sleep = ms =>
new Promise(r => setTimeout(r, ms))
使用简单的函数,我们可以构建更复杂的函数,例如 timeout
-
Using simple functions we can build more sophisticated ones, like timeout
-
const timeout = (p, ms) =>
Promise.race([ p, sleep(ms).then(_ => { throw Error("timeout") }) ])
现在假设我们有一个任务,myTask
最多需要 4 秒才能运行.如果生成奇数则成功返回.否则它拒绝,X 不是奇数" -
Now let's say we have a task, myTask
that takes up to 4 seconds to run. It returns successfully if it generates an odd number. Otherwise it rejects, "X is not odd" -
async function myTask () {
await sleep(Math.random() * 4000)
const x = Math.floor(Math.random() * 100)
if (x % 2 == 0) throw Error(`${x} is not odd`)
return x
}
现在假设我们要运行 myTask
,timeout
为两 (2) 秒,retry
最多为三 (3)次 -
Now let's say we want to run myTask
with a timeout
of two (2) seconds and retry
a maximum of three (3) times -
retry(_ => timeout(myTask(), 2000), 3)
.then(console.log, console.error)
Error: 48 is not odd (retry 1/3)
Error: timeout (retry 2/3)
79
myTask
有可能在第一次尝试时产生奇数.或者它可能会在发出最终错误之前用尽所有尝试 -
It's possible myTask
could produce an odd number on the first attempt. Or it's possible that it could exhaust all attempts before emitting a final error -
Error: timeout (retry 1/3)
Error: timeout (retry 2/3)
Error: 34 is not odd (retry 3/3)
Error: timeout
Error: failed after 3 retries
现在我们实现retry
.我们可以使用一个简单的 for
循环 -
Now we implement retry
. We can use a simple for
loop -
async function retry (f, count = 5, ms = 1000) {
for (let attempt = 1; attempt <= count; attempt++) {
try {
return await f()
}
catch (err) {
if (attempt <= count) {
console.error(err.message, `(retry ${attempt}/${count})`)
await sleep(ms)
}
else {
console.error(err.message)
}
}
}
throw Error(`failed after ${count} retries`)
}
现在我们看到了 retry
是如何工作的,让我们编写一个更复杂的例子来重试多个任务 -
Now that we see how retry
works, let's write a more complex example that retries multiple tasks -
async function pick3 () {
const a = await retry(_ => timeout(myTask(), 3000))
console.log("first pick:", a)
const b = await retry(_ => timeout(myTask(), 3000))
console.log("second pick:", b)
const c = await retry(_ => timeout(myTask(), 3000))
console.log("third pick:", c)
return [a, b, c]
}
pick3()
.then(JSON.stringify)
.then(console.log, console.error)
Error: timeout (retry 1/5)
Error: timeout (retry 2/5)
first pick: 37
Error: 16 is not odd (retry 1/5)
second pick: 13
Error: 60 is not odd (retry 1/5)
Error: timeout (retry 2/5)
third pick: 15
[37,13,15]
展开下面的代码段以在浏览器中验证结果 -
Expand the snippet below to verify the result in your browser -
const sleep = ms =>
new Promise(r => setTimeout(r, ms))
const timeout = (p, ms) =>
Promise.race([ p, sleep(ms).then(_ => { throw Error("timeout") }) ])
async function retry (f, count = 5, ms = 1000) {
for (let attempt = 0; attempt <= count; attempt++) {
try {
return await f()
}
catch (err) {
if (attempt < count) {
console.error(err.message, `(retry ${attempt + 1}/${count})`)
await sleep(ms)
}
else {
console.error(err.message)
}
}
}
throw Error(`failed after ${count} retries`)
}
async function myTask () {
await sleep(Math.random() * 4000)
const x = Math.floor(Math.random() * 100)
if (x % 2 == 0) throw Error(`${x} is not odd`)
return x
}
async function pick3 () {
const a = await retry(_ => timeout(myTask(), 3000))
console.log("first", a)
const b = await retry(_ => timeout(myTask(), 3000))
console.log("second", b)
const c = await retry(_ => timeout(myTask(), 3000))
console.log("third", c)
return [a, b, c]
}
pick3()
.then(JSON.stringify)
.then(console.log, console.error)
而且由于timeout
与retry
是解耦的,我们可以实现不同的程序语义.相比之下,以下示例不会使单个任务超时,而是会在 myTask
返回偶数时重试 -
And because timeout
is decoupled from retry
, we can achieve different program semantics. By contrast, the following example not timeout individual tasks but will retry if myTask
returns an even number -
async function pick3 () {
const a = await retry(myTask)
const b = await retry(myTask)
const c = await retry(myTask)
return [a, b, c]
}
我们现在可以说 timeout
pick3
如果它花费的时间超过十 (10) 秒,并且 retry
如果它超过了整个选择-
And we could now say timeout
pick3
if it takes longer than ten (10) seconds, and retry
the entire pick if it does -
retry(_ => timeout(pick3(), 10000))
.then(JSON.stringify)
.then(console.log, console.error)
这种以多种方式组合简单函数的能力使它们比一个试图自行完成所有事情的大型复杂函数更强大.
This ability to combine simple functions in a variety of ways is what makes them more powerful than one big complex function that tries to do everything on its own.
当然这意味着我们可以将 retry
直接应用于您问题中的示例代码 -
Of course this means we can apply retry
directly to the example code in your question -
async function main () {
await retry(callA, ...)
await retry(callB, ...)
await retry(callC, ...)
return "done"
}
main().then(console.log, console.error)
您可以将 timeout
应用于单个调用 -
You can either apply timeout
to the individual calls -
async function main () {
await retry(_ => timeout(callA(), 3000), ...)
await retry(_ => timeout(callB(), 3000), ...)
await retry(_ => timeout(callC(), 3000), ...)
return "done"
}
main().then(console.log, console.error)
或将 timeout
应用到每个 retry
-
Or apply timeout
to each retry
-
async function main () {
await timeout(retry(callA, ...), 10000)
await timeout(retry(callB, ...), 10000)
await timeout(retry(callC, ...), 10000)
return "done"
}
main().then(console.log, console.error)
或者可能将 timeout
应用到整个过程 -
Or maybe apply timeout
to the entire process -
async function main () {
await retry(callA, ...)
await retry(callB, ...)
await retry(callC, ...)
return "done"
}
timeout(main(), 30000).then(console.log, console.error)
或任何其他符合您实际意图的组合!
Or any other combination that matches your actual intention!
这篇关于如何实现 Promise 重试和撤消的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!