watchdog自身が残した孤児プロセスを回収する
今回やったこと / この記事について
無人ジョブを時間制限付きで実行するrun_with_watchdogに、見張り役自身が残すsleepプロセスを回収する処理を入れた。元の問題は、監視対象のコマンドが先に終了した時、見張り用サブシェルだけを止めても、その子のsleepが上限時間まで残ることだった。孤児は標準出力・標準エラーのパイプを握るため、コマンド自体は終わっているのに、capture_output=Trueやパイプの利用側がEOFを待ち続ける。
監視対象と見張り役を分けて考える
時間制限は、対象プロセスが指定秒数を超えたらTERMを送り、猶予後にKILLへ進む構造になっている。問題は、見張り役が前景のsleepで待っていたことだった。対象が早く終了すると親側は見張り役を止めるが、プロセスグループの扱いによっては待ち時間の子だけが残る。監視を追加した時点で、監視プロセスの終了条件まで設計対象にしなければならない。
修正後は、見張り用のsleepを独立したプロセスとして起動し、そのPIDを保持する。見張り役がTERMを受けた時は、そのPIDが空でなければ先に止める。待機用プロセスの標準入出力は/dev/nullへリダイレクトし、回収できない場合にもパイプを保持しないようにした。監視対象がすでに終了していた場合は、見張り役も終了し、呼び出し側へ制御を戻す。
trapだけでは不十分だった
最初の修正では、見張り役にTERMのtrapを置き、trap内でsleepを止めていた。しかし、次の二つの競合が残った。
sleepをバックグラウンドで起動してPIDを変数へ代入する前にTERMが届くwaitへ入る直前にTERMが届き、シグナル配送とtrapの実行順序が競合する
実測では、繰り返し実行テストで孤児が9〜59個累積する回があり、3回中2回は呼び出し元プロセスへの制御復帰が猶予秒数ぶん遅延した。ログ解析の結果、監視役と呼び出し側の終了処理が同じプロセスを見ていなかったことによる競合だった。現在は見張りの子プロセスを同じプロセスグループで扱い、対象の終了後に監視役と待機プロセスをまとめて回収する。
回帰テストは遅延とプロセス数を見る
test_watchdog_orphan.pyでは、すぐ終了するコマンドを$(run_with_watchdog ...)とパイプ経由で実行し、上限秒数まで待たされないことを測る。さらに、時間切れでTERMを送るケース、TERMを無視してKILLへ進むケース、短い実行を繰り返すケースを分けて検査する。プロセス一覧からこのテストが作った秒数のsleepだけを識別し、別の実行が作ったsleepを誤って数えないようにしている。
| 検査 | 見ている失敗 |
|---|---|
| 即時終了+コマンド置換 | パイプを握る孤児 |
| 即時終了+パイプ | EOF待ちの遅延 |
| 時間切れ+TERM | 猶予用sleepの孤児 |
| TERM無視 | KILL後の残留 |
| 繰り返し実行 | 累積する孤児 |
試した環境・参照
- Bashの
run_with_watchdogとPythonのプロセス検査 - watchdogと孤児プロセス回収の回帰テスト
- 孤児プロセス回収の実装変更
やってみてわかったのは、ジョブ監視の完了条件は「対象が終わった」ではないことだ。監視役、待機タイマー、標準入出力のパイプがすべて解放されて初めて1回の実行が終わる。無人運用ではこの後始末を機能本体と同じ回帰テストに入れる必要がある。
更新履歴
- 2026-09-18: 初稿。watchdogの待機プロセスと回帰テストを整理。
訂正履歴
なし。