2024年新春、YAPC::Hiroshimaのスポンサーとなった株式会社スマートバンクはイベントを最大限に盛り上げるため、広島のお食事どころ紹介企画を行っています。参加者の皆様にはグルメマップをノベルティとして配布していますのでぜひご覧ください。
さて、当企画にあたりスマートバンクCTO @yutadayo は広島の飲食店を練り歩いたのですが...困ったことが起きました。カード決済ネットワークの途中で障害が起き、飲食店からカード会社への決済リクエストが何重にもリトライ送信されてしまったのです。
もし全てのリクエストがカード会社で処理されたら...口座残高がなくなりスマートバンクCTOは破産に追い込まれてしまいます。重複リクエストを適切にさばくプログラムを記述してCTOを破産から救いましょう!
10~20分
何はともあれ、さっそくチャレンジしてみましょう。
git clone https://github.com/smartbank-inc/yapc2024-quiz
cd yapc2024-quiz
make run
make run
を実行するとDocker Composeにより http://localhost:3000 でリクエストを待ち受けるHTTPサーバーが起動し、client/client.rb
から同サーバーへの決済リクエストが行われます。ターミナルにあなたの回答や採点結果が表示されれば正常に動いています。
初期実装ではすべての決済リクエストを許可しているのでCTOは破産してしまいます🥲 重複したリクエストを処理しないようにサーバープログラムを改修して、残高をプラスに保ちつつ訪問したお店が正しく記録されるようにできればチャレンジ成功です。
Ruby, Go, Perlの3種類の実装をそれぞれapi-ruby/app.rb
, api-go/app.go
, api-perl/app.pl
として用意しています。任意の言語を指定してサーバ起動を行う場合は以下のコマンドが利用できます。
make run-ruby # `make run`では`run-ruby`が実行されます
make run-go
make run-perl
いずれの言語でもdocker compose logs
でサーバーのログを見ることができます。
カード決済の仕組みを簡略化すると以下のようになります。
flowchart LR
payfac([""ペイメント\nファシリテーター""])
s1([加盟店A]) --決済許可してOK?--> payfac
s2([加盟店B]) --決済許可してOK?--> payfac
s3([加盟店C]) --決済許可してOK?--> payfac
payfac --決済許可してOK?--> issuer([カード会社API])
今回のクイズではペイメントファシリテーターに相当するのがclient/client.rb
となり、改修してもらうのはなぜかあなたの手元にあるカード会社APIのプログラム(api-xxx/
のコード)です。
flowchart LR
payfac([""ペイメントファシリテーター\n == client.rb""])
payfac --#POST http://localhost:3000/payments\n with Idempotency-Key Header--> issuer([カード会社API\n==あなたが改修するコード])
今回のシナリオでは加盟店またはペイメントファシリテーターのいずれかに障害があり、カード会社サーバーへ大量のリクエストが飛んできます。
障害は悲しいことですが、幸いにも、ペイメントファシリテーターからの決済リクエストにはIdempotency-Key: {UUID}
のヘッダーが付与されています1。
このHTTPヘッダーはリクエストの同一性を示すもので、受け取ったサーバーには以下の振る舞いが期待されます。
- 同じ値を持つリクエストを複数回受け取ったサーバーは最初の1回のみリクエストのみ処理し、後続のリクエストでは処理を行ってはいけません。
- Idempotency-Keyヘッダーを受け付けるAPIエンドポイントは、このヘッダーが付与されていないリクエストを処理してはいけません。
上記仕様を満たす実装を完成させてCTOを救えたらクリアです 🎉
さらに高みを目指したい方は以下の条件を満たす実装に挑戦してみましょう!正しく実装できるとハードモードクリアと認定され、採点結果が変わります。
- サーバ側で既知のidempotency keyに対して以前と異なるリクエストボディが送信された場合、サーバーはHTTPステータスコード422 Unprocessable Entityを返します。
# 初回のリクエストにて以下のリクエストボディを送信したとします。
curl -X POST -d '{"amount": 1000, "shop_name": "Chef The Okonomiyami"}' -H "Idempotency-Key: 362b327e-50a8-4074-a182-3f1016283ac2"
# もし同じIdempotency-Keyにも関わらず、以下のような異なるリクエストボディが送信された場合は422を返しましょう。
curl -X POST -d '{"amount": 2000, "shop_name": "Chef The Okonomiyami"}' -H "Idempotency-Key: 362b327e-50a8-4074-a182-3f1016283ac2"
- この物語はフィクションであり、実在の人物・団体とは一切関係ありません。
- このクイズはIdempotency-Keyヘッダーのコンセプトとカード決済の雰囲気を理解していただくためのものです。
- 提案仕様に追従・準拠したものではありません。
- Idempotency-Keyヘッダーを使わずにプログラムを変更して破産を避けることもできますが、せっかくなので使ってみましょう!
- 実際のカード決済のリクエスト(オーソリゼーション)はIdempotency-Key Headerとは異なる仕組みで冪等性を保っています。興味がある方はスマートバンク社員をつかまえて聞いてみてください。
- 実際のカード決済は遥かに複雑です。入門には「B/43カード決済システムのしくみ(前編)」がおすすめです。
グルメマップキービジュアル
ありえるかもしれない未来
Footnotes
-
api-rubyの場合はRackによって
HTTP_IDEMPOTENCY_KEY
という形式に変換されます。 ↩