番犬のexit codeを健康状態と混ぜない — 実行成否と異常通知を分離する
今回やったこと
自動化の番犬が異常を見つけたとき、常に exit 1 を返す設計を見直した。launchd が見る終了コードは「番犬自身が点検を実行できたか」と「点検対象の世界に異常があるか」を同じ値で表していた。そのため、対象の異常が長く続くと、番犬そのものが失敗し続けているように見える。
必要なのは、異常を見逃さないことと、終了コードの意味を一つにすることの両立である。通常運用では異常を Discord へ通知し、手元や CI で厳密に落としたい場合だけ --strict-exit を付ける形に分けた。
二つの失敗を分ける
対象の状態に問題があるケースと、点検処理そのものが壊れているケースは別である。たとえばフィードが落ちている、サイトがダウンしている、安全弁の設定が期待と違う、といった状態は、番犬が正常に点検した結果として報告できる。一方、未知の媒体を渡した、セルフテストに失敗した、必要な設定を読めない、といった状態は点検不能なので、通常運用でも非0にする必要がある。
| 状態 | 通常実行 | --strict-exit |
|---|---|---|
| 対象に異常がある | 0、通知あり | 非0、通知あり |
| 対象が正常 | 0、通知なし | 0、通知なし |
| 番犬自身が点検不能 | 非0 | 非0 |
この表の「0」は健康宣言ではない。番犬が予定された点検を実行し、結果を通知できたという意味に限定する。監視対象の健康状態は通知本文や別の状態記録で扱う。終了コードをこの意味へ戻すことで、同じ異常が居座ったときも、launchd のジョブ失敗と本物の自動化失敗を区別できる。
フラグだけでなく分岐をテストする
実装に --strict-exit という文字列があるだけでは配線済みとは言えない。scripts/test_watchdog_exit_codes.py では、フラグの定義と、実際に戻り値が分岐していることを確認する。その上で、異常なフィード、ダウンしたサイト、安全弁のズレ、レーンの無音、計測異常、古いデプロイをそれぞれ模擬する。
python3 scripts/test_watchdog_exit_codes.py
各テストで確認する条件は三つある。既定では対象異常でも exit 0 になること、--strict-exit では非0になること、どちらの場合も Discord 通知が発生することだ。通知が消えてしまえば、終了コードを整理しただけで観測性を失う。
反対に、セルフテストの失敗や未知の対象は、--strict-exit が無くても非0であることをテストする。ここまで固定しないと、すべてを0にして launchd を静かにするだけの危険な変更へ戻ってしまう。
launchd側で厳密モードを有効にしない理由
通常の launchd plist に --strict-exit を渡すと、対象異常があるだけでジョブが恒久的な失敗に見える。そこでテストでは、各 plist が --strict-exit を渡していないことも検査する。厳密モードは開発者が回帰を止めたいとき、または CI が非0を必要とするときに明示的に使う。
この設計は、異常を軽く扱うためのものではない。異常を通知へ送り、番犬自身の故障は終了コードで残し、必要な場面だけ対象異常も非0にする。実行成否と判定結果を分離することで、監視の出口を別の監視対象にしないことが狙いである。
参照した実装はテストスクリプトと2つの監視スクリプトである。監視スクリプトで終了コードの意味が揃っていなかった問題と、既定0・strict時非0・通知ありの回帰条件を、テスト記録に基づいて整理した。
環境・バージョン: Pythonスクリプトとして実行し、M1 の launchd と手元・CI で同じ終了コード契約を検証する。テストは外部の実サービスを変更せず、依存関数を差し替えた模擬入力で判定する。
更新履歴
- 2026-09-02: 初稿。リポジトリ内の実装とテスト記録をもとに整理。
訂正履歴
(なし)