首页 > 解决方案 > 如何实现 Promise 重试和撤消

问题描述

我很好奇应该如何实现 API 重试和超时。有时仅仅等待一个 api 调用然后捕获出现的任何错误是不够的。如果我需要发出一连串异步请求,如下所示:

await client
  .callA()
  .then(async () => await callB())
  .then(async () => await callC())
  .catch(err => console.error(err));

如果其中一个承诺在中链失败,我想在几秒钟后再次尝试请求,直到尝试用完。

这是我尝试制作重试包装器。

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 -> callB -> callC

callA()成功,但callB()失败,我希望包装器callB()每隔一段时间重试一次,而不是重新开始。然后:

  1. callB()最终在允许的尝试范围内成功,继续前进callC()
  2. callB()尝试用完,调用undoCallA()以恢复之前所做的更改。

重复上述直到链结束。

我想了解一下这是如何实现的,或者是否有一个库可以做类似的事情。谢谢!

标签: javascripttypescriptasynchronousasync-awaitpromise

解决方案


函数应该很简单,只做一件事。我会从一个通用的开始sleep-

const sleep = ms =>
  new Promise(r => setTimeout(r, ms))

使用简单的函数,我们可以构建更复杂的函数,比如timeout-

const timeout = (p, ms) =>
  Promise.race([ p, sleep(ms).then(_ => { throw Error("timeout") }) ])

现在假设我们有一个任务,myTask最多需要 4 秒才能运行。如果生成奇数则成功返回。否则它会拒绝,“X 不是奇数” ——

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

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会产生奇数。或者它可能会在发出最终错误之前用尽所有尝试 -

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循环 -

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工作,让我们编写一个更复杂的示例来重试多个任务 -

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]

展开下面的代码片段以在您的浏览器中验证结果 -

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返回偶数会重试 -

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如果需要,整个选择 -

retry(_ => timeout(pick3(), 10000))
  .then(JSON.stringify)
  .then(console.log, console.error)

这种以多种方式组合简单功能的能力使它们比一个试图独自完成所有事情的大型复杂功能更强大。

当然,这意味着我们可以retry直接应用于您问题中的示例代码 -

async function main () {
  await retry(callA, ...)
  await retry(callB, ...)
  await retry(callC, ...)
  return "done"
}

main().then(console.log, console.error)

您可以申请timeout个人电话 -

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-

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于整个过程 -

async function main () {
  await retry(callA, ...)
  await retry(callB, ...)
  await retry(callC, ...)
  return "done"
}

timeout(main(), 30000).then(console.log, console.error)

或任何其他符合您实际意图的组合!


推荐阅读