SNS APIのページング処理は再試行を入れないと止まる

SNS APIでフォロワー一覧のような大量データを取得するとき、ページング処理をそのまま書くと途中の1回の失敗で全体が落ちます。実際に InvokeTimeoutError で止まり、しかも例外メッセージが空でログから原因が追えなかった話と、その対策を2つ書きます。

ページングは「10回以上のリクエスト」になる

フォロワー一覧のような大量データは、一度のリクエストでは返ってきません。ページングで少しずつ取得していく形になります。

問題はリクエスト回数です。フォロワーが1,000人を超えると、それだけで10回以上のリクエストになります。そしてその10回以上のうち1回でも失敗すると、処理全体が落ちます。9回成功していても関係ありません。

1回のAPI呼び出しなら「たまに失敗する」で済みます。しかしページングは直列に何回も呼ぶので、失敗する確率が回数分だけ積み上がっていきます。ここが見落としやすいところでした。

失敗したこと

ページング呼び出しに再試行を入れずに運用していたところ、InvokeTimeoutError で処理が止まりました。取得できたところまでで終わるのではなく、処理全体が落ちました。

もっと困ったのは「ログを見ても分からない」

止まったこと自体は、まだいいのです。困ったのはその後でした。

ログに記録していたのは str(e) だけでした。ところがこの InvokeTimeoutErrorstr(e) が空文字だったのです。つまりログには、エラーが起きたという事実だけが残り、中身が何も残っていませんでした。

何が起きたのか、どこで止まったのか、ログからは分かりませんでした。例外を捕まえて記録していたのに、実質的に何も記録できていなかったことになります。

注意

str(e) だけをログに書く実装は、中身が空の例外に当たった瞬間に完全に手がかりを失います。「例外は捕まえてログに出している」だけでは安全ではありません。

対策1:ページング呼び出しを再試行でくるむ

まずページング呼び出し自体を再試行で包みました。設定はこうしています。

実測

10秒 → 20秒 → 30秒 / 最大4回
ページング呼び出しを再試行でくるみ、待ち時間を伸ばしながら4回まで試す

待ち時間を10秒・20秒・30秒と伸ばしていき、4回まで試す形です。すぐに再試行して同じように失敗するより、間隔を置いたほうが通ることがあるためです。

ポイントは、再試行を入れる場所をページング呼び出しの単位にしたことです。処理全体をやり直すのではなく、失敗したそのページの取得だけを再試行します。10回以上あるリクエストのうち1回が失敗しただけなのに、全部を捨てるのは無駄が大きすぎます。

ポイント

リクエストが10回以上に増える処理では、再試行はオプションではなく前提です。1回だけの呼び出しと同じ書き方をしていると、いつか必ず途中で落ちます。

対策2:ログに型名とトレースバックの最後の1行を入れる

2つ目は、ログの書き方を変えたことです。str(e) だけでは足りないと分かったので、次の2つを必ず入れるようにしました。

  • 例外の型名(メッセージが空でも型名は残ります)
  • トレースバックの最後の1行(どこで落ちたかが分かります)

この2つがあれば、メッセージが空文字の例外でも「何が起きたか」「どこで起きたか」の最低線は残ります。実際に InvokeTimeoutError という型名が分かっていれば、あの時点でもう少し早く見当がついたはずでした。

  • 例外の型名をログに残す
  • トレースバックの最後の1行をログに残す
  • ページング呼び出しを再試行で包む
  • str(e) だけをログに書く
  • ページング全体を再試行なしで一発勝負にする

分かっていないこと

正直に書いておくと、なぜ InvokeTimeoutError が発生したのか、根本原因は分かっていませんstr(e) が空文字だったため当時のログから原因を特定できず、後から遡ることもできませんでした。

今回やったのは、原因の解明ではなく「落ちても止まらないようにする」と「次に起きたら分かるようにする」の2つです。再試行4回で必ず通るという保証もありません。4回とも失敗すれば、やはり処理は落ちます。

うまくいったこと

再試行を入れたことで、1回の失敗が処理全体を落とすという状態からは抜けられました。ログに型名とトレースバックを入れたことで、次に何かあったときに「何も分からない」状態は避けられます。

同じことをやる人への順番

  1. 取得件数からリクエスト回数を見積もる(1,000人超えなら10回以上)
  2. ページング呼び出しの単位を再試行で包む(待ち時間を伸ばしながら4回まで)
  3. 例外ログに型名とトレースバックの最後の1行を入れる
  4. str(e) だけに頼っている箇所を探して直す

特に4番目は、動いているうちは絶対に気づきません。中身が空の例外に当たって初めて分かります。

まとめ

  • フォロワー一覧のような大量データはページングで取得するため、1,000人超で10回以上のリクエストになる
  • 途中で1回失敗すると処理全体が落ちる。実際に InvokeTimeoutError で止まった
  • その例外の str(e) は空文字で、ログを見ても原因が分からなかった
  • 対策1:ページング呼び出しを再試行で包む(10秒・20秒・30秒と待って4回まで)
  • 対策2:ログに例外の型名とトレースバックの最後の1行を必ず入れる
  • str(e) だけを記録すると、中身が空の例外で完全に手がかりを失う
  • 根本原因は今も分かっていない。やったのは「止まらないようにする」と「次は分かるようにする」だけ