コンテンツへスキップ
DedicatedPHP 接触

PHPで順序が前後するWebhookでも状態を破損させない

検証、監査記録、冪等性により、重複・遅延・並行して到着するイベントに耐え、状態を安全に保つPHP統合を設計します。

状態制御を備えたPHPアプリケーションで処理される、重複および遅延Webhookイベントの編集用図解

PHPで順序が前後するWebhookは、単なる接続性ではなく整合性の問題です。プロバイダーは有効な応答を受信しなかったため配信を再送する場合があり、キューはメッセージを遅延させることがあり、同一エンティティの2つのイベントが別々の経路を通ることもあります。アプリケーションが各イベントは一度だけ順番に到着すると仮定すると、古い確認が後の取消を上書きしたり、重複によって不可逆な操作が2回実行されたりする可能性があります。

出発点となる原則は単純です。Webhookは、別のシステムで何かが変わった可能性があるという通知です。それ自体は、検証なしにローカル状態を変更するための信頼できる指示ではありません。設計では、受信した証跡を保持し、どのイベントを受け入れ可能とするかを判断し、ドメインルールに従って冪等かつ順序立てて変更を適用する必要があります。

受信、検証、ドメインへの適用を分離する

受信、検証、ドメインへの適用を分離する — guía visual de DedicatedPHP

HTTPエンドポイントは、少ない処理を予測可能な形で行うべきです。その責務は、リクエストを受信して検証し、不変のレコードを永続化し、送信元が期待する期限内に応答することです。注文、サブスクリプション、在庫、その他の業務エンティティを変更する処理は、その後、通常は非同期プロセスによって実行すべきです。

フェーズを分離すると、内部APIの一時的な障害によって有効な配信が曖昧な再試行に変わることを防げます。また、プロバイダーに古いイベントの再送を要求せずに処理を再開できます。

  1. 受信: ヘッダー、変換していない本文、受信時刻、特定された送信元を取得します。
  2. 入力検証: 署名、形式、サイズ、コンテンツタイプ、最低限のフィールドを確認します。
  3. 永続化: 短いトランザクションでイベントとその初期状態を保存します。
  4. キュー投入: HTTP応答内での処理に依存せず、保留中の作業があることを通知します。
  5. 適用: workerがイベントを解釈し、必要な状態を取得して、制御された業務上の遷移を実行します。

配信とイベントを区別することが重要です。同じ配信が繰り返されることがあり、一部のプロバイダーは配信試行ごとに異なる識別子を割り当てます。安定したイベント識別子が存在するなら、通常はそれが重複排除の最適な基盤です。存在しない場合は、送信元、外部エンティティ、タイプ、既知の意味を持つバージョンまたはタイムスタンプからキーを定義する必要があります。

監査と再処理のために記録すべき項目

イベントテーブルには解釈済みのJSONだけを保存してはいけません。保存前に正規化すると、署名の検証、インシデント調査、後続パーサーの調整に必要な情報が失われる可能性があるため、元の本文を保持してください。

少なくとも、レコードには次の内容を含めるべきです。

  • 送信元またはプロバイダー、および統合環境。
  • 外部イベント識別子と、存在する場合は配信識別子。
  • イベントタイプ、外部エンティティ識別子、バージョン、シーケンス、または有効日。
  • 関連ヘッダーと、改変から保護された元のペイロード。
  • ローカルの受信時刻と、それとは別の送信元が宣言したタイムスタンプ。
  • 診断および補助的な重複排除のためのペイロードの暗号学的フィンガープリント。
  • 処理状態: 受信、検証済み、保留中、適用済み、無視、失敗、またはレビュー中。
  • 試行回数、要約されたエラー、最終試行時刻、影響を受けたローカルエンティティへの参照。

(origin, external_event_id)に一意制約を設ければ、プロバイダーが安定したIDを提供する場合の重複を解決できます。まず挿入し、競合は業務エラーではなく既知の配信として扱います。再試行を止めるため、応答は引き続き成功としてよい場合があります。

ただし、メッセージの重複排除だけでは冪等性を保証できません。たとえば、異なる2つのイベントが同じ確認を表し、どちらも会計仕訳を作成しようとすることがあります。業務操作自体に保護が必要です。冪等性キー、結果に対する一意制約、または結果がすでに存在するかを確認する遷移を用います。

真正性を検証し、入力面を制限する

想定されたIPアドレスから来た、あるいは秘密らしきフィールドが含まれるという理由だけでWebhookを受け入れてはいけません。プロバイダーが対応している場合は、生の本文とタイムスタンプに基づいて計算された署名を検証します。比較は定数時間で行い、妥当な時計のずれを考慮しつつ、時間枠によってリプレイを制限する必要があります。

永続化する前に、本文の最大サイズ、読み取り時間、受け入れる形式、最小スキーマといった運用上の制限を課してください。有効なJSONが有効なイベントとは限りません。効果を適用せずにアーカイブする明示的なポリシーがない限り、未知のタイプは拒否します。

署名シークレットにはローテーションが必要です。切り替え時には、期間を限定して旧キーと新キーを受け入れ、どちらが配信を検証したかを記録する必要がある場合があります。完全な本文、トークン、不必要な個人データをアプリケーションログに含めないでください。監査記録にはアクセス制御と、データの機微性に応じた保持ポリシーが必要です。

ネットワークの順序を信頼せず、論理的な順序を決める

受信時刻は、何が先に起きたかを定義しません。ペイロードに含まれる日時も常に十分とは限りません。概算である、遷移ではなくイベント作成時に属する、または同期されていない時計の影響を受ける可能性があります。最適なシグナルは、送信元システムが提供するエンティティごとの単調増加バージョンまたはシーケンス番号です。

バージョンがある場合は、ローカルエンティティに最後に適用したバージョンを保存します。workerはイベントのバージョンが保存済みのものより大きい場合にのみ適用できます。同じバージョンは重複を示し、小さいバージョンは遅延イベントです。シーケンスに欠番がある場合、中間状態を推測してはいけません。エンティティを照合対象としてマークするか、そのAPIが参照元の記録であればソースAPIを照会します。

シーケンスもバージョンもない場合、ルールはドメインに基づく必要があります。受信したテキストを直接代入するよりも、明示的な状態機械の方が安全です。たとえば、取消済みのエンティティは、文書化され認可された遷移がない限り、確認済みに戻れないようにできます。モデルは、現在の状態と入力イベントの組み合わせごとに何をするかを定義しなければなりません。

if ($eventVersion <= $entity->lastExternalVersion) {
    markIgnored($event, 'version_no_mas_reciente');
    return;
}

applyAllowedTransition($entity, $event);
$entity->lastExternalVersion = $eventVersion;

このコードは基準を示すものであり、トランザクションや遷移ルールの代わりにはなりません。バージョンのないイベントでは、送信元との契約がその意味論と精度を保証する場合にのみ、日時比較を許容できます。

誤るコストに応じて遅延イベントを扱う

遅延したすべてのイベントに同じ対応が必要なわけではありません。無視、記録、再計算、補償のいずれを選ぶかは、イベントが実際の義務を変更し得るか、および真実の情報源がどこかに依存します。

  • 無視: 影響が検証可能な後続状態にすでに含まれている古いバージョンに適しています。
  • 記録とアラート: シーケンスに不整合がある、または介入なしに判断する情報が不足している場合に有用です。
  • 再計算: 外部ソースが優先される場合に、外部システムの現在状態を照会し、ローカルミラーを更新します。
  • 補償: 以前の効果がすでに結果を生み、安全に削除できない場合に、追跡可能な是正アクションを作成します。

外部操作の仮想的なケースを考えてみます。バージョン12の確認が到着し、その後バージョン13の取消が到着し、さらに後で確認12が再試行されます。バージョン制御により、この重複によって操作が復活することはありません。取消が先に到着し、システムがバージョン12の欠落を認識している場合、状態機械が許可すれば取消を適用でき、そうでなければ機微な効果を生じさせる前に照合を求めることができます。

内部並行性、キュー、エンティティごとのロック

非同期処理は応答性を向上させますが、内部競合をもたらします。一方が書き込む前に、2つのworkerが同じ状態を読み取る可能性があります。イベントの重複排除では、この状態を防げません。

機微なエンティティでは、外部またはローカルエンティティキーごとに直列化してください。これは、そのキーに基づくキューのパーティション、慎重に設計された有効期限付き分散ロック、または短いトランザクション内の行ロックで実現できます。別の選択肢は楽観的制御です。保存済みバージョンが依然として期待値である場合にのみ更新し、競合を検出したら再試行します。

リモートサービスの呼び出し中にトランザクションを開いたままにしないでください。まず一貫した方法で状態を予約または読み取り、次に可能であれば冪等性キーを付けて呼び出しを実行し、最後に結果を記録します。プロセスが手順の途中で失敗しても、再試行では保留中の操作とすでに完了した操作を区別できなければなりません。

公開前の運用、可観測性、テスト

公開前の運用、可観測性、テスト — guía visual de DedicatedPHP

運用ダッシュボードでは、保留中のイベント、繰り返し失敗するイベント、古さにより無視されるイベント、署名により拒否されるイベント、シーケンスに欠番があるイベントの件数を表示すべきです。キューの滞留時間と、受信から適用までの時間も測定します。これらのシグナルにより、ずれが業務上の問題になる前に、劣化した統合を検出できます。

元のイベントと、パーサーまたはハンドラーの明示的なバージョンから開始する再処理の仕組みを保持してください。再処理は盲目的な実行を意味しません。対象範囲を制限し、誰が要求したかを記録し、同じ冪等性保証を有効なまま維持します。

チェックリスト

  • 同じイベントを、並行しても含めて複数回送信する。
  • 関連する確認より前に取消を配信する。
  • 古いイベントを、より高いバージョンのイベントの後まで遅延させる。
  • 欠番、未知のタイプ、切り詰められたペイロード、無効な署名を投入する。
  • 外部効果を作成した後、イベントを適用済みとマークする前にworkerが停止する状況をシミュレートする。
  • 同じエンティティに対する2つのworkerが不可能な遷移を生じさせないことを確認する。
  • 再処理が監査を維持し、効果を重複させないことを確認する。

堅牢な統合は、ネットワークに順序どおりの配信を強制しようとはしません。検証可能な各入力を保存し、冪等な業務ルールを適用し、存在する場合は論理的な順序を使用し、状態を安全に把握できない場合は照合するという、信頼できる境界を設計します。

これらのアイデアをあなたのプロジェクトに活用してみませんか?あなたのPHPプラットフォームについて話し合いましょう。
関連サービスを見る