「外部への通信を行いたい」
「その過程で、一時的な通信エラーやサーバエラーの場合にはリトライしたい」
こんな要件ってありますよね。
今回は TypeScript で簡単にリトライできるライブラリを探してみたので紹介します。
リトライ向けのライブラリ
候補となりそうなライブラリを 2 つほど見つけました。
どっちがいいかわかりませんが、今回は github のスター数が多い方を選択。
ということで、TypeScript の「@types/async-retry」を採用してみます。
async-retryのインストール
インストールは簡単。
依存関係に「@types/retry」がありますので、こちらも一緒に node_modules へ。
1 | $ npm install --save @types/async-retry |
package.json の dependencies に以下が追加されました。
1 2 | "dependencies": { "@types/async-retry": "^1.4.4", |
package-lock.json も同様に更新されています。
async-retryの使い方
利用したい ts ファイルで async-retry を import すれば使えます。
1 | import asyncRetry from 'async-retry'; |
後から知ったことですが、github 上のソースコードを見てもらえば分かる通り、declare でアンビエント宣言されていますね。
これって JavaScript のライブラリに静的型付けをして TypeScript で利用できるという意味だったのですね。
1 2 3 4 5 6 7 8 | // 一部抜粋 declare function AsyncRetry<A>( ): Promise<A>; declare namespace AsyncRetry { } |
async-retryのエラー
アンビエント宣言の存在なんて知らなかったので、こんな現象に悩まされました。
エディタ(IDE)上ではエラーが出ないものの、ビルド時に以下のエラーが発生します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | node:internal/modules/cjs/loader:942 throw err; ^ Error: Cannot find module 'async-retry' Require stack: - /hoge/fuga/app/test/test.js at Module._resolveFilename (node:internal/modules/cjs/loader:939:15) at Module._load (node:internal/modules/cjs/loader:780:27) at Module.require (node:internal/modules/cjs/loader:1005:19) at require (node:internal/modules/cjs/helpers:102:18) at Object.<anonymous> (/hoge/fuga/app/test/test.js:99:99) at Module._compile (node:internal/modules/cjs/loader:1105:14) at Module._extensions..js (node:internal/modules/cjs/loader:1159:10) at Module.load (node:internal/modules/cjs/loader:981:32) at Module._load (node:internal/modules/cjs/loader:827:12) at Module.require (node:internal/modules/cjs/loader:1005:19) { code: 'MODULE_NOT_FOUND', requireStack: [ ] } Node.js v18.1.0 make: *** [run] Error 1 |
npm のキャッシュなどをクリアしても状況は変わりません。
1 | $ rm -rf node_modules package-lock.json && npm cache clean --force && npm install |
ライブラリのパスを確認
node コマンドでライブラリの参照先などを確認しますが、特に問題があるようには見受けられない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $ node > require.resolve.paths("async-retry"); [ '/hoge/fuga/app/test/repl/node_modules', '/hoge/fuga/app/test/node_modules', '/hoge/fuga/app/node_modules', '/hoge/fuga/node_modules', '/hoge/node_modules', '/node_modules', '/hoge/fuga/.node_modules', '/hoge/fuga/.node_libraries', '/hoge/fuga/.nodenv/versions/18.1.0/lib/node', ] |
モジュールがあるのに「MODULE_NOT_FOUND」の繰り返しです・・・。
1 2 3 4 5 6 7 8 9 10 | > require.resolve("async-retry"); Uncaught Error: Cannot find module 'async-retry' Require stack: - <repl> at Module._resolveFilename (node:internal/modules/cjs/loader:939:15) at Function.resolve (node:internal/modules/cjs/helpers:108:19) { code: 'MODULE_NOT_FOUND', requireStack: [ '<repl>' ] } |
ただ、エラーで「async-retry」が見つからないと言っているので、もしかすると JavaScript 側のライブラリが必要?
ということで早速インストール。
1 | $ npm install --save async-retry |
package.json に追加されました。
1 | "async-retry": "^1.3.3", |
そして、この問題が解決します。
上の方で書いたように、TypeScript 側のライブラリでは静的型付けしているだけだったのですね。
TypeScript 初心者にはなかなか気付けない・・・。
@types/request とはまた違うのだろうか。
async-retryを使ってみる
asyncRetry の引数は以下の 2 つ。
(asyncRetry は import の名前に置き換えてください)
・リトライ対象の関数(func)
・リトライの設定(option)
要はこんなイメージでしょうか。
asyncRetry(func: Function, option: Object) => Promise
リトライには backoff 的な機能も提供されているので、設定を確認していきましょう。
retries: リトライ回数
minTimeout: 最小タイムアウト
maxTimeout: 最大タイムアウト
factor: リトライ間隔の乗数
randomize: ランダム有無
リトライ回数のデフォルトは 10 となっています。
最小と最大のタイムアウトの兼ね合いもありますが、用途に応じてここは変わってくるでしょうか。
自分主体のバッチ処理であれば、リトライ間隔を多くとっても他に影響はしません。
逆に別のクライアントから自分がサーバ側として呼び出されている立場なら、あまりクライアントを待たせることはできませんよね。
デフォルトの乗数が 2 なので、最小タイムアウトを 1 秒とした場合は、以下のようにリトライ間隔が増えていきます。
(ランダムはオフにしておきます)
1回目のリトライまでの間隔: 1秒
2回目のリトライまでの間隔: 2秒
3回目のリトライまでの間隔: 4秒
4回目のリトライまでの間隔: 8秒
5回目のリトライまでの間隔: 16秒
通信先の状態によって、このあたりも調整が必要になってきます。
すぐにリトライして成功する可能性があるような通信先の場合は、リトライ間隔を短くした方がいいですし。
通信エラーが発生したら、5 秒以内に回復することはない通信先なら、それ以上に間隔を空けてリトライしたいですよね。
まとめ
TypeScript で使えるリトライのライブラリについて紹介してきました。
async-retry は最低限のリトライ機能が揃っているので、これで十分ですね。
通常、外部との通信がサーバエラーとなったら、その旨をレスポンスしてリトライしてもらうことが多いと思います。
ただ、リトライすることで成功の余地があるなら、サーバサイド側で 2, 3 回リトライを試みてあげても良さそう。
この対応でユーザの離脱などが減るなら、仕込んでおく価値はあるでしょう。