节点,等待并重试 api 个失败的调用
Node, wait and retry api calls that fail
所以我 fetch
来自 api 的具有速率限制的 url 数组,目前我通过为每个调用添加超时来处理这个问题,如下所示:
const calls = urls.map((url, i) =>
new Promise(resolve => setTimeout(resolve, 250 * i))
.then(() => fetch(url)
)
);
const data = await Promise.all(calls);
每次调用之间强制等待 250 毫秒。这样可以确保永远不会超过速率限制。
事实是,这并不是真正必要的。我尝试过 0 毫秒的等待时间,大多数情况下我必须在 api 开始到 return:
之前重复重新加载页面四到五次
{ error: { status: 429, message: 'API rate limit exceeded' } }
大多数情况下,您只需等待一秒钟左右即可安全地重新加载页面并获取所有数据。
一个更合理的方法是收集 return 429 的呼叫(如果他们这样做),等待一定的时间然后重试(也许重做一定次数) .
问题,我对如何实现这一点感到有点困惑?
编辑:
刚回到家,会仔细查看答案,但似乎做出了一个我认为没有必要的假设:调用不必是 顺序的 ,它们可以被解雇(和 returned)以任何顺序。
您想要的术语是指数退避。您可以修改您的代码,使其在特定的失败条件下继续尝试:
const max_wait = 2000;
async function wait(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
const calls = urls.map(async (url) => {
let retry = 0, result;
do {
if (retry !== 0) { await wait(Math.pow(2, retry); }
result = await fetch(url);
retry++;
} while(result.status !== 429 || (Math.pow(2, retry) > max_wait))
return result;
}
或者您可以尝试使用库来为您处理退避 https://github.com/MathieuTurcotte/node-backoff
这里是一个示例,通过设置以毫秒表示的 延迟 并接受第三个,可以顺序 处理承诺数组回调决定是否请求重试.
在下面的代码中,一些示例请求被模拟为:
- 测试成功响应。
- 测试错误响应。如果错误响应包含错误代码且错误代码为 403,则返回
true
并在接下来的 运行 中重试调用(延迟 x 毫秒)。
- 测试没有错误代码的错误响应。
下面有一个全局计数器,在 N 次尝试后放弃承诺(在下面的示例 5 中),所有这些都在这段代码中处理:
const result = await resolveSequencially(promiseTests, 250, (err) => {
return ++errorCount, !!(err && err.error && err.error.status === 403 && errorCount <= 5);
});
首先增加错误计数,如果定义错误,则 returns 为真,有错误 属性,其状态为 403。
当然,该示例只是为了测试,但我认为您正在寻找可以让您更巧妙地控制 promise 循环周期的东西,因此这里有一个解决方案可以做到这一点。
我会在下面添加一些评论,您可以运行下面的测试直接在控制台中检查发生了什么。
// Nothing that relevant, this one is just for testing purposes!
let errorCount = 0;
// Declare the function.
const resolveSequencially = (promises, delay, onFailed, onFinished) => {
// store the results.
const results = [];
// Define a self invoking recursiveHandle function.
(recursiveHandle = (current, max) => { // current is the index of the currently looped promise, max is the maximum needed.
console.log('recursiveHandle invoked, current is, ', current ,'max is', max);
if (current === max) onFinished(results); // <-- if all the promises have been looped, resolve.
else {
// Define a method to handle the promise.
let handlePromise = () => {
console.log('about to handle promise');
const p = promises[current];
p.then((success) => {
console.log('success invoked!');
results.push(success);
// if it's successfull, push the result and invoke the next element.
recursiveHandle(current + 1, max);
}).catch((err) => {
console.log('An error was catched. Invoking callback to check whether I should retry! Error was: ', err);
// otherwise, invoke the onFailed callback.
const retry = onFailed(err);
// if retry is true, invoke again the recursive function with the same indexes.
console.log('retry is', retry);
if (retry) recursiveHandle(current, max);
else recursiveHandle(current + 1, max); // <-- otherwise, procede regularly.
});
};
if (current !== 0) setTimeout(() => { handlePromise() }, delay); // <-- if it's not the first element, invoke the promise after the desired delay.
else handlePromise(); // otherwise, invoke immediately.
}
})(0, promises.length); // Invoke the IIFE with a initial index 0, and a maximum index which is the length of the promise array.
}
const promiseTests = [
Promise.resolve(true),
Promise.reject({
error: {
status: 403
}
}),
Promise.resolve(true),
Promise.reject(null)
];
const test = () => {
console.log('about to invoke resolveSequencially');
resolveSequencially(promiseTests, 250, (err) => {
return ++errorCount, !!(err && err.error && err.error.status === 403 && errorCount <= 5);
}, (done) => {
console.log('finished! results are:', done);
});
};
test();
如果我对问题的理解正确,您将尝试:
a) 按顺序执行 fetch()
调用(可能有可选的延迟)
b) 使用退避延迟重试失败的请求
正如您可能发现的那样,.map()
对 a) 并没有真正的帮助,因为它在迭代时不等待任何异步内容(这就是为什么您使用 [=13= 创建越来越大的超时时间的原因) ]).
我个人认为使用 for of
循环来保持顺序是最简单的,因为这将与 async/await
:
一起很好地工作
const fetchQueue = async (urls, delay = 0, retries = 0, maxRetries = 3) => {
const wait = (timeout = 0) => {
if (timeout) { console.log(`Waiting for ${timeout}`); }
return new Promise(resolve => {
setTimeout(resolve, timeout);
});
};
for (url of urls) {
try {
await wait(retries ? retries * Math.max(delay, 1000) : delay);
let response = await fetch(url);
let data = await (
response.headers.get('content-type').includes('json')
? response.json()
: response.text()
);
response = {
headers: [...response.headers].reduce((acc, header) => {
return {...acc, [header[0]]: header[1]};
}, {}),
status: response.status,
data: data,
};
// in reality, only do that for errors
// that make sense to retry
if ([404, 429].includes(response.status)) {
throw new Error(`Status Code ${response.status}`);
}
console.log(response.data);
} catch(err) {
console.log('Error:', err.message);
if (retries < maxRetries) {
console.log(`Retry #${retries+1} ${url}`);
await fetchQueue([url], delay, retries+1, maxRetries);
} else {
console.log(`Max retries reached for ${url}`);
}
}
}
};
// populate some real URLs urls to fetch
// index 0 will generate an inexistent URL to test error behaviour
const urls = new Array(101).fill(null).map((x, i) => `https://jsonplaceholder.typicode.com/todos/${i}`);
// fetch urls one after another (sequentially)
// and delay each request by 250ms
fetchQueue(urls, 250);
如果请求失败(例如,您收到数组中指定的带有错误状态代码的错误之一),上述函数将最多重试 3 次(默认情况下),退避延迟会增加一个每次重试第二个。
如您所写,请求之间的延迟可能不是必需的,因此您可以只删除函数调用中的 250
。因为每个请求都是一个接一个地执行的,所以你不太可能 运行 遇到速率限制问题,但如果你这样做,添加一些自定义延迟就很容易了。
所以我 fetch
来自 api 的具有速率限制的 url 数组,目前我通过为每个调用添加超时来处理这个问题,如下所示:
const calls = urls.map((url, i) =>
new Promise(resolve => setTimeout(resolve, 250 * i))
.then(() => fetch(url)
)
);
const data = await Promise.all(calls);
每次调用之间强制等待 250 毫秒。这样可以确保永远不会超过速率限制。
事实是,这并不是真正必要的。我尝试过 0 毫秒的等待时间,大多数情况下我必须在 api 开始到 return:
之前重复重新加载页面四到五次{ error: { status: 429, message: 'API rate limit exceeded' } }
大多数情况下,您只需等待一秒钟左右即可安全地重新加载页面并获取所有数据。
一个更合理的方法是收集 return 429 的呼叫(如果他们这样做),等待一定的时间然后重试(也许重做一定次数) .
问题,我对如何实现这一点感到有点困惑?
编辑:
刚回到家,会仔细查看答案,但似乎做出了一个我认为没有必要的假设:调用不必是 顺序的 ,它们可以被解雇(和 returned)以任何顺序。
您想要的术语是指数退避。您可以修改您的代码,使其在特定的失败条件下继续尝试:
const max_wait = 2000;
async function wait(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
const calls = urls.map(async (url) => {
let retry = 0, result;
do {
if (retry !== 0) { await wait(Math.pow(2, retry); }
result = await fetch(url);
retry++;
} while(result.status !== 429 || (Math.pow(2, retry) > max_wait))
return result;
}
或者您可以尝试使用库来为您处理退避 https://github.com/MathieuTurcotte/node-backoff
这里是一个示例,通过设置以毫秒表示的 延迟 并接受第三个,可以顺序 处理承诺数组回调决定是否请求重试.
在下面的代码中,一些示例请求被模拟为:
- 测试成功响应。
- 测试错误响应。如果错误响应包含错误代码且错误代码为 403,则返回
true
并在接下来的 运行 中重试调用(延迟 x 毫秒)。 - 测试没有错误代码的错误响应。
下面有一个全局计数器,在 N 次尝试后放弃承诺(在下面的示例 5 中),所有这些都在这段代码中处理:
const result = await resolveSequencially(promiseTests, 250, (err) => {
return ++errorCount, !!(err && err.error && err.error.status === 403 && errorCount <= 5);
});
首先增加错误计数,如果定义错误,则 returns 为真,有错误 属性,其状态为 403。 当然,该示例只是为了测试,但我认为您正在寻找可以让您更巧妙地控制 promise 循环周期的东西,因此这里有一个解决方案可以做到这一点。
我会在下面添加一些评论,您可以运行下面的测试直接在控制台中检查发生了什么。
// Nothing that relevant, this one is just for testing purposes!
let errorCount = 0;
// Declare the function.
const resolveSequencially = (promises, delay, onFailed, onFinished) => {
// store the results.
const results = [];
// Define a self invoking recursiveHandle function.
(recursiveHandle = (current, max) => { // current is the index of the currently looped promise, max is the maximum needed.
console.log('recursiveHandle invoked, current is, ', current ,'max is', max);
if (current === max) onFinished(results); // <-- if all the promises have been looped, resolve.
else {
// Define a method to handle the promise.
let handlePromise = () => {
console.log('about to handle promise');
const p = promises[current];
p.then((success) => {
console.log('success invoked!');
results.push(success);
// if it's successfull, push the result and invoke the next element.
recursiveHandle(current + 1, max);
}).catch((err) => {
console.log('An error was catched. Invoking callback to check whether I should retry! Error was: ', err);
// otherwise, invoke the onFailed callback.
const retry = onFailed(err);
// if retry is true, invoke again the recursive function with the same indexes.
console.log('retry is', retry);
if (retry) recursiveHandle(current, max);
else recursiveHandle(current + 1, max); // <-- otherwise, procede regularly.
});
};
if (current !== 0) setTimeout(() => { handlePromise() }, delay); // <-- if it's not the first element, invoke the promise after the desired delay.
else handlePromise(); // otherwise, invoke immediately.
}
})(0, promises.length); // Invoke the IIFE with a initial index 0, and a maximum index which is the length of the promise array.
}
const promiseTests = [
Promise.resolve(true),
Promise.reject({
error: {
status: 403
}
}),
Promise.resolve(true),
Promise.reject(null)
];
const test = () => {
console.log('about to invoke resolveSequencially');
resolveSequencially(promiseTests, 250, (err) => {
return ++errorCount, !!(err && err.error && err.error.status === 403 && errorCount <= 5);
}, (done) => {
console.log('finished! results are:', done);
});
};
test();
如果我对问题的理解正确,您将尝试:
a) 按顺序执行 fetch()
调用(可能有可选的延迟)
b) 使用退避延迟重试失败的请求
正如您可能发现的那样,.map()
对 a) 并没有真正的帮助,因为它在迭代时不等待任何异步内容(这就是为什么您使用 [=13= 创建越来越大的超时时间的原因) ]).
我个人认为使用 for of
循环来保持顺序是最简单的,因为这将与 async/await
:
const fetchQueue = async (urls, delay = 0, retries = 0, maxRetries = 3) => {
const wait = (timeout = 0) => {
if (timeout) { console.log(`Waiting for ${timeout}`); }
return new Promise(resolve => {
setTimeout(resolve, timeout);
});
};
for (url of urls) {
try {
await wait(retries ? retries * Math.max(delay, 1000) : delay);
let response = await fetch(url);
let data = await (
response.headers.get('content-type').includes('json')
? response.json()
: response.text()
);
response = {
headers: [...response.headers].reduce((acc, header) => {
return {...acc, [header[0]]: header[1]};
}, {}),
status: response.status,
data: data,
};
// in reality, only do that for errors
// that make sense to retry
if ([404, 429].includes(response.status)) {
throw new Error(`Status Code ${response.status}`);
}
console.log(response.data);
} catch(err) {
console.log('Error:', err.message);
if (retries < maxRetries) {
console.log(`Retry #${retries+1} ${url}`);
await fetchQueue([url], delay, retries+1, maxRetries);
} else {
console.log(`Max retries reached for ${url}`);
}
}
}
};
// populate some real URLs urls to fetch
// index 0 will generate an inexistent URL to test error behaviour
const urls = new Array(101).fill(null).map((x, i) => `https://jsonplaceholder.typicode.com/todos/${i}`);
// fetch urls one after another (sequentially)
// and delay each request by 250ms
fetchQueue(urls, 250);
如果请求失败(例如,您收到数组中指定的带有错误状态代码的错误之一),上述函数将最多重试 3 次(默认情况下),退避延迟会增加一个每次重试第二个。
如您所写,请求之间的延迟可能不是必需的,因此您可以只删除函数调用中的 250
。因为每个请求都是一个接一个地执行的,所以你不太可能 运行 遇到速率限制问题,但如果你这样做,添加一些自定义延迟就很容易了。