如何实现 Promise 重试和撤消 [英] How to implement a Promise retry and undo

查看:67
本文介绍了如何实现 Promise 重试和撤消的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我很好奇应该如何实现 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:

  1. callB() 最终在允许的尝试范围内成功,转到 callC().
  2. callB() 尝试次数用完,调用 undoCallA() 以恢复之前所做的更改.
  1. callB() eventually succeeds within the allowed attempts, move onto callC().
  2. callB() runs out of attempts, call undoCallA() 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
}

现在假设我们要运行 myTasktimeout 为两 (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)

而且由于timeoutretry是解耦的,我们可以实现不同的程序语义.相比之下,以下示例不会使单个任务超时,而是会在 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆