在 for...of 循环中重试失败的 API 调用

问题描述 投票:0回答:3

我正在尝试在 Notion JS SDK 的

iteratePaginatedAPI
上构建一个包装器来处理错误。我对如何以一种可以实际重试的方式捕获 API 错误(又名重试失败的迭代)感到特别迷失。这是我的尝试:

async function* queryNotion(listFn, firstPageArgs) {
  try {
    for await (const result of iteratePaginatedAPI(listFn, firstPageArgs)) {
      yield* result
    }
  } catch (error) {
    if (error.code === APIErrorCode.RateLimited) {
      console.log('rate_limited');
      console.log(error);
      sleep(1);
      // How would I retry the last iteration?
    }
  }
}

来自 Ruby 世界,

retry
块中有一个
rescue
。任何帮助将不胜感激!

javascript async-await try-catch notion-js
3个回答
1
投票

非常有趣的问题。问题是异常来自

for await
本身,而不是来自它的主体,所以你无法在那里捕获它。当异常发生时,循环结束。

请注意,迭代器可能会在拒绝/异常之后完成,在这种情况下,除了开始一个新的迭代器之外,您无能为力。

也就是说,您随时可以自己调用

Iterator.next()
并手动处理结果。异步迭代器的
next()
调用将返回类似
{value: Promise<any>, done: boolean}
的对象,并且在循环中运行它时,您可以在
try..catch
中等待 Promise,并且仅在
done
变为 true 时退出循环:

async function* queryNotion(listFn, firstPageArgs) {
  const asyncGenerator = mockIteratePaginatedAPI(listFn, firstPageArgs)
  while (true) {
    const current = asyncGenerator.next()
    if (current.done) {
      break
    }
    try {
      yield* await current.value
    } catch (e) {
      console.log(`got exception: "${e}" - trying again`)
      continue
    }
  }
}

function* mockIteratePaginatedAPI(a, b) {
  for (let i = 0; i < 8; i++) {
    yield new Promise((resolve, reject) => setTimeout(() => [3, 5].includes(i) ? reject(`le error at ${i}`) : resolve([i]), 500))
  }
}

(async function() {
  for await (const n of queryNotion('foo', 'bar')) {
    console.log(n)
  }
})()

如果我们保留对生成器的引用,我们也可以将其放回

for async
。这可能更容易阅读,但是一个
for await ...of
会在提前退出循环时调用迭代器的
return()
,可能会完成它,在这种情况下,这个将不起作用

async function* queryNotion(listFn, firstPageArgs) {
  const asyncGenerator = mockIteratePaginatedAPI(listFn, firstPageArgs)
  while (true) {
    try {
      for await (const result of asyncGenerator) {
        yield* result
      }
      break
    } catch (e) {
      console.log('got exception:', e, 'trying again')
    }
  }
}

function* mockIteratePaginatedAPI(a, b) {
  for (let i = 0; i < 8; i++) {
    yield new Promise((resolve, reject) => setTimeout(() => [3, 5].includes(i) ? reject(`le error at ${i}`) : resolve([i]), 500))
  }
}

(async function () {
  for await (const n of queryNotion('foo', 'bar')) {
    console.log(n)
  }
})()


0
投票

只需在

continue
 中添加 
if

语句即可
async function* queryNotion(listFn, firstPageArgs) {
  try {
    for await (const result of iteratePaginatedAPI(listFn, firstPageArgs)) {
      yield* result
    }
  } catch (error) {
    if (error.code === APIErrorCode.RateLimited) {
      console.log('rate_limited');
      console.log(error);
      await sleep(1);
      continue; // retry the last iteration
    }
  }
}

0
投票

如果我们忽略这里的生成器,下面是 TypeScript 中可以在任何地方使用的通用重试例程:

type AsyncRetryOptions = {
    retries?: number, // number of retries (infinite by default)
    delay?: number, // delay between retries, in ms
    error?: (e: any) => void // error notification callback
};

function retryAsync<T>(func: () => Promise<T>, options?: AsyncRetryOptions): Promise<T> {
    let {retries = Number.POSITIVE_INFINITY, delay = -1, error} = options || {};
    const call: () => Promise<T> = () => func().catch(e => {
        const r = () => retries-- ? call() : Promise.reject(e);
        typeof error === 'function' && error(e);
        return delay >= 0 ? (new Promise(res => setTimeout(res, delay))).then(r) : r();
    });
    return call();
}

您只需向它传递一个创建异步操作的函数,加上一些可选参数,就这样了。它比迄今为止提出的解决方案更加通用和可重用。

© www.soinside.com 2019 - 2024. All rights reserved.