Chrome を --remote-debugging-port 付きで常駐させ、定時の処理から chromium.connectOverCDP() でつないで使っています。ログイン状態を保ったまま、検索や投稿の確認を自動で回すためです。
この常駐ブラウザには、固まったら再起動する監視を付けていました。それでも、監視は「正常」と言い続けているのに、実際の処理だけが全部時間切れになる状態が起きました。この記事は、その見抜き方と直し方の手順です。
起きたこと
- 朝の定時処理(検索ページを27語ぶん開いて候補を集める)が、全語で
page.goto: Timeout 45000ms exceeded - 結果のファイルは空の
[](2バイト)で書き出され、処理としては正常終了 - 同じブラウザに別の処理がつなぐと、
connectOverCDP: Timeout 30000ms exceeded - その間ずっと、1時間ごとの監視は
PROBE_OK
手で調べると、about:blank は開けるのに、対象サイトの新しいタブだけが読み込みを始めない状態でした。DevTools の /json/list では、開いたタブの title が空のまま止まっていました。
# 新しいタブで対象サイトを開いて、30秒後に題が入っているかを見る
id=$(curl -s -X PUT "http://127.0.0.1:9444/json/new?https://x.com/home" | jq -r .id)
sleep 30
curl -s http://127.0.0.1:9444/json/list | jq -r ".[] | select(.id==\"$id\") | .title"
# → 空のまま(正常なら "Home / X" などが入る)
原因:監視の検査が、壊れ方を見ていなかった
元の監視は「接続して about:blank を1枚開いて閉じる」だけでした。
// 元の検査(これでは今回の固まり方を見抜けない)
const b = await chromium.connectOverCDP({ endpointURL: 'http://127.0.0.1:9444', timeout: 30000 });
const p = await b.contexts()[0].newPage();
await p.goto('about:blank', { timeout: 10000 });
await p.close();
about:blank はネットワークも描画もほぼ使いません。そのため、「ポートは開いている」「タブも作れる」ところまでしか確かめていませんでした。実際の処理が失敗していた「外のページの読み込み」は、一度も試していなかったわけです。
さらに、呼び出し側のやり直しも「結果のファイルが無いとき」だけでした。全部時間切れでも空の [] は書き出されるので、やり直しが走りませんでした。
手順1:検査で「実際に使うページ」を1枚開く
// engage_probe.mjs — 75秒以内に「接続 → about:blank → 実ページ」まで行けなければ rc=1
import { chromium } from 'playwright';
const t0 = Date.now();
const timer = setTimeout(() => { console.log('PROBE_TIMEOUT'); process.exit(1); }, 75000);
try {
const b = await chromium.connectOverCDP({ endpointURL: 'http://127.0.0.1:9444', timeout: 30000 });
const ctx = b.contexts()[0];
const p = await ctx.newPage();
await p.goto('about:blank', { timeout: 10000 });
// ここが本題: 処理が実際に使うサイトを開く。domcontentloaded まで来れば読み込みは始まっている
await p.goto('https://x.com/home', { waitUntil: 'domcontentloaded', timeout: 30000 });
await p.close();
clearTimeout(timer);
console.log(`PROBE_OK ${Date.now() - t0}ms`);
process.exit(0);
} catch (e) {
console.log('PROBE_FAIL ' + String(e).split('\n')[0]);
process.exit(1);
}
ポイントは3つです。
- 検査するページは、監視したい処理が実際に開くサイトにする(汎用の
example.comだと、サイト固有の固まり方を見逃す) -
browser.close()は呼ばない。connectOverCDPでつないだ常駐ブラウザごと落ちて、ログインが消えることがある - 全体にもタイマーを掛ける。
gotoのtimeoutだけだと、接続の段階で止まったときに戻ってこない
監視側は、この検査が2回続けて落ちたら、常駐ブラウザのプロセスを止めて起動し直します。
手順2:結果が「空」のときもやり直す
# 結果ファイルが無い、または 2 バイト以下(空の [])ならやり直す
& node collect.mjs $out @words *>> $log
if ((-not (Test-Path $out)) -or ((Get-Item $out).Length -le 2)) {
"[retry] no/empty output -> health check then retry once" | Out-File -Append -Encoding utf8 $log
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\engage_health.ps1 *>> $log # 固まっていれば再起動
Start-Sleep -Seconds 20
& node collect.mjs $out @words *>> $log
}
「0件」と「全部失敗した結果の0件」は、ファイルだけ見ると区別できません。本当に0件の日があるなら、件数ではなくログの時間切れの数でやり直すかを決めてもよいと思います。
手順3:接続そのものにも再試行を付ける
送信前の段階(まだ何も送っていない)なら、何回やり直しても副作用はありません。
let b;
for (let k = 1; ; k++) {
try { b = await chromium.connectOverCDP({ endpointURL: 'http://127.0.0.1:9444', timeout: 60000 }); break; }
catch (e) {
if (k >= 3) throw e;
console.error(`[retry] connectOverCDP ${k}/3: ${String(e).split('\n')[0]}`);
await new Promise(r => setTimeout(r, 20000));
}
}
逆に、送信ボタンを押した後の失敗は再試行しません。二重送信になるからです。
確かめ方
- 固まった状態で新しい検査を流し、
PROBE_FAILになることを見る(今回は再起動の前に1回確かめた) - 常駐ブラウザを起動し直した直後に、
PROBE_OKに戻ることを見る - 空の結果ファイルを置いて呼び出し側を流し、
[retry]の行がログに出ることを見る
まとめ
- 死活監視は「生きているか」ではなく、**「この処理に必要なことができるか」**を確かめる
-
about:blankは、ネットワークと描画の固まりを見抜けない - 「結果が空」も失敗の候補として扱い、やり直しの条件に入れる
- 再試行は、副作用が起きる前の段階にだけ付ける