valid,invalid

関心を持てる事柄について

Idempotency-Key Headerの現状・仕様・実装の理解を助けるリソースまとめ

Idempotency-Key Header に関する調査と実装を半年ぐらい前に行ったので、そのとき参考にしたリソースと 2021 年 9 月時点で得られる最新情報をメモしておく。

前提: Idempotency-Key Header とは

HTTP リクエストのうち冪等ではないとされるリクエス*1を冪等にし、安全なリトライを可能にするための仕組みの 1 つ。

Jayadeba Jena, Sanjay Dalal, Erik Wilde 氏らによって 2021 年 11 月に仕様が提案された。現在は IETF のもと、インターネット標準化過程(Standard Track)にあり、IETF Meeting や GitHub issues にて議論が行われている。real world ではすでに多くの企業で類似する実装が行われている。

現状と今後

GitHub issues を見る限り論点はいくつか残っており、RFC になるにはまだ時間がかかりそう。

また、de facto と言えるほどサーバ側の実装パターンはまだ固まっていない印象。実装する際には後述する Airbnb や Stripe の記事などを参照しつつ、各実装者が多少なり手心を加えて設計をしなければならなさそうだ。

今後の最新議論を追いかけるなら GitHub issues や IETF meeting をウォッチすることになると思う。


ここからリソースまとめ。

仕様を説明するもの

The Idempotency-Key HTTP Header Field

https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-00

2021-07-01 に提出された最新の draft。IETF HTTPAPI WG によって accept されたもの。 ちなみに初出は 2020-11-17*2

解決したい課題、API サーバおよびクライアントに期待する振る舞い、先行する実装等々、全体像を掴むのに十分な情報がある。

POST リクエストを冪等処理可能にする Idempotency-Key ヘッダの提案仕様

https://asnokaze.hatenablog.com/entry/2020/11/19/015243

2020 年 11 月、初期の draft 公開を受けての記事。日本語でわかりやすい簡潔な説明。

2021 年 9 月時点では日本語で "dempotency key header" を検索したときに最上位に来るのが当記事。

ちなみに、そのあと英語の記事がいくつか続いた後に僕の Scrapbox のページが来るぐらいに情報が少ない。英語で検索しよう。

2021-09 時点での最新議論

IETF 111 Meeting の発表資料

https://datatracker.ietf.org/meeting/111/materials/slides-111-httpapi-idempotency-header-00

2021-07-26 の IETF 111 Meeting 資料。現在のステータスと議論の状況をざっと掴める。

Discussion on GitHub

https://github.com/ietf-wg-httpapi/idempotency

draft を受けての各界からの反応・意見・課題について議論している repository。

いくつかななめ読みしてみる。

#2 Clarification for status code for various scenarios

各シナリオで返す status code や命名について。「英語が母国語じゃない自分としては〜」のような意見が寄せられるのも面白い。

#3 Feedback from Google Standard Payments

protocol agnostic な実装であるべきでは?と Googler からの指摘。個人の意見として書いているようだが、gRPC を推す Google 側の position talk と見られる節はある。

#5 How does this header compare with OASIS Repeatable Requests Header?

類似する実装として OASIS (Organization for the Advancement of Structured Information Standards) によるRepeatable Requests Headerが存在するがどう使い分けるか、歩調を合わせるか、はたまた統合の道を辿るか。

各種 SaaS の先行実装

IETF なのですでに実装 (Running code) がある*3

draft の "4. Implementation Status" に記されているものを参照するとよい。

https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-00#section-4

主に決済・金融系の SaaS が公開する API で実装されている。先行実装にはIdempotency-Key の名前を使うものもあれば、Request-Idと呼んでいるものもある。

SaaS で実装されている、いくつか代表的なものを挙げる。

Stripe

https://stripe.com/docs/api/idempotent_requests

動画もあった。

https://www.youtube.com/watch?v=nnMqSQtSZUQ

各言語の client 実装があるが、いずれも利用者が Idempotency Key のことを知らなくても自動でセットするようになっている。知らなくても安全にリトライできる良い設計と思う。

もちろん、key のことをよく知る実装者であれば form 入力値などによって「一意性」を決定し、key を生成できる。

以下は Ruby client の例。

https://github.com/stripe/stripe-ruby/blob/v5.38.0/lib/stripe/stripe_client.rb#L869-L873

      # It is only safe to retry network failures on post and delete
      # requests if we add an Idempotency-Key header
      if %i[post delete].include?(method) && config.max_network_retries > 0
        headers["Idempotency-Key"] ||= SecureRandom.uuid
      end

Amazon Pay

https://developer.amazon.com/ja/docs/amazon-pay-api-v2/idempotency.html

日本語で読める。

Note: 冪等キーは各 Amazon Pay 事業者アカウントに固有のものであり、無期限に保存されます。これは、同じキーを異なる事業者アカウントに使用できることを意味します。

key が無期限に保存されるタイプ。

draft によれば、expiry を設定するかどうかは任意。

2.3. Idempotency Key Validity and Expiry

The resource MAY enforce time based idempotency keys, thus, be able to purge or delete a key upon its expiry. The resource server SHOULD define such expiration policy and publish in related documentation.

PayPal

https://developer.paypal.com/docs/business/develop/idempotency/

Idempotency-Key header の draft を書いたうちの 1 人が PayPal のメンバーなのだが、PayPal 自身はPayPal-Request-Id header を使っているというのが興味深いポイント。PayPalには"歴史"がありそうだ。

実装するときに参考になるリソース

各種 SaaS の仕様は参考になるが、当然ながら、記されているのは外から見た振る舞いの定義のみ。

仕組み自体はシンプルなので draft を読み込めばコア部分はわりとすんなり実装できるものの、細かい点において迷うポイントがあった。key を保存するストレージに何を選ぶか、エラーシナリオで返す status code は何か、など。

いざ自分で実装してみようと思ったときに参考になる記事をいくつか。

Designing robust and predictable APIs with idempotency

https://stripe.com/blog/idempotency

2017 年、@brandur が Stripe 在籍時に書いた記事。分散システムにおいてなぜ idempotent な HTTP リクエストが重要か、といった基本思想のおさらい。

加えて、クライアントと API を設計する際に従うべきいくつかの基本原則。

  • 障害が一貫して処理されるようにする - クライアントにリモートサービスに対する操作を再試行させる。そうしないと、データが一貫性のない状態のままになってしまい、将来的に問題が発生する可能性がある
  • 失敗が安全に処理されるようにする - idempotency および idempotency key を使用して、クライアントが一意の値を渡し、必要に応じてリクエストを再試行できるようにすうる
  • 失敗が責任を持って処理されることを確認する
    • Exponential Backoff や jitter を使って、立ち往生している可能性のあるサーバに配慮する

先述した Stripe の Ruby client の実装を読むと Exponential Backoff や jitter のお手本が見られる。

Implementing Stripe-like Idempotency Keys in Postgres

https://brandur.org/idempotency-keys

これも 2017 年の@brandur の記事。

記事はとても長いのだが Sinatra で書かれた実装が公開されており、この実装をベースに解説しているので非常に参考になる。

https://github.com/brandur/rocket-rides-atomic

同実装では key のストレージとして RDBMS を使用している。これは key の保存とリソースの状態の更新が atomic でなければならないため。リクエスト単位で atomic なのではなく、1 リクエストの中で atomic な処理が複数ある複雑なケースではそういうこともある。

また、リクエストハンドラの中だけでなく idempotency key の reap (expire したレコードの削除) や completer (中途半端に終わったリクエストをあとで非同期に完了させる) の実装も参考になる。

Patterns of Service-oriented Architecture: Idempotency Key

https://multithreaded.stitchfix.com/blog/2017/06/26/patterns-of-soa-idempotency-key/

2017 年に concept を解説している記事。トレーサビリティへの言及が良い。

  • とある idempotency key が既存のトランザクションを見つけるために使用されたログを記録しておく
  • サービスがいつ既存のレコードを見つけたのか知ることができる
  • 不適切に設計された idempotency key algorithm を使っていることを発見できる

Good diagram.

Avoiding Double Payments in a Distributed Payments System

https://medium.com/airbnb-engineering/avoiding-double-payments-in-a-distributed-payments-system-2981f6b070bb

2019 年の Airbnb による記事。分散システムに結果整合をもたらすテクニックのうち、write repair を実現するのが Idempotency-Key である、という導入から始まる。

Airbnb が実装している Orpheus と呼ばれる Java library と設計の基本理念が参考になる。API call を3つのフェーズ(Pre-RPC, RPC, Post-RPC)に分けて考え、API が DB に書き込むのは Pre, Post-RPC フェーズのみとし、APIがさらに downstream や external services (payment processor や bank) にリクエストする部分を RPC とするあたりなど。

他にも、クライアントのリトライや Idempotency-Key の生成方法、key を DB から読み出すときは必ず master を参照する、などのテクニックが詳説されている。

Idempotency-Key IETF standards draft

https://brandur.org/fragments/idempotency-key-draft

2021 年 7 月、 https://news.ycombinator.com/item?id=27729610 を見ての@brandur の記事なので比較的新しい。HackerNews と合わせて読むと面白い。

Googler のコメントでは「ヘッダーではなくペイロードに入れたほうが良い」と主張があり、これは gRPC との相性のためと@brandur は見ている。

@brandur は「リクエストボディと分離しておくことで、クライアントが意識せずに idempotency を実現できる」と反論している。

実際に Stripe の API クライアントはそのように実装されているので説得力がある。

その他

国内ではメルペイから数多くの知見が公開されている。


各リソースを再訪して細かくコメントしていたら本記事が思ったより長くなってしまった。全体をもう少し整理したら薄い本にできるような気もする。

*1:https://datatracker.ietf.org/doc/html/rfc7231 , https://developer.mozilla.org/en-US/docs/Glossary/Idempotent あたり参照

*2:https://datatracker.ietf.org/doc/html/draft-idempotency-header-01

*3:IETF の仕様策定プロセスではラフコンセンサス。仕様より先に複数の独立した実装があることがほとんど。 https://www.nic.ad.jp/ja/tech/ietf/section4.html