我正在尝试在 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
。任何帮助将不胜感激!
非常有趣的问题。问题是异常来自
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)
}
})()
只需在
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
}
}
}
如果我们忽略这里的生成器,下面是 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();
}
您只需向它传递一个创建异步操作的函数,加上一些可选参数,就这样了。它比迄今为止提出的解决方案更加通用和可重用。