リトライと冪等性のデザインパターン

リトライを肴に一晩酒が飲める古橋です。

大規模なデータに触れることが日常茶飯事になっている今日この頃。この分野のおもしろいところは、いつまで経っても終わらないプログラムを簡単に作れてしまうことかもしれません。エラー処理リトライそして冪等性*1の3つを抑えていないプログラムは、小規模なデータなら問題ないが、データ量が多くなると使い物にならなくなる可能性が大です。

大規模データをバッチ処理するケース以外でも、リトライは一般にプログラムの信頼性に関わる重要な問題です。

そんなわけで、リトライに関わるいくつかのデザインパターンを、連載でまとめておこうと思います*2
では、第1回は背景から:

なぜリトライが必要なのか

プログラムは色々な理由で失敗する。例えば、

  • A) 通信先のプログラムが高負荷すぎて応答できなかった
  • B) メモリを消費しすぎてメモリ確保に失敗した。またはOOM KIllerに殺された
  • C) 通信先のサーバがハードウェア的に落ちていた
  • D) データの転送中にネットワークエラーが起きた
  • E) 読み込んだデータが壊れていた
  • F) 設定が間違っていた
  • G) プログラムがバグっていた

など。一部の問題はプログラミングや運用の努力で未然に回避できる可能性がある。A)はキャパシティプランニングでミスっているか監視が足りていないし、B)はメモリ管理の実装が甘い。
とは言え、正直なところそこまで完璧に実装していられないし、ハードウェア故障や想定できなかった突発的な高負荷など、どうしようも無いことも多い*3
堅牢なプログラムを手早く作ろうとすると、リトライでカバーするのが妥当なケースは数多い。

リトライと冪等性

しかし、不用意にリトライすると問題が起きる。具体的には、こんなことが起こりうる:

  1. クライアントが要求を発行した。
  2. サーバはリクエストを正常に受け付けたが、高負荷過ぎてすぐには処理できなかった。
  3. クライアントがリトライした。
  4. 同じリクエストが2度実行されてしまった。

例えば、新しいアイテムを作り、そのIDを返すという要求を不用意にリトライすると、サーバの負荷やネットワークエラーなどの状況によって、同じアイテムが1度に2つ作成されてしまったりする可能性がある。これではマズい。

そこで、リトライを実装する場合には処理を冪等にすることが重要になる。すなわち、同じ要求を複数回行っても結果が同じになるようにする。

例えば、新しいアイテムを作る処理は、ID=xyzで識別される新しいアイテムを作成する という処理に変更できないだろうか? これなら、同じ要求を何回繰り返しても同じアイテムが2つ作成されてしまうことは無い。

冪等にするやり方はアプリケーションによって変わってくる。トランザクションの粒度を考えるのと同じように、それなりに大きな処理をまとめないと冪等にできないことも多い。そこでここでは、リトライと冪等性に関するいくつかのパターンをまとめてみる。

パターン1:IDを付けてCREATEを冪等にする

CRUDのC(Create)を冪等にするには、リソースに一意な名前を付ければいい。前節で挙げた新しいアイテムを作成する処理は、IDが既に存在したらエラーを返すようにサーバを実装しておけば、クライアントは安全にリトライできる。

クライアント側のエラー処理は少し難しい。なぜなら、クライアントが「同じIDが既に存在する(ので新しいリソースを作成できない)」というエラーを受け取ったとき、以前から同じIDが存在していたのが原因なのか、リトライした結果として重複してしまったのかを区別することができない。これはケースバイケースで対処するしか無い。無視しても良いし、警告をレポートしても良いが、決定性なエラーなのでリトライしてはいけない

言い換えれば、Create系のAPIは、HTTPの409 Conflictに相当するようなエラーコードを定義しておき、クライアントはそれを他のエラーとは区別して扱う必要がある。クライアント側の実装は後回しにしてもいいが、もし何か新しいAPIセットを設計することがあれば、エラーコードや例外クラスにConflictを含めておいた方がいい。

パターン2:エラーを区別してDELETEを冪等にする

次回 続・リトライと冪等性のデザインパターン - リトライはいつ成功するか に続く。

*1:読み方は『ベキトウ』。『冪』は常用漢字では無いので、『べき等』という書き方もよくされる。

*2:連載本数は未定

*3:そして客は必ず想定外のことをやるという事実。色々な意味で予想を超えてくるのがお客さんというものであるようです。