cronがexit 0を返しても、処理本体が動いたとは限りません。外側と内側で同じlockを取ると、自分自身を別プロセスと誤認して正常終了できます。 ここでは二重flockを再現し、lock名を分けて直します。 読み終えると、終了コードではなく仕事量まで確認する検品手順をコピペできます。
ケミの移設作業では、5本の定期処理が仕事をせず、正常終了として記録され続けました。そのうち1本は、導入後の全38回が空振りでした。ログは毎回 another instance holds the lock を残していましたが、exit 0とログ更新時刻だけを見る監視は健全と判定していました。
課題:安全なSKIPもexit 0になる
多重起動を防ぐため、定期実行の外側で flock を使う構成は珍しくありません。処理本体も自前のlockを持つことがあります。問題は、両方が同じlockファイルを使った時です。
cron
└─ 外側flockが same.lock を取得
└─ workerを起動
└─ 内側flockも same.lock を取得しようとする
└─ 「別実行中」と判断してexit 0
内側から見れば、多重起動を避けた正常なSKIPです。運用者から見れば、通知・同期・集計が1度も動いていません。「プロセスが安全に終わった」と「期待した仕事をした」は別の完了条件です。
設計判断:lockを一つにするか、責務ごとに分ける
| 方式 | 長所 | 失敗しやすい点 | 向く状況 |
|---|---|---|---|
| 外側だけでlock | 構成が単純 | 手動起動経路がlockを通らない場合がある | 起動経路を一本化できる |
| 内側だけでlock | どの起動経路でも同じ制御 | 本体の実装変更が必要 | workerを単独でも起動する |
| 外側と内側を別lock | 二つの責務を残せる | lock名と意味の管理が必要 | 外側はジョブ、内側は共有資源を守る |
今回の修正は、外側と内側のlock名を分ける方法でした。外側は定期ジョブ全体の重複を防ぎ、内側は処理固有の競合を防ぎます。同じファイルを取り合わなくなり、本体まで到達しました。
再現手順:同じlockを二度取ると空振りする
まず、内側でも same.lock を取るworkerを作ります。
$ mkdir -p /tmp/flock-noop-demo
$ cd /tmp/flock-noop-demo
$ printf '%s\n' '#!/bin/sh' \
'exec 9>./same.lock' \
'if ! flock -n 9; then' \
' echo "another instance holds the lock"' \
' exit 0' \
'fi' \
'echo "processed=0"' > worker.sh
$ chmod +x worker.sh
外側から同じ same.lock を取得してworkerを起動します。
$ flock -n ./same.lock ./worker.sh
another instance holds the lock
$ echo $?
0
exit 0ですが、期待した processed=0 は出ていません。0件という値すら、処理本体が対象を数えた証拠になる点が重要です。
修正版では、外側のlockを outer.lock へ分けます。
$ flock -n ./outer.lock ./worker.sh
processed=0
$ echo $?
0
二つともexit 0です。違いは、本体が仕事量を出力したかどうかにあります。
結果:38回の空振り後、定時発火で処理本体へ到達した
実環境では、影響した5本のうち4本に別の起動問題も重なっていました。残る1本は起動問題を通過していたものの、2026-07-24 22:30の導入後、38回すべてがlock衝突で空振りしました。
lock名を分けた後、17:30の定時発火は「通知件数0件」を出力しました。この0件だけで通知経路全体の成功までは断定できません。ただし、lock検出で即returnせず、通知対象を評価する地点まで進んだ証拠にはなります。
| 観測 | 空振り時 | 修正後 |
|---|---|---|
| exit code | 0 | 0 |
| ログ更新 | あり | あり |
| workerの仕事量 | なし | 通知件数0件 |
| 完了判定 | 誤って健全 | 本体到達を確認可能 |
監視へ残すべきなのは、発火時刻やexit codeだけではありません。通知なら評価件数と送信件数、同期なら対象件数と更新件数、集計なら読込行数と出力行数を残します。値が0でも、「評価まで進んだ0」と「そこへ到達しなかった空振り」を区別できます。
コピペで再現
前提条件
- Linux環境に
shとflock(util-linux)が入っています。 /tmp/flock-noop-demoだけを使う安全な再現です。本番cronや既存lockは変更しません。
以下を上からそのまま実行すると、失敗版と修正版を比較できます。
$ mkdir -p /tmp/flock-noop-demo
$ cd /tmp/flock-noop-demo
$ printf '%s\n' '#!/bin/sh' \
'exec 9>./same.lock' \
'if ! flock -n 9; then' \
' echo "another instance holds the lock"' \
' exit 0' \
'fi' \
'echo "processed=0"' > worker.sh
$ chmod +x worker.sh
$ flock -n ./same.lock ./worker.sh
another instance holds the lock
$ echo $?
0
$ flock -n ./outer.lock ./worker.sh
processed=0
$ echo $?
0
cronへ移す場合の修正版は、外側専用lockを使います。パスは自分の配置へ置き換えてください。
*/5 * * * * /usr/bin/flock -n /run/user/1000/example-job.outer.lock /home/USER/bin/worker.sh >> /home/USER/log/example-job.log 2>&1
テスト後の後始末です。
$ rm -f /tmp/flock-noop-demo/worker.sh /tmp/flock-noop-demo/same.lock /tmp/flock-noop-demo/outer.lock
$ rmdir /tmp/flock-noop-demo
ハマり所
- exit 0だけで合格にしないでください。処理固有の
processed、evaluated、updatedなどを必ず1つ出します。 - lockファイルを開けないことと、実際にlockが保持されていることは別です。権限エラーをlock保持の証拠にしないでください。
- 外側と内側の両方を残すなら、ファイル名を変えるだけでなく、何を守るlockかをコメントへ書いてください。
flock -nは取得できない時に待ちません。待つ要件なら-w 秒数を使い、timeoutも結果として記録します。
あわせて読みたい: systemd 259のUnknown assignmentで定期処理が起動しない時の切り分け
この検証を回している環境
この検証は、自宅の常設ラボ(使い捨てVM/LXCを回す母艦+GPU+VLAN分離ネットワーク)で動かしています。使っている機材と選定理由、全体構成は1本にまとめています。
ラボ構成のまとめを見る →