祭りは中止できたのに完了ボタンが赤い
目標
宇宙のお祭りの変更リクエストを受け取り、承認・実行・観測・報告・別の補償へつなぐ調整役を書きます。SQLの操作が正しくても、調整役が間違っていれば起きる事故を、実際のDBとCLIで確認します。
なぜ重要なのか
前の承認バージョン・補償・証拠報告のレッスンを終えてから進めてください。Pythonの関数・dict・list・例外・JSON・CLIの使い方が必要です。今回は、すでに学んだDBのコードをすべて書き直さず、提供される操作を組み合わせます。想定所要時間は140分なので、期限が切れる前に+時間で延長してください。最大180分で、セッションが終わるとファイルが消えます。必要なコードは別に保管してください。
環境と成果物
成果物は/root/change-capstone/coordinator.pyです。PostgreSQL 16・psycopg 3.2.3・Python 3がイメージに用意されていて、追加のインストール・ネットワーク・権限は必要ありません。受講生はpostgresユーザーとして/rootに書き込みます。
提供されるアダプターは、/opt/lab/fixtures/change_capstone/operations.pyのOperations(dsn)です。同じフォルダーのrevision_ops.py・compensation_ops.py・evidence_ops.pyは、前の3つの単元の累積した正解から生成したライブラリです。読んでみることができますが、今回の調整役の答えは含まれていません。採点ツールは、ローカルのlabdbのUUIDの一時スキーマに、仮想の注文・承認・監査・補償のテーブルを作り、自分が作ったスキーマだけを片付けます。渡したops・DSN・出力パスを使い、publicや運用DBは変更しません。
リクエストの正確な形式
すべてのリクエストは厳密なdictで、actionごとに、下のキーだけを許可します。
- preview: action, change_id, tenant, ids, report_path。
- apply: action, change_id, tenant, targets, approved, report_path。
- report: action, change_id, report_path。
- undo: action, change_id, undo_id, reason, approved。
actionは、該当する英語のstr 1つです。change_id・tenant・undo_idは、厳密なstrで、ASCIIの英数字・アンダースコア・ハイフンの1–64文字です。apply・undoのapprovedは、実際のTrueだけを許可し、1・文字列・Falseは拒否します。このフラグは、認証や署名ではなく、仮想の業務の入力の契約です。
idsは、厳密なint 1–2147483647を含む厳密なlistで、1–16個です。重複なしの昇順に正規化します。targetsは、id・revision・qtyの3つのキーだけを持つ厳密なdictのlistで、1–16個です。idの範囲は上と同じで、revisionは厳密なint 0–2147483646、qtyは厳密なint 1–1000です。boolを整数として受け取りません。IDの重複を禁止し、ID順のディープコピーを作ります。すべての関数は、元のリクエストを変更しません。
reasonは、1–200文字の厳密なstrで、前後の空白と、制御文字U+0000–001FおよびU+007Fを禁止します。report_pathは、厳密なstrの絶対パスで、既存の自分が所有するディレクトリの中の、通常のファイルの位置です。存在しないファイルは許可しますが、シンボリックリンク・ディレクトリ・相対パスは拒否し、親は自動で作りません。信頼できる、自分が所有するフォルダーを前提とし、親のパスを入れ替える攻撃全体を防ぐ契約ではありません。
提供される操作の契約
各呼び出しは、自分のDB接続を開いて片付けます。調整役が外側のトランザクションで包んだり、接続を別に閉じたりしません。値・ID・パスをハードコードしたり、SQLを再実装したりしません。
- ops.Conflict: 提供される操作の、承認・現在の条件の衝突の例外クラス。
- ops.preview(tenant,ids): 現在pendingの対象のid・revision・qtyのリスト。業務DBは変更しません。
- ops.apply(change_id,tenant,targets): 元の承認の条件で、キャンセルと監査をまとめて確定します。新しい適用はTrue、同じID・内容の再呼び出しはFalseです。別の承認や、現在の条件の不一致は、ops.Conflictです。監査・承認は、通常の実行経路では変更しない資料です。
- ops.observe(change_id): 存在しない承認はNone、それ以外はchange_id・tenant・targets・reportの4つのキーのdictです。targetsは正規化した元の承認で、reportは前の単元の証拠分析の結果です。読み取り専用の、一貫した観測を使い、破損したデータは例外で知らせます。
- ops.publish(change_id,report_path): 新しい証拠を収集し、報告のファイルを安全に置き換えて、SHA-256のstrを返します。業務DBは変更しません。一般の例外での、置き換えの前後のファイルの保全の契約は、前の証拠報告のレッスンと同じです。
- ops.undo(undo_id,change_id,reason): 元の監査と、現在のバージョンの条件が合っているときに、補償と監査をまとめて確定します。新しい適用はTrue、同一の補償はFalse、条件の衝突はops.Conflictです。元の承認と監査は削除しません。
採点では、各メソッドがExceptionを出したり、契約の範囲外の戻り値を返したりする代役も用意されます。検証済みのリクエストを渡したあとの、操作のエラーだけを、各段階の状態として分類します。KeyboardInterruptやSystemExitのようなBaseExceptionの全体を捕まえるという意味ではありません。ops.Conflictと、受講生のConflictは、別のクラスです。
調整の結果とCLIの契約
applyのdispatchの結果は、change_id・execution・observed・report・sha256の5つのキーです。executionは、applied・replayed・conflict・uncertainのどれかで、観測の結果で消したり変えたりしません。observedは、complete・incomplete・hold・unknown・mismatch・not_checkedです。reportは、saved・failed・not_attemptedで、保存の失敗・未試行では、sha256=Noneです。
preview dispatchは、approved=Falseのapplyの提案だけを返します。report dispatchは、change_idとreport関数の結果の3つのキー、undo dispatchは、compensateの結果の3つのキーを返します。dispatchで、リクエストを先に検証し、動作ごとの境界を守ってください。appliedでも、観測がunknownなら報告を試みず、replayedでも、観測がholdなら、その結果をそのまま報告することがあります。
decode(raw)は、厳密なbytesの1–65536個を、UTF-8のJSONとして解釈し、validateしたリクエストを返します。重複したキー・有限でない定数・途中で切れたJSON・誤ったUTF-8・大きすぎる入力は、ValueErrorです。main(argv,ops)は、リクエストファイルのパス1つだけを受け取り、最大65537バイトを読んで、decode→dispatchを実行します。成功したら、結果のJSONを1行でstdoutに出力して、0を返します。引数・読み取り・解釈・処理のExceptionなら、{"error":"request_failed"}を1行で出力して2を返し、DSN・元の例外・tracebackは出力しません。
スクリプトとして実行するときは、sys.pathに/opt/lab/fixtures/change_capstoneを追加して、operations.Operationsを読み込みます。環境変数LABHUB_CAPSTONE_DSNの、ローカル専用のDSNでOperationsを作り、main(sys.argv[1:],ops)の戻り値で終了してください。この環境変数は、正常なCLIの検証で必ず提供されます。モジュールとしてimportされたときに、CLIを実行してはいけません。CLIのコード0は、リクエストの処理の成功であり、業務の完了を意味しません。
観測と報告書の収集は、別々の呼び出しです。observedは前の観測、reportは後のファイルの保存状態で、2つの呼び出しの時点が同じだという保証はありません。savedだけで、ファイルの中の業務上の結論がcompleteだと仮定しません。報告書の実際の観測時刻と分類を、別に確認してください。
ステップ
- 明示した承認だけを入力として受け取ります。Conflict(Exception)と、validate(value)を実装してください。下の動作ごとの正確なキー・型・範囲を検査し、リストを並べ替えたディープコピーを返します。apply・undoは、approvedが実際のTrueのときだけ許可し、エラーはValueErrorです。
- プレビューを自動で承認しません。preview(value,ops)は、previewのリクエストを検証し、ops.preview(tenant,並べ替えたids)を1回呼びます。受け取ったtargetsを検証・並べ替え、IDのリストがリクエストと正確に同じかを照合します。違えばConflictです。同じなら、action=apply、元のchange_id・tenant・report_path、観測したtargets、approved=Falseの提案のdictを返します。
- 同じIDの別の承認を区別します。compare(value,observation)は、applyのリクエストを検証します。observationがNoneなら、unknownを返します。それ以外は、下の観測の形式で、change_id・tenant・正規化したtargetsがリクエストと同じかを照合し、違えばConflictです。同じなら、report.decisionのcomplete・incomplete・holdをそのまま返します。ほかの最上位のキーや、未知の判定は、Conflictです。
- 実行の応答と、結果不明を分けます。execute(value,ops)は、applyのリクエストを検証したあと、ops.apply(change_id,tenant,targets)を、正確に1回呼びます。Trueはapplied、Falseはreplayed、ops.Conflictはconflict、それ以外のExceptionや、boolではない戻り値は、uncertainです。呼び出しの外の検証エラーは、そのまま伝えます。
- 確定の記録を観測して、不確実性を明らかにします。settle(value,ops,outcome)は、applyのリクエストと、4つの実行結果の値を検証します。conflictならobserveなしで、observed=not_checkedです。それ以外は、ops.observe(change_id)を1回呼び、compareでobservedを決めます。参照のExceptionはunknown、比較のConflict・ValueError・KeyError・TypeErrorはmismatchです。change_id・execution=元のoutcome・observedのdictを返します。
- 業務に触れずに、報告を再生成します。report(value,ops)は、applyまたはreportのリクエストだけを許可します。ops.publish(change_id,report_path)を1回呼んで、小文字の64桁のSHA-256のstrなら、report=saved・sha256=その値、呼び出しのExceptionや、誤った戻り値なら、report=failed・sha256=Noneのdictを返します。
- 別に承認した補償だけを実行します。compensate(value,ops)は、undoのリクエストを検証したあと、ops.undo(undo_id,change_id,reason)を1回呼びます。True・False・ops.Conflict・その他のException/戻り値を、それぞれapplied・replayed・conflict・uncertainに分類します。change_id・undo_id・compensationのdictを返し、後続の呼び出しはしません。
- 実際のJSONリクエストとCLIを、最後までつなぎます。dispatch(value,ops)・decode(raw)・main(argv,ops)と、CLIのエントリーポイントを、下の契約で実装します。applyは、execute→settleのあとで、complete・incomplete・holdの観測のときだけ、reportを呼びます。それ以外は、report=not_attempted・sha256=Noneです。ほかの3つの動作は、該当する関数だけを呼びます。実際のDB・ファイル・CLIで、全体の流れを検査します。
参考
- 累積の直接の診断: python3 -B /opt/lab/fixtures/change_capstone/check.py 8 /root/change-capstone/coordinator.py。数字を現在のステップに変えてください。1回の採点は40秒が上限です。
- reportモードでapply・undoが呼ばれていないか、previewがTrueの承認を作り出していないか、同じIDの別の内容を許可していないかを、実際の呼び出しの記録で確認します。
- 最終ステップは、別の書き込み接続で2つ目の注文を変更し、古い承認の全体のロールバックを確認します。コミットのあとの応答の消失と、報告の置き換えの前の例外を同時に注入し、注文・監査・ファイルをそれぞれ確認します。
- 一般の例外の注入と、実際のDB・CLIを検証するもので、実際のネットワークの断絶・DBサーバーの終了・電源障害への耐久性は証明しません。提供されるDBは学習用で、fsyncとfull_page_writesがoffです。
- このレッスンは、ユーザー認証・他の顧客へのアクセス権限・電子的な承認の署名・外部への送信を提供しません。実際の個人情報や運用のDSNを入れないでください。uncertainな補償は、前のレッスンのレシートと現在の状態の確認につなげ、別のIDで自動の再試行はしません。
明示した承認だけを入力として受け取る
Conflict(Exception)と、validate(value)を実装してください。下の動作ごとの正確なキー・型・範囲を検査し、リストを並べ替えたディープコピーを返してください。apply・undoは、approvedが実際のTrueのときだけ許可し、エラーはValueErrorにしてください。
boolはintのサブタイプです。真偽値があるということと、明示的な承認は別のことです。
プレビューを自動で承認しない
preview(value,ops)は、previewのリクエストを検証し、ops.preview(tenant,並べ替えたids)を1回呼んでください。受け取ったtargetsを検証・並べ替え、IDのリストがリクエストと正確に同じかを照合してください。違えばConflictです。同じなら、action=apply、元のchange_id・tenant・report_path、観測したtargets、approved=Falseの提案のdictを返してください。
提案は、validateでそのまま実行できてはいけません。実行・補償・報告の操作を、ここで呼ばないでください。
同じIDの別の承認を区別する
compare(value,observation)は、applyのリクエストを検証してください。observationがNoneなら、unknownを返してください。それ以外は、下の観測の形式で、change_id・tenant・正規化したtargetsがリクエストと同じかを照合し、違えばConflictにしてください。同じなら、report.decisionのcomplete・incomplete・holdをそのまま返してください。ほかの最上位のキーや、未知の判定は、Conflictにしてください。
提供されるobserveは、前の単元の証拠の収集・分類を行います。今回の責任は、その観測が現在の承認と同じかどうかを確認することです。
実行の応答と、結果不明を分ける
execute(value,ops)は、applyのリクエストを検証したあと、ops.apply(change_id,tenant,targets)を、正確に1回呼んでください。Trueはapplied、Falseはreplayed、ops.Conflictはconflict、それ以外のExceptionや、boolではない戻り値は、uncertainにしてください。呼び出しの外の検証エラーは、そのまま伝えてください。
例外を受け取っても、コミットされている可能性があります。新しいID・新しい目標・自動の再試行を作らないでください。
確定の記録を観測して、不確実性を明らかにする
settle(value,ops,outcome)は、applyのリクエストと、4つの実行結果の値を検証してください。conflictならobserveなしで、observed=not_checkedにしてください。それ以外は、ops.observe(change_id)を1回呼び、compareでobservedを決めてください。参照のExceptionはunknown、比較のConflict・ValueError・KeyError・TypeErrorはmismatchにしてください。change_id・execution=元のoutcome・observedのdictを返してください。
参照の失敗と、別の承認だという事実を、同じ状態にまとめないでください。このステップは、書き込みの操作をしません。
業務に触れずに、報告を再生成する
report(value,ops)は、applyまたはreportのリクエストだけを許可してください。ops.publish(change_id,report_path)を1回呼んで、小文字の64桁のSHA-256のstrなら、report=saved・sha256=その値、呼び出しのExceptionや、誤った戻り値なら、report=failed・sha256=Noneのdictを返してください。
実行の応答を解釈し直したり、補償したりしないでください。保存に失敗したあとも、業務の操作を追加で呼ばないでください。
別に承認した補償だけを実行する
compensate(value,ops)は、undoのリクエストを検証したあと、ops.undo(undo_id,change_id,reason)を1回呼んでください。True・False・ops.Conflict・その他のException/戻り値を、それぞれapplied・replayed・conflict・uncertainに分類してください。change_id・undo_id・compensationのdictを返し、後続の呼び出しはしないでください。
元の承認は、補償の承認まで意味しません。補償の例外も、新しいundo_idで自動の再試行をしないでください。
実際のJSONリクエストとCLIを、最後までつなぐ
dispatch(value,ops)・decode(raw)・main(argv,ops)と、CLIのエントリーポイントを、下の契約で実装してください。applyは、execute→settleのあとで、complete・incomplete・holdの観測のときだけ、reportを呼んでください。それ以外は、report=not_attempted・sha256=Noneです。ほかの3つの動作は、該当する関数だけを呼んでください。実際のDB・ファイル・CLIで、全体の流れを検査します。
終了コード0は、業務の完了ではなく、リクエストの処理の成功です。JSONの実行・観測・ファイルの状態を、別々に読んでください。