プロンプトとコンテキスト
最大20ステップからなる顧客定義プロセス向けのワークフローエンジンを設計してください。そのうち1ステップは人間による承認を必要とし、数日間待機する可能性があります。アクティビティは失敗したり、重複配信されたり、レスポンスが失われた状態で成功したりすることがあります。システムは障害から回復し、タイムアウト、キャンセル、監査、バージョンアップグレードをサポートする必要があります。状態モデル、スケジューリング、冪等性、コールバックのセキュリティ、スケーリング、トレードオフについて説明してください。
このシステムデザインの設問は、バックエンド、プラットフォーム、インフラストラクチャのロールに適しています。焦点は単なるキューの描画ではなく、高耐久な実行セマンティクス(durable execution semantics)にあります。20ステップの上限や数日間の待機は面接上の前提条件です。まずはスループット、レイテンシ、テナント分離、データの機密性、復旧目標(RTO/RPO)、保持義務を明確にしてください。
面接官が見ているポイント
面接官は、ワークフローの状態、アクティビティの実行、外部の副作用が適切に分離されているか、アクティビティが少なくとも1回(at-least-once)実行され得ることを明示的に認識しているか、ビジネスインバリアントに紐づく冪等性キーが定義されているかを確認しています。人間による承認には、偽造不可で有効期限付きの使い捨てクレデンシャルが必要です。また、回答にはイベント履歴、スナップショット、タイマー、キャンセル、バージョン管理、運用時の修復についても定義を含める必要があります。
最初に明確にすべき質問
- テナントごとの並行性、ワークフロー開始レート、最大待機時間はどのくらいか?
- アクティビティは関数、コンテナ、外部HTTPサービスのいずれか。また、どの副作用が不可逆か?
- 承認者はどのように認証、認可、差し替えされるか?クォーラム(定足数)承認は必要か?
- リトライによってアクティビティが再実行される可能性はあるか?また、ビジネスサービスは冪等性キーを受け入れ可能か?
- 定義はどのように公開、固定(freeze)、移行されるか?実行中のインスタンスは新しいバージョンに従うか?
- どの監査フィールドを保持する必要があり、誰がそれらを閲覧・変更できるか?
- キャンセル、一時停止、手動リトライ、ステップのスキップは通常のプロダクト運用に含まれるか?
30秒の回答フレームワーク
「ワークフローを高耐久なステートマシンとしてモデル化します。定義バージョン、実行(Run)、アクティビティの試行、イベントログ、現在のスナップショットを個別に保存します。スケジューラーはリトライ可能なアクティビティタスクを配信し、ワーカーはアクティビティID、試行回数、冪等性キーとともに結果を報告し、ステートマシンが重複を排除します。人間の承認には、実行、ステップ、テナント、承認バージョンにバインドされた有効期間の短い使い捨てトークンを発行します。タイマー、タイムアウト、キャンセルは永続化されたイベントとして扱います。パーティション化されたキューとテナントクォータでスケールさせ、リプレイ、監査、障害注入によって復旧を検証します。」
ステップごとの詳細回答
まず実行モデルを定義します。ワークフロー定義は、ステップタイプ、入力マッピング、タイムアウト、リトライポリシー、補償ルールを含むイミュータブルなバージョンです。実行インスタンスは1つの固定されたバージョンを参照するため、新しい定義を公開しても履歴が勝手に書き換わることはありません。追記専用(append-only)のイベントログから状態を再構築し、スナップショットを使用して読み取りを高速化します。イベントの追記にはシーケンスまたはバージョンの条件が必要であり、同時書き込みによって互いが上書きされるのを防ぎます。
データベーストランザクション内で外部サービスを呼び出してはいけません。ActivityScheduled イベントをコミットし、スケジューラーがタスクをキューに入れ、ワーカーがリース(lease)を取得してサービスを呼び出し、その後に ActivitySucceeded、ActivityFailed、または ActivityTimedOut をコミットします。期待される実行、ステップ、試行に対する結果のみを受け入れます。遅延した結果や重複した結果は監査の証拠として記録し、状態遷移としては扱いません。
最小限の状態モデルは以下の通りです:
| Entity | Key fields | Purpose |
|---|---|---|
| DefinitionVersion | tenant, definition, version, digest | Freeze steps and policies |
| WorkflowRun | run, definitionVersion, status, sequence | Track an instance |
| Event | run, sequence, type, payload, createdAt | Facts and replay |
| ActivityAttempt | step, attempt, lease, idempotencyKey, status | Delivery, lease, result |
| Approval | step, tokenHash, approver, expiresAt, status | Human approval credential |
| Timer | run, step, fireAt, generation, status | Wakeups, delays, timeouts |
配信は少なくとも1回(at-least-once)行われます。キューの確認応答(ack)はビジネス上の完了を意味しません。メッセージには実行ID、ステップID、試行回数、定義バージョン、冪等性キーが含まれます。リースにより2つのコンシューマが同時に作業するのを防ぎ、フェンシングトークンや条件付き書き込みによって古いワーカーを拒否します。リースの期限切れにより再配信が可能になりますが、外部の副作用が重複するかどうかはアクティビティの規約(contract)に依存します。
複数のレイヤーで冪等性を処理します。ステートマシンのイベントには一意の (run, sequence) キーを使用します。アクティビティの結果には (run, step, idempotencyKey) を使用します。決済、メール送信、ビジネスサービスへの書き込みは、同じキーを受け入れるか、ビジネス上の一意性制約を強制する必要があります。呼び出しが成功したにもかかわらずレスポンスが失われた場合、リトライによって完了済みの結果または安全な重複応答を返す必要があります。エンドツーエンドで厳密に1回(exactly-once)であると主張してはいけません。非冪等な副作用については、人手による確認、補償トランザクション、またはリトライなしポリシーを使用します。
承認用URL自体は認可ではありません。高エントロピーのランダムデータを生成し、そのハッシュのみを保存し、トークンをテナント、実行、ステップ、アクション、有効期限にバインドします。コールバック時には、トークン、署名またはログインアイデンティティ、CSRF保護、1回限り使用可能な状態、現在のワークフローステップを検証します。送信時にも承認者の認可を再チェックします。承認および却下は改ざん防止の監査イベントとなり、期限切れのコールバックは無効化されます。
タイマーを永続化します。タイムアウトまたは待機を作成する際に fireAt と世代(generation)を保存します。スケジューラーは時間インデックスまたはバケットをスキャンし、条件付き更新によって期限の到来したタイマーを要求(クレーム)し、世代および状態の条件によって重複実行を排除します。再起動時はストレージから復旧します。承認の完了やキャンセルによって古いタイマーは無効としてマークされます。メモリ内でのスリープ(sleep)によってワーカーを数日間ブロックさせてはいけません。
キャンセル、一時停止、修復は、ステートマシンの明示的なコマンドです。キャンセルは意図を記録し、まだ開始されていない作業を防ぎます。処理中の外部副作用を魔法のように元に戻すことはできないため、その結果を待つか補償を実行します。管理者によるスキップ、リトライ、入力の編集には、認可、理由、古い状態、新しい状態、監査イベントが必要です。スナップショットを直接編集してはいけません。リプレイ時に異なる結果が生成されてしまいます。
実行ごとにバージョンを分離します。新しい定義には新しいダイジェストが付与され、既存の実行はデフォルトで古いバージョンを保持します。移行を行う場合は、互換性のある状態と入力を定義し、必要な承認を取得した上で、両方のバージョンを含む WorkflowMigrated イベントを追記する必要があります。ワーカーは自身が実行する定義バージョンのみを受け入れ、古いタスクが誤って新しいステートマシンを進めてしまうのを防ぎます。
テナントまたは実行IDでパーティショニングしてスケールさせます。アクセス頻度の高いテナント(hot tenant)には並行性クォータを設定し、アクティビティのタイプと優先度ごとにワーカープールを分離します。イベントをパーティション化されたストレージに追記し、スナップショットとインデックスをオンラインデータベースに保持し、機密ペイロードを暗号化して保持期間を適用します。バックプレッシャー、デッドレター、リースメトリクス、レート制限を追加し、1つの制御不能なワークフローがシステム全体のキャパシティを消費しないようにします。
オブザーバビリティはランタイムとビジネス運用の両方に役立つ必要があります。現在のステップ、待機理由、試行回数、キュー遅延、タイマー遅延、承認滞留時間、リトライ増幅、補償結果、バージョンを記録します。トレースには実行、ステップ、試行の識別子を含めることができますが、機密データを含む入力は管理された監査ストレージに保持します。運用画面では次のアクションを説明できるようにすべきであり、「待機中」は「失敗」ではありません。
障害注入テストを実施します:イベントコミット後のクラッシュ、重複メッセージ、リースの期限切れ、レスポンスが失われたアクティビティの成功、承認のリプレイ、重複タイマー、データベース障害、キューの滞留、中断されたバージョンデプロイ、テナントクォータの枯渇。観察可能な結果を定義します:古い状態に退行しないこと、事前の規約なしに副作用が繰り返されないこと、トークンが2回消費されないこと、最終的に復旧が継続するか明示的な人手による対応状態に入ること。
質の高い模範回答
「ワーカーにワークフローの状態を記憶させるのではなく、高耐久なステートマシンを構築します。定義バージョンはイミュータブルであり、各実行は特定のバージョンに固定されます。イベントログが事実の唯一のソース(source of truth)であり、スナップショットは読み取りを高速化します。ステートマシンはスケジューリングイベントをコミットし、キューは少なくとも1回配信し、ワーカーは実行、ステップ、試行、冪等性キーを報告します。重複した結果や古い結果は監査エントリとして扱われ、不正な状態遷移にはなりません。
承認時には、テナント、実行、ステップ、アクションにバインドされた使い捨てトークンが作成されます。そのハッシュのみが保存され、有効期限が設定されます。コールバックはアイデンティティ、権限、CSRF、トークンの状態、現在のステップを検証し、条件付きでトークンを消費して監査イベントを追記します。承認、却下、タイムアウト、キャンセルはすべてイベントであり、スナップショットの直接編集ではありません。
タイマーは fireAt と世代を永続化します。バケット化されたスケジューラーが条件付き書き込みでそれらをクレームします。再起動時に復旧され、重複した起動は排除されます。リースによりワーカーの重複を防ぎ、フェンシングトークンが古い書き込みを拒否します。ダウンストリーム側で証明された冪等性規約がない決済やメール送信は、厳密に1回にはできません。ダウンストリームキー、一意性制約、補償トランザクション、または人手による処理を使用します。
実行中のプロセスが暗黙的に新しい定義に従うことはありません。移行時には両方のバージョンを含むイベントを追記します。テナントと実行でパーティションを分割し、hot tenantのクォータを分離し、アクティビティタイプごとにワーカープールを使用します。追記専用イベント、オンラインスナップショット、暗号化された監査データ、保持ポリシーを階層化します。障害注入では、クラッシュ、再配信、失われたレスポンス、コールバックのリプレイ、重複タイマー、中断されたバージョンデプロイを網羅する必要があります。」
よくある間違い
- キューの確認応答(ack)を完了と見なす → ワーカーが処理後にクラッシュし、メッセージを再度受信する可能性がある → 永続化された状態と冪等な結果で重複を排除する。
- 厳密に1回(exactly-once)を主張する → ローカルトランザクションでは分散環境の副作用を保証できない → 少なくとも1回の配信を前提とし、ダウンストリームの冪等性、補償、または人手処理を明記する。
- 承認URLをそのまま認可と見なす → 漏洩やリプレイによって不正アクセスを許す可能性がある → アイデンティティ、テナント、ステップ、アクション、有効期限、使い捨て状態をバインドする。
- メモリ内で数日間スリープさせる → 再起動やスケーリング時に待機状態が失われる → タイマーを永続化し、スケジューラーを通じて起床させる。
- スナップショットを直接編集する → リプレイ時に異なる結果が生成されてしまう → 認可されたステートマシンコマンドと監査イベントを使用する。
- 全テナントで1つのキューを共有する → hot tenantが他のリソースを枯渇させる → パーティション化、クォータ設定、優先順位付け、バックプレッシャーの適用を行う。
- 新しい定義が古い実行に勝手に影響を与える → 実行中のワークフローの挙動が説明不能になる → バージョンを固定し、移行を明示的に行う。
フォローアップの質問と回答
フォローアップ1:アクティビティは成功したが、ワーカーが結果を書き込む前にクラッシュしました。リトライによって2重請求が発生しますか?
ダウンストリームサービスが冪等性キーをサポートしている場合は、同じキーでリトライして完了済みの結果をマッピングします。そうでない場合は、盲目的にリトライしてはいけません。ダウンストリームの状態を問い合わせるか、人手による処理に移行するか、補償を行います。ステートマシンは自身のイベントの一貫性を保つことはできますが、単体で決済の厳密に1回(exactly-once)のセマンティクスを作成することはできません。
フォローアップ2:承認者が「承認」を2回クリックした場合はどうなりますか?
pending から approved への条件付き更新または一意性制約を用いて、1回のみ成功するトークン消費を許可します。2回目のリクエストは処理済みまたは期限切れとして返され、ワークフローを進めることはありません。両方のリクエストとアイデンティティは監査証跡に残ります。
フォローアップ3:「任意の2名の承認者」をサポートするにはどうすればよいですか?
承認インボックスとポリシーバージョンを永続化します。認可スナップショットを含め、承認者ごとに重複排除された決定を1つ記録します。クォーラムに達した場合にのみ進めます。却下、取り消し、承認者の差し替えは明示的なコマンドとして扱います。
フォローアップ4:オペレーターが至急ステップをスキップする必要があります。安全な手順は何ですか?
まず認可されたロール、スキップ可能なステップ、安全条件を定義します。古い状態、新しい状態、オペレーター、承認を含む、理由付きの StepSkipped コマンドを発行します。スキップがビジネスインバリアントに違反する場合は拒否し、データベースを直接編集するのではなく、補償または終了を提供します。
フォローアップ5:テナントが無制限にワークフローを作成するのを防ぐにはどうすればよいですか?
テナントごとにステップ数、分岐の深さ、アクティビティの並行性、履歴サイズ、タイマー数、合計実行時間を制限します。定義を静的に検証し、実行を計測します。クォータを超過した場合は、無制限な肥大化を許すのではなく、オペレーターが確認可能な理由とともに作業を一時停止または拒否します。