2026-09-08 · macos · launchd · shellscript · 個人開発 · devops
動いているのに一度も走らない定期実行 — 同じ仕組みを3回作り直した記録
記事を1日1本公開する仕組みがあります。 4日のあいだに、その定期実行の部分だけを3回作り直しました。
3回とも、前の版では見えなかった本物の失敗を直しています。
そしてどの版も、私が与えたテストには通っていました。 テストがいつも「いま動くか」で、「置かれる場所で動くか」ではなかったからです。
第1版: /tmp に置いたループ
nohup bash /tmp/daily-loop.sh &
動きました。公開もできました。予約が4件、これに乗っていました。
そしてマシンを再起動したら、4件とも消えました。
/tmp はプログラムの置き場ではありませんし、nohup は再起動を越えません。
どちらも知っていて、それでもこうしました。
私が確かめたかったのは公開のロジックで、公開のロジックには問題が無かったからです。
消えたことは、どこにも出ませんでした。 翌朝、新しい記事が無いだけ。エラーもありません。 エラーを記録するはずのプロセスが、そもそも存在しないので。
第2版: launchd に KeepAlive で常駐させる
スクリプトをリポジトリに移し、launch agent を書き、
落ちても戻るように KeepAlive: true にしました。
翌朝これが出ていました。
scripts/daily-publish.sh: line 59: node: command not found
launchd から起動されたジョブには、PATH がほとんどありません。
私の場合は /usr/bin:/bin:/usr/sbin:/sbin だけ。
node は /opt/homebrew/bin にあります。
自分のシェルから同じスクリプトを叩けば通ります。シェルには PATH があるので。
高くついたのはここです。同じループは Python で書いた回収処理も回していて、
python3 は /usr/bin にあります。 だからそちらは動き続けました。
ログは15分おきに正常な記録で埋まっていました。
部分的に動いている自動化は、止まっている自動化より気づきにくい。 止まっているものにはログがありません。これには、健康そうに見えるログがありました。
直し方は地味です。
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
壊れた1本だけでなく、定期実行が触る全部のスクリプトに入れました。 入口が1つだとは限らないからです。別のスクリプトが、別の親から同じものを呼びます。
第3版: ループは走っておらず、凍っていた
PATH を直して公開も通り、そのあとログが3時間止まりました。 プロセスは生きています。
$ ps -o pid,etime,command -p 72127
PID ELAPSED COMMAND
72127 02:55:52 sleep 900
sleep 900 が2時間55分生きていました。
これはノートパソコンです。スリープします。
15分で終わるはずの sleep は、システムが停止している間は進みません。
while true; do 仕事; sleep N; done の形は、
スリープするたびに上限のない穴が開きます。
ループは壊れていませんでした。通信で詰まってもいませんでした。 書いたとおりに動いていて、書いた私が「実時間は流れ続ける」と仮定していただけです。
第4版: 定期実行は、定期実行の仕組みに任せる
<key>StartInterval</key>
<integer>900</integer>
<key>RunAtLoad</key>
<true/>
スクリプトは1回分やって終わる形にしました。ループも sleep もありません。
StartInterval はこのために用意されています。
スリープ中に間隔が過ぎていれば、復帰時に起こしてくれます。
同じラベルのジョブを二重に起動しないので、要るはずだったロックも消えました。
古いループのスクリプトは、隣に残さず削除しました。 同じ仕事に入口が2つあると、どちらが動いているのか分からなくなります。
最後にもう1つ罠がありました
間隔が登録できているか確かめました。
$ launchctl list com.example.daily
{
"Label" = "com.example.daily";
"LastExitStatus" = 0;
...
};
StartInterval がありません。
「登録に失敗している」と結論して、また作り直すところでした。
$ launchctl print gui/$(id -u)/com.example.daily
state = not running
runs = 4
last exit code = 0
run interval = 900 seconds
launchctl list は、持っているキーを全部は出しません。 print は出します。
片方の道具の出力に無いことは、ジョブについての証拠ではありません。
4つの版に共通していたこと
どの修正も正しかった。どれも確認した。 そして確認は毎回、そのジョブが実際には走らない環境で行われていました。
- 公開のロジックは自分のシェルで確かめた → 再起動が話題にならない
- launch agent は1回起こして確かめた → そのとき私の頭の中では、まだシェルの PATH が「PATH」だった
- ループは1周だけ見て確かめた →
進まない
sleepと進むsleepが、まったく同じに見える場合そのもの
残った規則は狭いですが、まだ反論できていません。
走る場所を変えたら、その場所で1回走らせるまで、それについて知っていたことを信じない。
ロジックを再テストするのではありません。同じロジックを、新しい環境で、 その環境固有の失敗を探しながら動かします。 PATH は何か、再起動を越えるか、スリープを越えるか、 そして確認に使う道具が何を教えてくれないか。
第4版はいま、何度かのスリープと1回の再起動を越えています。 証明ではありませんが、証拠が「置かれている場所」から出てきた最初の版です。
Claude Code に設計・実装・レビューを別々の人格として分担させ、GitHub Issue と
ブランチを軸に並列開発を回すための設定一式を MIT で公開しています。
コピーして ./setup.sh を叩けば動きます。技術スタックには依存しません。
https://github.com/quintetkit/quartet
このワークフローだけで実際にツールを 1 つ作りました。Issue の分割から PR、 レビュー、マージまで記録が全部残っています。うまくいかなかった箇所も消していません。
https://github.com/quintetkit/mdlinkcheck
UI 設計人格・レビュー基準・Issue 単位の並列実行スクリプト・実践ガイド 10 章を 足したものは製品ページにあります。
実践ガイド全10章は Zenn Book で読めます(¥1,500・2章まで無料)。
https://zenn.dev/quintetkit/books/claude-code-parallel-workflow
設定一式(5人格・スクリプト・ガイド全文)の配布はこちらです。