watchdogの間欠失敗を、テストではなく停止処理の競合として直す
今回やったこと
watchdogの孤児プロセスを検出するテストがCIで間欠的に失敗した問題について、テストの待ち時間を調整するのではなく、停止処理そのものにある競合を修正した記録を整理する。対象は scripts/lib/agent_watchdog.sh と scripts/test_agent_watchdog_orphan.py である。負荷下の比較では、旧方式を300回試したとき孤児が225件、本番修正方式では孤児・ハングがともに0件だった。
実装ステータス
修正と負荷下の反復検証済み。この記事の数値は比較結果であり、リポジトリ内のテストコードから独立に再実行した結果ではない。
間欠失敗を「flaky」で片づけない
テストが時々落ちると、まずテスト環境の揺れを疑いたくなる。しかし再現頻度が低いことは、テスト対象が正しいことの証明にはならない。今回の検証では、原因はwatchdog自身がTERMを処理してsleep中の子プロセスを止める方式の競合だった。停止シグナルの処理と子プロセスの終了タイミングが重なると、親側が終わったように見えても子が残る可能性がある。
手元で再現しにくい問題は、単発実行では比較しにくい。比較記録では負荷をかけて反復し、旧方式で300回中225件の孤児を確認した。修正版では同じ比較で孤児とハングが0件だった。この対照は「テストがたまたま安定した」ではなく、停止方式の変更が問題に効いたかを見るための材料になる。ただし、あらゆるOSや負荷条件で発生率がゼロだと一般化する数字ではない。
停止の担当を呼び出し側へ移す
修正では、watchdog自身にTERMを処理させて子を止める設計から、呼び出し側が停止手順を管理する形に変えた。呼び出し側はまずwatchdogをSTOPし、その後に子プロセスとwatchdogをKILLして回収する。停止中にwatchdogが自分の子を処理するタイミングへ依存しないよう、外側で順序を固定する狙いがある。
呼び出し側
1. watchdogをSTOP
2. 子プロセスをKILL
3. watchdogをKILLして回収
テスト側にも別の問題があった。同時に複数のテストが走ると、あるテストが起動したsleepを別テストのプロセスとして誤認する可能性があった。修正では識別子を分け、各テストが自分の対象を見分けられるようにした。対象コードの修正だけでなく、テスト自身の誤判定も抑える変更である。
失敗理由を最後まで見える形にする
さらに、テストランナーが失敗理由を末尾3行だけに縮めていたため、原因を示すNG行がそこに含まれなければ、ログから何が壊れたか判断しづらかった。修正ではNG行を順序を保ったまま出すようにしている。これは競合を直接なくす処理ではないが、次に失敗したときの診断材料を減らさないための変更だ。
| 変更箇所 | 目的 |
|---|---|
| 呼び出し側のSTOP・KILL・回収 | 子の停止をwatchdog内の競合に任せない |
| テスト対象の識別子 | 同時実行テスト間の誤認を防ぐ |
| NG行の出力 | 失敗理由をログで追えるようにする |
間欠失敗への対応では、待ち時間を延ばして症状を隠す前に、終了処理の所有者とプロセスの親子関係を確認する。再現を反復し、変更前後を同じ条件で比べる。テストコードが別ジョブのプロセスを見ていないか、失敗出力が診断に足りているかも合わせて点検する。今回の事例が示すのは、テストの不安定さとして見える症状が、実運用の停止競合を知らせている場合があるということだ。
更新履歴
- 2026-09-28: 初稿を作成。