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

副作用を重複させずにPHPの失敗プロセスを復旧する方法

安全な地点からPHPのフローを再開し、副作用を補償する方法と、決済・発送・記録を重複させない運用上の境界を解説します。

状態を永続化したPHPプロセスの手順、制御された再試行、結果が曖昧な場合の人手による確認を示す図

複数の手順からなるプロセスが失敗したとき、最初から再実行すると、すでに発生した副作用が繰り返されることがあります。注文を二重に作成したり、通知を二重に送信したり、同じレコードを二度インポートしたりする可能性があります。PHPで失敗したプロセスを部分的に復旧するには、どの手順が確定済みか、どの手順なら安全に繰り返せるか、どの手順に補償処理や人手による確認が必要かを判断します。

判断は例外が発生した場所だけでなく、各操作の意味に基づいて行います。たとえば、APIからの応答が失われても、外部システムがリクエストを拒否したとは限りません。処理自体は完了していて、PHPが結果を受け取る前に接続が切れた可能性があります。このケースを想定して設計すれば、中断された実行を、実行されなかったものと取り違えずに済みます。

再試行・再開・補償を選択する

再試行・再開・補償を選択する — guía visual de DedicatedPHP

全体の再試行では、すべての手順を再実行します。フロー全体が冪等(繰り返しても最終状態が変わらない)である場合、または外部への副作用がまだ発生していない場合に適しています。どちらの条件も保証できないなら、確認せずに繰り返すのは危険です。

再開とは、未確定の最初の手順から処理を続けることです。そのためには、各手順の状態と結果を記録し、結果が曖昧な呼び出しの影響を取得または検証できる必要があります。完了したように見える手順をすべて飛ばすこととは異なり、永続化された証拠が必要です。

補償とは、予約のキャンセルのように、以前の影響を相殺する操作を実行することです。必ずしも元の状態を完全に復元できるわけではありません。送信済みの通知は取り消せず、売上確定済みの決済には、独自の期限や記録を伴う返金が必要になることがあります。そのため、補償は明示的な業務操作であり、データベースの自動ロールバックではありません。

状態と永続化された結果で手順をモデル化する

フローをシーケンスまたはステートマシンとして表し、各手順に安定した名前、識別可能な入力、永続化された結果を持たせます。初期モデルには、pending、running、succeeded、retryable、failed、manual_reviewなどの状態を含められます。許可する状態遷移を定義し、必要な証拠を保存せずにプロセスが最終状態へ移行しないようにします。

プロセスごとのレコードには、安定した識別子、フローの種類、全体の状態、プロセス定義のバージョン、開始日時と更新日時、試行回数、直近の遷移理由などを含められます。各手順には、状態、操作ID、タイムスタンプ、関連する結果への参照を保存します。再開または結果の説明に必要な情報だけを永続化し、APIの応答全体やシークレットを無差別にコピーしないでください。

PHPでは、コーディネーターが状態遷移と手順の実行を分離できます。複数のworkerが同じジョブを取得し得る場合、更新はアトミックに行う必要があります。トランザクションまたは適切なロック機構を使い、誰がプロセスを取得したか、ロックがいつまで有効かを記録します。期限付きロックを使えば、放棄されたジョブを回収しつつ、途中で止まった手順を完了済みと誤認せずに済みます。

「厳密に1回」を前提とせずチェックポイントを設定する

システムが確実に確認できる結果ごとにチェックポイントを保存します。ローカル操作の場合、業務上の変更と手順の状態を一緒に保存するトランザクションが使えます。外部呼び出しでは、データベースとプロバイダー間で共通のトランザクションは存在しません。プロバイダーが処理した後、PHPが応答を記録する前にプロセスが停止する可能性があります。

この境界では、プロバイダーが対応している場合、プロセスと手順の安定した識別子から冪等性キーを生成して使用します。対応していなければ、リクエストを繰り返す前に、操作IDを使ってリモート側の状態を照会します。冪等性も信頼できる照会手段もない場合は、結果を曖昧なものとして扱い、確認に回します。タイムアウトだけを根拠に、操作が発生しなかったと結論づけてはいけません。

非同期タスクでは、トランザクショナル・アウトボックス(outbox)パターンを使うと、ローカルの変更と未送信メッセージを同一トランザクション内に保存できます。その後workerがメッセージを配信します。処理済みIDを保存するなど、コンシューマー側も重複に対応する必要があります。こうした仕組みは不整合を減らしますが、分散インテグレーション全体を自動的にアトミックな操作にするものではありません。

再実行と補償の境界を定める

手順ごとに、一時的なエラー、恒久的なエラー、結果が不明な状態を定義します。一時的な障害は、待機時間を段階的に延ばし、ランダムな揺らぎを加えた再試行が可能な場合があります。最大試行回数と総所要時間を設定してください。検証や権限のエラーは、再試行しても通常は改善しません。フローを停止し、原因を修正したうえで再実行の可否を判断します。

外部への影響ごとに、繰り返し、照会、補償、またはロールバックの可否を文書化します。操作が有効で、繰り返すほうが部分状態を維持するより有害な場合は、その結果を保持します。安全で承認済みの業務操作がある場合に限り補償してください。何が起きたかデータから判断できない場合、補償にも失敗した場合、または顧客・財務・法務への影響に承認が必要な場合は、処理を止めてエスカレーションします。

補償ポリシーには、順序、条件、担当者、期待される結果を明記します。履歴を削除するのではなく、元の影響に紐づく新しい手順として補償を記録します。これにより、運用担当者は、未実行の操作、実行済みの操作、補償済みの操作を区別できます。

運用担当者が判断するための制御と情報を用意する

コンソールまたは運用手順では、全体と各手順の状態、分類済みの直近のエラー、試行回数、外部参照、許可される操作を表示します。「すべて再試行」のような汎用ボタンは避け、冪等な手順の再試行、リモート状態の照会、補償の実行、エスカレーションなど、範囲を限定した選択肢を提示します。

これらの操作はロールベースの認可で保護し、影響の大きい操作には追加確認を求めます。また、誰が、いつ、何を選び、なぜ実行したかを記録します。再実行時に入力データを変更する場合は、過去のプロセスの入力を暗黙に変更せず、新しい実行を作成するか、明示的な再確認を必須にします。

機密情報を漏らさずに診断できるよう、相関ID、エラーコード、プロセスのバージョン、ソースシステムを照会するために必要な参照情報を保持します。トークン、個人データ、完全なペイロードはマスキングしてください。記録の保持期間と閲覧できる担当者も定義します。有用なトレースは、業務データを不必要に複製することなく、何が起きたかを説明できるものです。

障害をテストし、復旧を段階的に導入する

具体的な地点での中断をテストします。手順の実行前、プロバイダーが処理した後で応答を保存する前、補償の実行中、2つのworkerが同じプロセスを取得しようとする状況などです。各ケースの後で状態に矛盾がないこと、副作用が重複しないこと、人手による操作が監査記録に残ることを確認します。

曖昧な応答、重複する冪等性キー、不正なデータ、再試行上限、フローのバージョン変更に関するテストも含めます。依存先をシミュレートした統合テストでは、制御された障害を再現できます。実際のプロバイダーが異なる挙動をする場合は、適切な環境で契約内容と照会機構も検証してください。

既存のフローでは、まず手順を可逆性と冪等性で分類します。次に、限定した段階の状態を永続化し、リスクの高い障害から復旧処理を実装して、対象範囲を広げる前に未解決ケースを観察します。導入を容易にするために過去の記録を削除したりリセットしたりしてはいけません。追跡可能性を維持し、以前のバージョンで作成されたプロセスの解釈方法を定義します。

復旧を導入するためのチェックリスト

復旧を導入するためのチェックリスト — guía visual de DedicatedPHP
  • 各手順に識別可能な入力、永続化された状態、検証可能な結果があるか。
  • どの呼び出しが冪等か、結果が曖昧なときに何をするかが明確か。
  • 試行回数、期限、エラー分類の上限があるか。
  • 補償が、監査記録と担当者を伴う業務操作として定義されているか。
  • 運用担当者が適切な権限で照会・操作でき、不要なデータにアクセスしないか。
  • 手順間の障害、並行実行、再実行、補償の失敗をテストしたか。
  • 自動で安全に再開できない場合のエスカレーション手順があるか。

実務上の基準は、次の手順を判断するのに十分な証拠を保持し、証拠が足りない場合は自動化を停止することです。安全な復旧は、障害を隠そうとするものではありません。何が完了し、何が保留で、誰が解決できるのかを明確にします。

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