Stripe Webhookを使用する時の注意点をまとめます.
Webhookの仕様
https://stripe.com/docs/webhooks/best-practices
- イベントは1回以上配信される.
- 2xx HTTPステータスコードで応答しなければリトライされる.
- イベントの順番は保証されない.
顧客の作成に成功した時のイベント customer.created
と,インボイスの支払いに成功した時のイベントinvoice.paid
の2個のイベントを使用して,作成された顧客に製品の提供を開始する機能を実装することを考えます.
実装 その1
深く考えずに実装すると,以下のようになります.
customer.created
を受信したら,Stripeのcustomer_id
を顧客情報に追加する.invoice.paid
を受信したら,customer_id
が一致する顧客を検索して,製品の提供を開始する.
この実装では,イベントの順番が逆になると,invoice.paid
に含まれるcustomer_id
に一致する顧客は存在しないので,うまく動作しません.
ドキュメントには,以下のように注意が記載されています.
お客様のエンドポイントでは、これらのイベントがこの順序どおりで配信されると想定するのではなく、状況に応じて処理する必要があります。
実装 その2
一時的に未知の顧客情報を保存するように工夫して,イベントの順番が逆になっても大丈夫なように工夫してみます.
customer.created
を受信したら,Stripeのcustomer_id
を顧客情報に追加する.未知の顧客の中に一致するcustomer_id
が存在すれば,製品の提供を開始する.invoice.paid
を受信したら,customer_id
が一致する顧客を検索して,製品の提供を開始する.一致する顧客が存在しなければ,未知の顧客の支払い情報として保存する.
この実装では,Write Skew が発生する可能性があります.
Write Skewとは,2個のトランザクションが,すれ違いざまに相手の変更前の値に依存した更新を行うこと.
例えば以下のようなタイミングで処理される場合があります.
トランザクション1で,customer.created
を受信し,未知の顧客の中からcustomer_id
が一致する顧客を検索したところ存在しなかった.トランザクション2で, invoice.paid
を受信し,customer_id
が一致する顧客が存在しなかった.トランザクション1で,customer_id
を顧客情報に追加した.トランザクション2で,未知の顧客の支払い情報として保存した.
最終的に,製品の提供は開始されません.
実装 その3
2個の処理を並列処理するのがそもそもの問題なので,処理を直列化します.
customer.created
を受信したら,イベント情報をデータベースに保存する.invoice.paid
を受信したら,イベント情報をデータベースに保存する.- 別のトランザクションにて,データベースから
customer_id
が一致するcustomer.created
とinvoice.paid
を検索し,存在すればその顧客に製品の提供を開始する.
最終的な処理をする時に,2個のイベントが揃っていることを確認するので ,イベントの順番が変わっても問題ありませんし,Write Skew は発生しません.
実装 その4
2個のイベントを使用するのがそもそもの問題なので,イベントを1個のみに絞ります.
invoice.paid
を受信したら,customer_id
の顧客の詳細を取得するリクエストをStripeに送信し,結果に従って製品の提供を開始する.
この実装が最もシンプルかもしれません.ただし,顧客の詳細を取得するのに失敗した場合のリトライ処理など別の配慮が必要でしょう.
まとめ
外部のサービスと連携するWebサービスを提供する時に,内容が関連している複数のリクエストを適切に処理するには,工夫が必要です.