ウェブサイト検索

javascript - Promise.allSettled() - 複数の非同期呼び出しのリトライ戦略


要約: 複数の非同期呼び出しに対して Promise.allSettled() を使用する際、未知の数のプロミス拒否を処理し、それらを X 回再試行する戦略や例を探しています。

この記事を読んだ後: https://www.coreycleary.me/better-handling-of-rejections-using-promise-allsettled この文章にとてもワクワクしました:

これは素晴らしいことです。なぜなら、ユーザーアカウント情報をまだ読み込むことができ、後でユーザーのアクティビティ取得を再試行できるからです。(再試行はこの記事の範囲外であり、そのための複数の戦略があります)

しかし、オンラインで調べたところ、この問題を直接扱った具体的な例や投稿はまったく見つかりませんでした。

こちらにいくつかのサンプルコードがあります:

  public async test() {
    try {
      console.log('trying..');
      const multiAsync = [this.somethingA(), this.somethingB(), this.somethingC()];
      const [a, b, c] = await Promise.allSettled(multiAsync);
    } catch (e) {
      console.log('catch');
      console.log(e);
    }
  }

では、上記のコードについて、AとCの両方が失敗したとしましょう。そして、それらをもう一度だけ再試行したいとします。a, b, cがあるものの、どれがfulfilled:trueでどれがそうでないかしかわかりません。しかし、どのがsomethingA()に対応し、どのがsomethingC()に対応するのかはわからないので、その2つの関数だけを再試行したいですし、somethingB()を2回呼び出すことは絶対に避けたいです。

何かアイデアはありますか?

回答 #1

Promiseはどこから来たのかの記録を保持しません:一度Promiseを手に入れると、その起源を何度もリトライする手段はありません。そのため、一般的なPromiseのリトライパターンでは、Promise自体ではなく、Promiseを返す関数("promise factory")を受け入れます。このように各個別のPromiseをallやallSettledを呼び出す前にリトライ関数でラップすることが最も実用的な解決策です。なぜなら、すぐに失敗するPromiseは、allSettledのようにリスト全体を待たずにすぐに再試行できるからです。

const multiAsync = [
  retry(() => this.somethingA(), 3),
  retry(() => this.somethingB(), 3),
  retry(() => this.somethingC(), 3),
];
const [a, b, c] = await Promise.allSettled(multiAsync);

// or

const [a, b, c] =
    await Promise.allSettled([/* functions */].map(x => retry(x, 3));

しかし、もし直接これを行うために Promise.allSettled の使い方を見たい場合、ここに1つ例があります。ここでの私の解決策はタイムアウトを考慮していませんが、Promise.race の実装を使ってグローバルに、または個別に追加することができます。

/**
 * Resolves a series of promise factories, retrying if needed.
 *
 * @param {number} maxTryCount How many retries to perform.
 * @param {Array<() => Promise<any>>} promiseFactories Functions
 *     that return promises. These must be functions to enable retrying.
 * @return Corresponding Promise.allSettled values.
 */
async function allSettledWithRetry(maxTryCount, promiseFactories) {
  let results;
  for (let retry = 0; retry < maxTryCount; retry++) {
    let promiseArray;
    if (results) {
      // This is a retry; fold in results and new promises.
      promiseArray = results.map(
          (x, index) => x.status === "fulfilled"
            ? x.value
            : promiseFactories[index]())
    } else {
      // This is the first run; use promiseFactories only.
      promiseArray = promiseFactories.map(x => x());
    }
    results = await Promise.allSettled(promiseArray);
    // Avoid unnecessary loops, though they'd be inexpensive.
    if (results.every(x => x.status === "fulfilled")) {
      return results;
    }
  }
  return results;
}

/* test harness below */

function promiseFactory(label) {
  const succeeds = Math.random() > 0.5;
  console.log(`${label}: ${succeeds ? 'succeeds' : 'fails'}`);
  return succeeds
      ? Promise.resolve(label)
      : Promise.reject(new Error(`Error: ${label}`));
}

allSettledWithRetry(5, [
  () => promiseFactory("a"),
  () => promiseFactory("b"),
  () => promiseFactory("c"),
  () => promiseFactory("d"),
  () => promiseFactory("e"),
]).then(console.log);

回答 #2

これは私にはうまくいきました。

export const allSettledWithRetries = async (
  promises: Promise<FixMeLater>[],
  retries: number,
  waitBetweenRetries = 1000
): Promise<FixMeLater[]> => {
  const results = await Promise.allSettled(promises);

  const successResponses: FixMeLater[] = [];
  results.forEach((promiseResult) => {
    if (promiseResult.status === 'fulfilled') {
      successResponses.push(promiseResult.value);
    }
  });

  if (retries > 0 && successResponses.length !== promises.length) {
    const failedPromises: Promise<FixMeLater>[] = [];

    results.forEach((promiseResult, index) => {
      if (promiseResult.status === 'rejected') {
        failedPromises.push(promises[index]);
      }
    });

    logger.warn(`failed to run ${failedPromises.length} and will try again`);

    // eslint-disable-next-line no-promise-executor-return
    await new Promise((resolve) => setTimeout(resolve, waitBetweenRetries));

    const failedResults = await allSettledWithRetries(
      failedPromises,
      retries - 1
    );

    failedResults.forEach((promiseResult) => {
      if (promiseResult.status === 'fulfilled') {
        successResponses.push(promiseResult.value);
      }
    });
  }

  return successResponses;
};