Serverless Operations, inc

>_cd /blog/id_ud8p0oxkq6l

title

Node.jsでリトライ処理の制御をしてくれるライブラリ「p-retry」の紹介

summary

昨今ではAmazon Bedrockを中心としたLLMを使った実装の機会が増えたことにより、いわゆる大量のリクエストをLLMに送った際にエラーが返ってきてしまいリトライ処理を実施するケースが多くなってきているのではないでしょうか?もちろん、AWS Step Functionsなどをでステートごとリトライを実装するケースもありますが、プログラムロジック内のループ処理にてリトライをしたい場合もあるでしょう。そんな時に便利なライブラリが、p-retryです。この記事ではp-retryについての紹介を行いたいと思います。

p-retryが無い時

まず、ライブラリ無しでリトライ処理を書いてみましょう。リトライが失敗するたびにその指数関数的にリトライ間隔をのばしていくExponential Backoffを手法として採用してみます。この場合、例えば以下のような再試行ロジックを実装することになります。

  • 1回目のリクエスト失敗、1 x 任意の秒数 秒待って再試行
  • 2回目のリクエスト失敗、2 x 任意の秒数 秒待って再試行
  • 3回目のリクエスト失敗、4 x 任意の秒数 秒待って再試行

以下はOpensearch Serverlessに大量のデータをBulk Importする際のロジックです。429のエラーが返ってきたときがOpensearchのBulk用のキューがいっぱいになり処理が続けられなくなったことを意味しています。その際に指数関数的再試行ロジックを採用して処理が成功するまでリトライを続けます。(サンプルコードでは指数関数的にリトライ間隔を伸ばしてはいませんが。。)ただ、while文を使っていたり、コードの質や可読性としてはあまり優れていないことは感じられるのではないでしょうか?

const MAX_RETRIES = 20 // リトライの上限回数

for (let i = 0; i < keys.length; i++) {
  const key = keys[i]
  let attempt = 0

  //途中ロジックは省略

  // 429エラーが返ってきたらリトライし続ける仕組み
  while (attempt < MAX_RETRIES) {
    attempt++
    // bulk importの実施
    const res = await ossClient.bulk({ body: bulkPayload })

    // 429エラーが帰ってきたときはリトライを実施
    if (res.body.errors === true && res.body.items[0].index.status === 429) {
      logger.warn(
        `BulkInsertエラー試行回数 ${attempt} 回、ソースファイル:${key}`,
      )

      if (attempt >= MAX_RETRIES) {
        throw new Error(
          `429エラーにより、最高試行回数${attempt}回を超えました`,
        )
      }

      // 再試行が失敗するたびに間隔を伸ばす
      await sleep(2000 * attempt)
      continue
    }
    break // while抜けて次のkeyへ
  }
}

p-retryがある時

上記のコードをp-retryでリファクタリングすると以下のようになります。かなり可読性の高いコードになったのがわかるのではないでしょうか?今回でいうとリトライ対象となる関数はossClient.bulk になります。この関数というかメソッドから429エラーが返ってきた時に、throw することで p-retry がリトライ対象と判断してくれます。

import pRetry from ‘p-retry’

const MAX_RETRIES = 20

for (const key of keys) {
  // 途中のロジックは省略

  await pRetry(
    async () => {
      const res = await ossClient.bulk({ body: bulkPayload })

      const status = res.body?.items?.[0]?.index?.status
      const hasError = res.body?.errors === true

      if (hasError && status === 429) {
        throw new Error(`429エラー: key = ${key}`)
      }

      return res
    },
    {
      retries: MAX_RETRIES,
      factor: 2,
      minTimeout: 2000, // 2秒から指数バックオフ
      maxTimeout: 30000, // 最大30秒
      onFailedAttempt: (error) => {
        logger.warn(
          `BulkInsertリトライ中: ${error.attemptNumber}回目、残り${error.retriesLeft}回。ソースファイル:${key}`,
        )
      },
    },
  )
}

factorという項目で指数バックオフの増加係数を設定します。

factor: 2 のとき:

  • 1回目のリトライ → minTimeout × 1 = 2000ms
  • 2回目 → 2000 × 2 = 4000ms
  • 3回目 → 2000 × 4 = 8000ms

といった形指数関数的にリトライ間隔が伸びで行く仕組みです。そして、maxTimeout で指定した間隔に達するまでリトライを継続します。onFailedAttempt はリトライが失敗するたびに呼ばれる関数で、ここでログなどを残してあげるとあとからデバッグ等の調査がやりやすいかもしれません。

こんな感じで便利に使えるライブラリですので、リトライロジックが必要になった際は是非試してみてください!

Written by
CEO

堀家 隆宏

Takahiro Horike

  • Facebook->
  • X->
  • GitHub->

Share

Facebook->X->
Back
to list
<-