Architecture

二回来る webhook を前提に設計する

決済イベントの二通目は新しい仕事ではない。一意のキー、送信箱、過去を書き換えない取消。

Design for the webhook that arrives twice

危ない要求は最初のものではありません。送り手が最初の答えを聞かなかったか、聞いても再送したために、同じ要求がもう一度来ることです。決済事業者はそうします。キューもそうします。人がボタンを二度押すとブラウザもそうします。システムが二通目を新しい仕事として扱うと、二度請求し、二度送り、一つの賭けに台帳の二行を書きます。

重複は来る前提で

二度目の配送が、何もしない成功になるようにハンドラを設計します。一度目は仕事をして鍵を保存します。提供者のイベント id か、クライアントが送った鍵で、データベース内で一意です。二度目はその鍵を見つけ、同じ結果を返します。二度目の請求はありません。二度目のメールもありません。二行目もありません。

信頼するのは一意制約です。挿入の直前にアプリケーションコードで行う確認は、二つの worker が同時に起きたとき競合に負けます。データベースはその競合に負けません。負けた側は衝突を受け取り、「すでに済んだ」に変えます。

一回目の到着二回目の到着
照合キーは新しいキーはすでに保存済み
書く一行、副作用一つ新しい行はない
返信受理同じ、すでに受理済み
処理が途中で死んだら送信箱はまだそのメッセージを借りている二つ目は借りていない

メッセージは書き込みと一緒に出る。そうでなければ出ない

リクエストの中、commit の前にメールやキューのメッセージを送ると、巻き戻った取引のメッセージが残ります。出ていくメッセージは業務の行と同じトランザクションに書き、別の worker が公開します。プロセスが死ねば、行とメッセージは両方あるか、両方ないかです。worker は公開を再試行できます。ハンドラは請求を再試行してはいけません。

取消は新しい仕訳であり、書き換えではない

取り消す必要があるとき、元の行は上書きしません。相殺の記入が何が変わったかを示し、最初の行は読めるままにします。一週間後でも、14:02 に何が信じられていたか答えられます。行を書き換えると、今何が信じられているかしか分からず、証人もいません。

再試行で操作が二度起こり得るなら、その操作は終わっていない。

ハンドラを書く前にキーに名前を付ける

再送のあいだに送り手が何を変えるか確かめていないなら、鍵は「リクエスト全体のハッシュ」ではありません。決済事業者は同じイベント id を、新しい配送時刻つきで再送します。その時刻が鍵の中にあると、二度目は新しく見え、防ぎたかった不具合を自分で作ります。送り手が安定だと約束する id を使います。約束がなければ、呼び出す側に一つ送るよう求め、それなしの呼び出しは拒みます。

それから、失敗を成功と同じ場所に書きます。衝突は、送り手にとってのエラーページではありません。再送への、予期された答えです。送り手は止まれるべきです。500 を返すと、三度目、四度目と試し、インシデントは同じ作業の待ち行列になります。

順調な道が 200 を返したからといって、終わりとはみなしません。テストは同じペイロードを再送し、副作用が一つだと確認します。それから commit と公開のあいだで worker を殺し、メッセージがなお一度だけ出ることを確認します。この二つのテストは、置き換えるインシデントより短いです。そこから始めます。