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
はリトライが失敗するたびに呼ばれる関数で、ここでログなどを残してあげるとあとからデバッグ等の調査がやりやすいかもしれません。
こんな感じで便利に使えるライブラリですので、リトライロジックが必要になった際は是非試してみてください!