Idempotency-Key Header
HTTP APIの冪等性を保つ仕組み
ネットワークは信頼できないという分散システムの定番問題に対処する方法の1つ
Patterns of Service-oriented Architecture: Idempotency Key 2017
idempotency key を受け取ったサービスはデータベースをチェックして同一コンシューマからのリクエストが、与えられた idempotency キーで以前にサービスされたものかどうかを調べる
idempotency キーが特定のコンシューマにスコープされていることが重要
ohbarye.icon SaaSに限らず、複数のコンシューマがいる場合を想定
既存のレコード/レスポンスが見つかった場合、サービスはそれを返す
見つからなければ、サービスは後で使えるようにconsumer ID/idempotencyキーを保存し、リクエストにサービスを提供しなければならない
この操作はそれ自体がidempotentでなければならない
コンシューマ ID/IDempotency キーを記録しているのに仕事をしない (あるいはその逆) という状況は避けたい
これを達成する最も簡単な方法は、データベース・トランザクション
作業が完了すると、新しいレスポンスがコンシューマに返される
key algorithm
コンシューマは他のコンシューマが再現できるkey algorithmを使ってはならない
コンシューマは特定のユースケースに合わせてkeyを設計しなければならない
サービス側もコンシューマ側もkeyから意味を解析する必要はない
とはいえ、難読化しすぎるとデバグが困難になる
ハッシュ化したくなるかもしれないがほとんど価値はない、keyの生成をあとからトレースするのが難しくなる
tracability
とあるidempotency keyが既存のトランザクションを見つけるために使用されたログを記録しておく
サービスがいつ既存のレコードを見つけたのか知ることができる
不適切に設計されたidempotency key algorithmを使っていることを発見できる
Understanding Idempotency in REST APIs
HTTP のコンテキストで idempotency について話すときに、safetyという言葉が出てくる
HTTP メソッドが安全とは、その HTTP メソッドがサーバーの状態を変更しないということ
https://developer.mozilla.org/ja/docs/Glossary/safe
下の表は、一般的に使用される HTTP メソッドとその安全性、および idempotency を示す
code:table
Idempotence in HTTP
+-------------------+--------------+-------------------+
| Http Method | Saftey | Idempotency |
+-------------------+--------------+-------------------+
| GET | Yes | Yes |
| PUT | No | Yes |
| POST | No | No |
| DELETE | No | Yes |
| PATCH | No | No |
+-------------------+--------------+-------------------+
https://developer.mozilla.org/en-US/docs/Glossary/Idempotent を参考にしていると思われる
https://tools.ietf.org/html/rfc7231 で定められていた
APIを実装するときの推奨事項
x-idempotency-keyのようなHTTP headerを使う
事前に決めた時間が経過したらidempotency keyをinvalidateする
経過後のリクエストは新たなリクエストとして扱う
おかしいidempotency keyの扱い方がされていないかを検証する
リトライの際に初回と同一のkeyを使いつつもpayloadが異なるなど
新規リクエストとして扱うか、bad requestとするかは実装次第
2020-11 The Idempotency HTTP Header Field
Paypalメンバーによるドラフト
2021-07 https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-00
実装例
StripeのIdempotency-Keyヘッダ
https://stripe.com/docs/api/idempotent_requests
すべてのPOSTリクエストに有効
GET, DELETEにはつけても意味ない
成功したか失敗したかに関わらず、与えられたidempotencyキーに対して最初に行われたリクエストの結果のステータスコードとボディを保存することで動作します。同じキーを使ったそれ以降のリクエストは、500エラーを含む同じ結果を返します。
keyは24時間以上経過すると自動的にシステムから削除され、削除された後に元のkeyが再利用されると新しいリクエストが生成されます。idempotency レイヤーは、誤って誤用されることを防ぐために、入力されたパラメータを元のリクエストのものと比較し、同じものでない限りエラーにします。
結果が保存されるのは、API エンドポイントが実行を開始した場合のみです。入力パラメータが検証に失敗した場合、またはリクエストが同時に実行されている別のリクエストと競合した場合、API エンドポイントが実行を開始しなかったため、結果は保存されません。これらのリクエストを再試行しても安全です。
GET, DELETE, OPTION はそもそも Idempotency を持つべき
https://stripe.com/docs/idempotency
サーバがIdempotent-Replayed: trueを返すことにより、リプレイ以前に実行されたかどうかを判別できる
key
v4 UUIDなど十分なエントロピーを持つランダムな文字列を使うべき
ショッピングカートのIDなどユーザー操作から導出するとよい
2017 Designing robust and predictable APIs with idempotency by @brandur
失敗による分散状態の不整合に対処する最も簡単な方法は、サーバのエンドポイントを idempotent に実装すること
HTTPのセマンティクスではPUT, DELETEはidempotent
PUTはペイロードによってターゲットリソースが完全に置き換えられる
本質的にidempotentなメソッドはよいが、そうでないメソッドはどうすればよいか
たとえば顧客にお金を請求する場合
ohbarye.icon この例めっちゃ出てくる、さすがStripe
分散システムに参加する良いシステムコンポーネントであるために、クライアントではExponential Backoff推奨
ただし複数のクライアントが同時に襲撃してしまうThundering herd problemもあるのでリトライまでに要する時間にはランダム性も必要
このランダムネスをjitterと呼ぶ
分散システムにおける障害の可能性とその対処方法を考慮することは、ロバストで予測可能なAPIを構築する上で最も重要
クライアント上でのリトライロジックとサーバー上でのidempotencyは、この目標を達成するために有用なテクニックであり、どのようなテクノロジースタックでも比較的簡単に実装できる
クライアントと API を設計する際に従うべきいくつかの基本原則
障害が一貫して処理されるようにする
クライアントにリモートサービスに対する操作を再試行させる。そうしないと、データが一貫性のない状態のままになってしまい、将来的に問題が発生する可能性がある
失敗が安全に処理されるようにする
idempotency および idempotency key を使用して、クライアントが一意の値を渡し、必要に応じてリクエストを再試行できるようにすうる
失敗が責任を持って処理されることを確認する
Exponential Backoffやjitterを使って、立ち往生している可能性のあるサーバに配慮する
Stripe Ruby client
Idempotent-Replayed header support https://github.com/stripe/stripe-ruby/pull/907
methodがPOST or DELETEで、リトライ回数を1以上に設定している場合は自動的にセット
headers["Idempotency-Key"] ||= SecureRandom.uuid
https://github.com/stripe/stripe-ruby/blob/db24334b9e032a491be1bc391f5451492fd225cd/lib/stripe/stripe_client.rb#L746-L748
Exponential Backoffとjitterを使ってリトライしている
https://github.com/stripe/stripe-ruby/blob/1bb9ac48b916b1c60591795cdb7ba6d18495e82d/lib/stripe/stripe_client.rb#L78-L92
2021-07 Idempotency-Key IETF standards draft by @brandur
Stripeの実例を紹介
HackerNews https://news.ycombinator.com/item?id=27729610
Googlerのコメント
ヘッダーではなくペイロードに入れたほうが良いと主張
これはgRPCとの相性のためとbrandurは見ている
brandurは「リクエストボディと分離しておくことで、クライアントが意識せずにidempotencyを実現できる」と反論
実際StripeのAPIクライアントはそのように実装されている
Amazon Pay
PayPalのPayPal-Request-Idヘッダ
Google Standard Payments APIのrequestId
メルペイ
1:14:00あたり
中間状態の決済・返金はリトライによって完了まで進めている
Idempotency-KeyによるAPIのリトライのときと同じ
非同期処理とリコンサイルによって可用性を高めている
https://www.youtube.com/watch?v=Dq9Erg3SIZk
その他
https://e34.fm/4/
POSTリクエストを冪等処理可能にするIdempotency-Keyヘッダの提案仕様 2020