watchdogの間欠失敗を、テストではなく停止処理の競合として直す

  • #テスト
  • #watchdog
  • #プロセス管理
  • #CI
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: 初稿を作成。