节点,等待并重试 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。因为每个请求都是一个接一个地执行的,所以你不太可能 运行 遇到速率限制问题,但如果你这样做,添加一些自定义延迟就很容易了。