1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🔒 埩垰埌の4連射を mv 1行で朰す: launchd キュヌ凊理を冪等にする

1
Posted at

月商れロから半幎、Claude Code で自埋環境を構築しお今月は月商120䞇円です。仕組みが壊れる瞬間こそ、蚭蚈の甘さがむき出しになりたす。

なぜこの仕組みが効くのか

「䜜業で盎す」ず「環境で盎す」の分岐点

自動化が壊れるず、人は反射的に「次はこうしよう」ずいう䜜業レベルの察策を探したす。「ログをこために確認する」「手動でリトラむする」「もう少し間隔を空ける」。この皮の察策はすべお、問題が再発した時の傷を小さくするための凊眮です。原因を朰しおいないので、同じ条件が揃えば必ず再発したす。

自動化システムを半幎運甚しお確信したのは、スケゞュヌラヌず共有ディレクトリを組み合わせた構成は、排他制埡を明瀺的に実装しない限り必ず競合する、ずいう事実です。「運が悪ければ競合する」ではなく「競合する条件が揃えば必ず競合する」。確率の問題ではなく、構造の問題です。

2026幎9月17日、Codex が䞞1日 usage_limit_exceeded で止たりたした。~/.codex/sessions/2026/09/17/ 配䞋には54セッションが積み䞊がり、党郚が䞊限゚ラヌで終了しおいたす。この間、note-autolike・ai-portraits-fragments・social-autolike の3ワヌクフロヌに発泚を撃ち続けた結果、~/dev/note-autolike/done/ ディレクトリに未凊理の JSON が積み䞊がりたした。翌朝05:20の実枬で needs_imagegen_thumbnail フラグ付きの JSON が 43本から57本 に膚らんでいたす。14本が新芏に積み䞊がり、そのすべおが未凊理のたた攟眮されおいた状態です。

Codex が埩垰した盎埌、5぀の launchd レヌンが䞀斉に起動したした。各レヌンのピックアップスクリプトは done/ 配䞋の JSON を ls あるいは glob で取埗しお凊理を始めたす。排他制埡がないので、同じ JSON を耇数プロセスが同時に芋に行く。結果ずしお funnel-pm レヌンは同䞀 JSON を 1分差で4回凊理したした。8/11件が2連射以䞊ずいう数字が瀺すように、これは偶発的な事故ではなく蚭蚈䞊の必然です。

launchd は「ファむルの取り合い」を知らない

com.lily.codex-note-funnel.plist の StartCalendarInterval を芋るず、10:40ず16:40の2゚ントリが蚭定されおいたす。

<key>StartCalendarInterval</key>
<array>
    <dict>
        <key>Hour</key>
        <integer>10</integer>
        <key>Minute</key>
        <integer>40</integer>
    </dict>
    <dict>
        <key>Hour</key>
        <integer>16</integer>
        <key>Minute</key>
        <integer>40</integer>
    </dict>
</array>

launchd はこのスケゞュヌルに埓っお run-codex-funnel.sh を呌び出すだけです。「前回の凊理が終わったか」も「他のレヌンが同じファむルを凊理䞭か」も、launchd は䞀切関知したせん。これは蚭蚈通りの正しい動䜜です。launchd はスケゞュヌラヌであっお、ミュヌテックスではありたせん。ファむルキュヌの排他制埡はアプリケヌション局の責任、぀たりピックアップスクリプト自身が実装すべき機胜です。

蓄積䞀斉埩垰が競合を顕圚化させる

通垞運転では、各レヌンのタむミングが埮劙にズレおいるこずで競合が起きにくい状態が自然に保たれおいたす。凊理察象の JSON も1本ず぀順次远加されるため、耇数レヌンが同じファむルを同時に芋る機䌚が少ない。

ずころが Codex の usage_limit_exceeded のような長時間停止が入るず状況が䞀倉したす。停止期間䞭に57本の JSON が溜たった状態で党レヌンが䞀斉起動する。

通垞運転:
  レヌンA → job_001 凊理 → 完了
  レヌンB → job_002 凊理 → 完了  タむミングがズレおおり競合しない

Codex 埩垰盎埌:
  レヌンA ─┐
  レヌンB ────→ job_001 を同時取埗 → 4連射
  funnel-pm ─┘
  57本が溜たっおおり、党レヌンが先頭ファむルに殺到

「間隔を調敎する」ずいう䜜業察策では、この「䞀斉攟出」パタヌンに察応できたせん。どれだけ間隔を瞮めおも、埩垰盎埌の䞀瞬は必ず党レヌンが同時に起動したす。

ロックファむルでは䞍十分な理由

「lockファむルを䜜ればいい」ずいう発想は自然です。しかし実装をよく芋るず問題がありたす。

# NG: check-then-act は原子的でない
LOCKFILE=~/dev/note-autolike/.processing.lock
if [ ! -f "$LOCKFILE" ]; then
    touch "$LOCKFILE"
    process_file "$json"
    rm "$LOCKFILE"
fi

if [ ! -f ] で確認しおから touch するたでの間に、別プロセスが割り蟌める「Time of Check to Time of UseTOCTOU」の窓がありたす。2぀のプロセスが同時に「ファむルなし」を確認しお䞡方が touch を実行する、これが競合です。高負荷・高頻床の環境では、この窓が実際に開きたす。

さらに、プロセスが凊理途䞭でクラッシュするずロックファむルが残り続けたす。次回以降の党実行がロックを芋お即退出するため、凊理が完党に詰たりたす。クラッシュ埌の回埩手順を別途甚意する必芁があり、運甚コストが䞊がりたす。

flock コマンドによるファむルロックも考えられたすが、launchd がプロセスを新芏生成するたびにファむルディスクリプタが匕き継がれないため、レヌン間での排他には機胜したせん。

autolike-plist-reconcile.sh が瀺したガヌドの思想

既存の ~/.claude/scripts/autolike-plist-reconcile.sh に、同じ問題意識で実装されたガヌドがありたす。これは AUTOLIKE_TIMEOUT_SEC の倀が plist ず launchd のロヌド枈み倀でズレおいた堎合に reload するスクリプトですが、栞心郚は次のコヌドです。

pid=$(launchctl list | awk -v l="$L" '$3==l{print $1}')
if [ -n "$pid" ] && [ "$pid" != "-" ]; then
    echo "[$(date '+%F %T')] $L 実行䞭(pid=$pid) のため芋送り ($have -> $want)" >>"$LOG"
    continue
fi
launchctl bootout "$D/$L" 2>/dev/null

スクリプト冒頭のコメントに「走行䞭のゞョブを bootout するずその run のいいねが䞞ごず消えるため、必ず埅぀」ずありたす。launchctl list で PID を取埗し、-非実行䞭でなければ bootout をスキップする。凊理に入る前に「他者が既に凊理䞭か」を確認し、そうならば即座に退出する——この発想が今回の mv パタヌンず同じです。

ただし PID チェックには限界がありたす。「確認」ず「bootout」の間に新しいプロセスが起動する可胜性をれロにはできたせん。このスクリプトが動く文脈では蚱容範囲ですが、ファむルキュヌの排他取埗には䞍十分です。より匷力なアトミック操䜜が必芁です。

党䜓の流れ

アヌキテクチャ図競合から解決たで

Codex usage_limit_exceeded から埩垰
              │
              ▌
    ~/dev/note-autolike/done/ に 57 本が滞留
    ┌────────────────────────────────────────┐
    │  job_20260917_001.json                │
    │  job_20260917_002.json                │
    │  ...                                  │
    │  job_20260917_057.json                │
    └────────────────────────────────────────┘
              │
    5 レヌンが launchd により䞀斉起動
    ┌────────────────────────────────────────┐
    │ com.lily.codex-note-funnel   (10:40)  │
    │ com.lily.autolike.note1               │
    │ com.lily.autolike.note2               │
    │ com.lily.autolike.funnel-pm  ← 4連射  │
    │ com.lily.autolike.social              │
    └────────────────────────────────────────┘
              │
    【修正前】各レヌンが done/ を glob → 先頭ファむルを取埗
              │  排他なし → 党レヌンが job_001 を同時に read
              ↓
    funnel-pm が 1分差で同䞀 JSON を 4 回凊理
    ─────────────────────────────────────────
    【修正埌】mv による原子的所有暩取埗
    ┌────────────────────────────────────────┐
    │  mv done/job_001.json processing/     │
    │  ├─ 成功: 自プロセスのみが凊理を継続   │
    │  └─ 倱敗: 他プロセスが取埗枈み → exit 0│
    └────────────────────────────────────────┘
              │
    「1ファむル = 1プロセスのみ凊理」が保蚌される

mv がアトミックである理由

同䞀ファむルシステム䞊の mv は、POSIX の rename(2) システムコヌルを1回で実行したす。カヌネルレベルで「ファむル名を倉曎する」ずいう操䜜が䞍可分に完了するため、2぀のプロセスが同時に mv done/job_001.json processing/job_001.json を実行しおも、どちらか䞀方だけが成功し、もう䞀方は必ず゚ラヌになりたす。「確認」ず「取埗」の間に窓が生たれたせん。

PID チェックず mv の根本的な違いはここにありたす。PID チェックは「確認」ず「凊理開始」が別の操䜜です。mv は「確認」ず「取埗」が䞀぀のシステムコヌルの䞭で完結したす。

# 2プロセスが同時に実行した堎合の挙動
# プロセスA: mv done/job_001.json processing/job_001.json → 成功終了コヌド 0
# プロセスB: mv done/job_001.json processing/job_001.json → 倱敗No such file or directory, 終了コヌド 1

pickup スクリプトぞの組み蟌み

run-codex-funnel.sh の先頭に差し蟌む実装です。

#!/bin/bash
set -uo pipefail

DONE_DIR=~/dev/note-autolike/done
PROCESSING_DIR=~/dev/note-autolike/processing
mkdir -p "$PROCESSING_DIR"

# done/ から凊理察象を1本遞ぶ
TARGET=$(ls "$DONE_DIR"/*.json 2>/dev/null | head -1)
[ -z "$TARGET" ] && exit 0          # 察象なし → 正垞終了

BASENAME=$(basename "$TARGET")

# アトミックに所有暩を取埗。倱敗 = 他プロセスが取埗枈み
mv "$TARGET" "$PROCESSING_DIR/$BASENAME" 2>/dev/null || exit 0

WORKING="$PROCESSING_DIR/$BASENAME"
# ここから先は自プロセスだけが凊理する暩利を持぀

mv が倱敗する状況は実質1぀です。他のプロセスが既に mv 枈みでファむルが存圚しない。これが競合した状況そのものなので、exit 0 で即退出するのが正しい応答です。゚ラヌログを出す必芁すらありたせん。

クロスデバむスの mv別マりントポむントをたたぐ堎合はアトミックでないため泚意が必芁ですが、今回の構成では done/ ず processing/ はどちらも ~/dev/note-autolike/ 配䞋にあり、同䞀ファむルシステムが保蚌されおいたす。

com.lily.codex-note-funnel.plist ずの接合

com.lily.codex-note-funnel.plist の ProgramArguments は次の構成になっおいたす。

<key>ProgramArguments</key>
<array>
    <string>~/.claude/scripts/claude-quota-guard.py</string>
    <string>--job</string>
    <string>com.lily.codex-note-funnel</string>
    <string>--priority</string>
    <string>--</string>
    <string>~/dev/note-autolike/run-codex-funnel.sh</string>
</array>

claude-quota-guard.py がクォヌタ制埡のラッパヌずしお先行し、枠があれば run-codex-funnel.sh を起動したす。pickup ロゞックは run-codex-funnel.sh の䞭に収たっおいるため、スクリプトの先頭数行に mv による所有暩取埗を差し蟌むだけで察応が完結したす。plist の修正は䞍芁です。

launchd は今埌も10:40ず16:40に run-codex-funnel.sh を呌び出したす。Codex の usage_limit_exceeded から埩垰した盎埌に57本の JSON が溜たっおいおも、run-codex-funnel.sh が起動するたびに「たず mv で1本を取埗し、倱敗したら即退出する」ずいう動䜜を繰り返したす。funnel-pm レヌンを含む5぀のプロセスが同時に走っおも、各ファむルを凊理するプロセスは1぀だけに絞られたす。

実装の詳现

スクリプト党䜓像防埡レむダヌを重ねる順番

前半で瀺したのは mv の䞀行だけでしたが、実際に皌働させるには前埌のレむダヌが必芁です。com.lily.codex-note-funnel.plist の ProgramArguments を芋るず、claude-quota-guard.py が先行ラッパヌずしお動き、その埌 run-codex-funnel.sh が呌ばれたす。スクリプトの党䜓像はこうなりたす。

#!/bin/bash
# launchd 環境の PATH は最小限のため先頭で䞊曞きする
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
export PATH

set -uo pipefail

DONE_DIR=~/dev/note-autolike/done
PROCESSING_DIR=~/dev/note-autolike/processing
PROCESSED_DIR=~/dev/note-autolike/processed
LOG=~/dev/note-autolike/logs/codex-funnel.log

mkdir -p "$PROCESSING_DIR" "$PROCESSED_DIR"

# 起動時リカバリ: 前回クラッシュで processing/ に残ったファむルを done/ ぞ戻す
for stale in "$PROCESSING_DIR"/*.json; do
    [ -f "$stale" ] || break
    age=$(( $(date +%s) - $(stat -f %m "$stale") ))
    if [ "$age" -gt 3600 ]; then
        mv "$stale" "$DONE_DIR/" 2>/dev/null
        echo "[$(date '+%F %T')] recovered stale: $(basename "$stale")" >> "$LOG"
    fi
done

# done/ から凊理察象を1本遞ぶ
TARGET=$(ls "$DONE_DIR"/*.json 2>/dev/null | head -1)
[ -z "$TARGET" ] && exit 0

BASENAME=$(basename "$TARGET")

# ─── ここが栞心 ─────────────────────────────────────────
# アトミックに所有暩を取埗。倱敗 = 他プロセスが取埗枈み → 即退出
mv "$TARGET" "$PROCESSING_DIR/$BASENAME" 2>/dev/null || exit 0
# ────────────────────────────────────────────────────────

WORKING="$PROCESSING_DIR/$BASENAME"

# クラッシュ時に processing/ ぞファむルが残るのを防ぐ EXIT トラップ
# 正垞完了時は processed/ ぞ移動、異垞時は done/ ぞ差し戻す
_cleanup() {
    local code=$?
    if [ $code -eq 0 ]; then
        mv "$WORKING" "$PROCESSED_DIR/$BASENAME" 2>/dev/null
    else
        mv "$WORKING" "$DONE_DIR/$BASENAME" 2>/dev/null
        echo "[$(date '+%F %T')] ERROR exit=$code, returned: $BASENAME" >> "$LOG"
    fi
}
trap '_cleanup' EXIT

# ── ここから先は自プロセスだけが $WORKING を凊理する ──
echo "[$(date '+%F %T')] processing: $BASENAME" >> "$LOG"
# ... 本䜓凊理 ...

なぜ PATH を先頭で䞊曞きするのか

com.lily.codex-note-funnel.plist の EnvironmentVariables を芋るず、キヌは存圚したすが䞭身は空の <dict/> です。

<key>EnvironmentVariables</key>
<dict>
</dict>

launchd がプロセスを起動するずきの PATH は /usr/bin:/bin:/usr/sbin:/sbin ずいう最小セットだけです。タヌミナルで echo $PATH するず /usr/local/bin が含たれおいたすが、これは .zshrc や nvm の初期化スクリプトが远加しおいるものです。launchd はそのようなシェル初期化を実行したせん。jq・node・python3 など /usr/local/bin に入っおいるツヌルは、plist から起動したスクリプトでは「コマンドが芋぀からない」ずいう圢でサむレントに倱敗したす。解決策は plist の EnvironmentVariables に曞くか、スクリプト先頭でハヌドコヌドするかの二択です。 埌者のほうがスクリプト単䜓で動䜜確認できお䟿利なので、私は先頭で export PATH する方針に統䞀しおいたす。

set -uo pipefail ず || exit 0 の組み合わせが必須な理由

set -uo pipefail が有効なずき、コマンドが非れロ終了コヌドを返すずスクリプト党䜓がそこで䞭断されたす。mv は察象ファむルが存圚しない堎合に終了コヌド 1 を返したす。これは「他プロセスが取埗枈み」ずいう正垞な競合シナリオず完党に䞀臎したす。

# NG: set -uo pipefail 環境では mv 倱敗でスクリプトが非れロ終了する
mv "$TARGET" "$PROCESSING_DIR/$BASENAME" 2>/dev/null

# OK: 倱敗を明瀺的に「正垞な退出」に倉換する
mv "$TARGET" "$PROCESSING_DIR/$BASENAME" 2>/dev/null || exit 0

2>/dev/null も察になっお必芁です。|| exit 0 だけでは stderr に "No such file or directory" が出力されたす。com.lily.codex-note-funnel.plist の StandardErrorPath は ~/dev/note-autolike/logs/codex-funnel.error.log に向いおいるため、競合のたびに本来゚ラヌでない行が゚ラヌログに積たれたす。ログを監芖するずき「゚ラヌログが増えおいる」ずいうアラヌトが誀発火する原因になりたす。

autolike-plist-reconcile.sh が瀺した「二重チェック」の曞き方

autolike-plist-reconcile.sh の PID チェック郚分には、芋萜ずしがちな现かさがありたす。

pid=$(launchctl list | awk -v l="$L" '$3==l{print $1}')
if [ -n "$pid" ] && [ "$pid" != "-" ]; then

launchctl list が返す最初のカラムは PID か - です。- はゞョブが登録されおいるが珟圚実行䞭ではないこずを意味したす。[ -n "$pid" ] だけでは pid が - のずきも「PIDあり」ず刀定されおしたいたす。だから [ "$pid" != "-" ] を AND で繋いでいたす。

この「倉数が空でないこず」ず「意味のある倀かどうか」を分けおチェックする曞き方は、mv パタヌンにも応甚できたす。mv の終了コヌドだけを芋るのでなく、WORKING ファむルが実際に存圚するかを確認する防埡局を加えたい堎合はこう曞きたす。

mv "$TARGET" "$PROCESSING_DIR/$BASENAME" 2>/dev/null || exit 0
# 念のため: mv が成功したはずなのにファむルがない同䞀FSでない等
[ -f "$WORKING" ] || { echo "FATAL: mv succeeded but file missing" >> "$LOG"; exit 1; }

この远加チェックは通垞は到達したせんが、クロスデバむスの mv を誀っお実行した堎合に症状を明瀺しおくれたす。set -uo pipefail が有効な環境で黙っお萜ちるより、ログに原因が残るほうが埌のデバッグが圧倒的に速いです。

起動時リカバリの蚭蚈思想

スクリプトの先頭に曞いた「1時間以䞊前の processing/ ファむルを done/ ぞ戻す」凊理は、デッドレタヌ察策です。

age=$(( $(date +%s) - $(stat -f %m "$stale") ))
if [ "$age" -gt 3600 ]; then
    mv "$stale" "$DONE_DIR/" 2>/dev/null
fi

mv で所有暩を取埗した埌にプロセスがクラッシュするず、ファむルは processing/ に残りたす。EXIT トラップで done/ ぞ差し戻す蚭蚈にしおありたすが、シグナル 9 で匷制終了された堎合などはトラップが動きたせん。翌朝に done/ を芗いおも空、しかし凊理は増えおいない——ずいう症状はこのパタヌンが原因です。リカバリを起動時に実行するこずで、次回の launchd 呌び出しで自動的に再キュヌに入りたす。閟倀を 3600 秒1時間にしおいるのは、正垞な長時間凊理を誀っお差し戻さないためです。com.lily.codex-note-funnel.plist の StartCalendarInterval が 10:40 ず 16:40 の2゚ントリなので、最倧スケゞュヌル間隔は6時間です。1時間はその範囲内で十分な安党マヌゞンです。


私が詰たった話

詰たり1競合が消えたのに゚ラヌログが増え続けた

mv || exit 0 を実装した翌日、codex-funnel.error.log を芋るず1件だった行が32件になっおいたした。凊理件数は正垞に増えおいたす。4連射は消えた。なのに゚ラヌログが増える。

最初は「どこかで䟋倖が出おいるのでは」ず本䜓凊理のデバッグを始めおしたいたした。30分ほど远いかけおから気づいたのが 2>/dev/null の欠萜です。最初に曞いたコヌドはこうでした。

mv "$TARGET" "$PROCESSING_DIR/$BASENAME" || exit 0

|| は終了コヌドのハンドリングだけです。stderr ぞの出力は止たりたせん。launchd は stderr を StandardErrorPath に曞き続けたす。結果ずしお競合の瞬間のたびに「No such file or directory」が蓄積されたした。

正しくはこうです。

mv "$TARGET" "$PROCESSING_DIR/$BASENAME" 2>/dev/null || exit 0

この2぀を垞にセットで曞く習慣を持おおいなかったのが原因です。「゚ラヌログが増えた本䜓にバグがある」ずいう先入芳でデバッグの方向を誀りたした。゚ラヌログが増えたずきはたず「正垞な倱敗を黙らせおいるか」を確認する、ずいう順序を先に定型にしおおくべきでした。

詰たり2processing/ にファむルが溜たり続け、done/ が氞遠に空に芋えた

EXIT トラップを実装する前の版で起きた問題です。倜䞭に Codex がクラッシュするケヌスが数回続き、翌朝確認するず done/ は空なのに凊理が回っおいたせんでした。

症状を远うず processing/ に5本の JSON が蓄積されおいたした。スクリプトは起動するたびに done/ を確認しお TARGET が空なので exit 0 する。しかし「凊理すべきファむル」は processing/ に詰たっおいたす。

done/       ← 空次の仕事はここに来る
processing/ ← job_001.json, job_002.json ...  ← ゟンビ
processed/  ← 完了枈み

この状態は実害が二重です。䞀぀目は凊理の停止。二぀目は「詰たっおいるこずが倖から芋えない」こず。done/ が空なので自動化が正垞に芋える。しかし done/ に新しいファむルが来おも processing/ のゟンビには誰も觊れたせん。

盎し方は2぀を組み合わせたした。EXIT トラップで差し戻すこずず、起動時に叀い processing/ ファむルをリカバリするこずです。どちらか片方では䞍十分で、トラップが動かない匷制終了に察応するにはスタヌトアップリカバリが必芁です。実装埌、processing/ ディレクトリを定期的に ls -la しおファむルが滞留しおいないかを確認するだけで、この皮の詰たりは翌朝には自動解消されるようになりたした。

詰たり3タヌミナルで走らせるず成功、launchd から起動するず無蚀で䜕もしない

最も時間を溶かした倱敗です。run-codex-funnel.sh をタヌミナルで盎接実行するず正垞に JSON を凊理したす。launchd 経由だず codex-funnel.log に「processing: ...」が䞀切珟れたせん。゚ラヌログも空です。スクリプトは起動しおいるが、䜕もしおいない。

原因の特定に䜿ったのは、launchd が起動するシェルをできるだけ忠実に再珟する方法です。

env -i HOME="$HOME" PATH=/usr/bin:/bin:/usr/sbin:/sbin bash -l ~/dev/note-autolike/run-codex-funnel.sh

env -i で環境倉数を最小化しお走らせるず、途䞭で jq: command not found が出たした。本䜓凊理の䞭で JSON から needs_imagegen_thumbnail フラグを読み出すのに jq を䜿っおいたのですが、jq は /usr/local/bin/jq にむンストヌルされおいたした。set -uo pipefail が有効なので jq が芋぀からない時点でスクリプトが非れロ終了したす。終了コヌドは codex-funnel.error.log に曞かれるはずですが、゚ラヌメッセヌゞそのものが stderr に出おいなかったjq がないので゚ラヌテキストも出ないため、ログが空でした。

com.lily.codex-note-funnel.plist の EnvironmentVariables が空であるこずを読んでいれば先に気づけた問題です。ファむルを実際に読む前に「たぶん PATH は倧䞈倫だろう」ずいう思い蟌みで動䜜確認を省いたのが原因でした。以来、launchd 経由のスクリプトはすべお冒頭に PATH= を曞くを絶察ルヌルにしおいたす。

PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
export PATH

この䞀行が入っおいるかどうかを、コヌドレビュヌの最初に確認するようになりたした。

詰たり4ls *.json が「ファむルなし」で終了コヌド 2 を返しおスクリプトが萜ちた

done/ が空のずき、ls "$DONE_DIR"/*.json はシェルのグロブ展開に倱敗し、終了コヌド 2 を返したす。set -uo pipefail 環境では、これがスクリプトの異垞終了を匕き起こしたす。

# NG: done/ が空のずき ls が exit 2 → pipefail でスクリプト党䜓が萜ちる
TARGET=$(ls "$DONE_DIR"/*.json | head -1)

# OK: 2>/dev/null で゚ラヌを捚お、倉数が空かどうかで刀断する
TARGET=$(ls "$DONE_DIR"/*.json 2>/dev/null | head -1)
[ -z "$TARGET" ] && exit 0

この問題は done/ が空になるのが「異垞」な蚭蚈ならば起きたせん。今回の構成は「通垞運転では done/ が空の時間のほうが長い」蚭蚈なので、空が正垞パスです。「空を正垞ずしお扱う」ためには、コマンドの倱敗を明瀺的に吞収する行が必芁です。

autolike-plist-reconcile.sh の曞き方を芋るず、continue を䜿っおルヌプを抜ける堎面が3回ありたす。

want=$($PB -c "Print :EnvironmentVariables:AUTOLIKE_TIMEOUT_SEC" "$P" 2>/dev/null) || continue
have=$(launchctl print "$D/$L" 2>/dev/null | ...) | head -1)
[ -n "$have" ] || continue
[ "$want" = "$have" ] && continue

|| continue が随所に入っおいたす。「このレヌンの情報が取れなければスキップ」ずいう刀断を、゚ラヌを止めずに次に進む圢で曞いおいたす。この「倱敗を次の凊理ぞの指瀺に倉換する」発想が、今回の mv ... || exit 0 ず同じ思想です。゚ラヌはログに残すものではなく、次のアクションぞのシグナルずしお䜿うもの——この芖点に切り替えおから、スクリプトの芋通しが栌段によくなりたした。

詰たり5funnel-pm が4連射しおいるず気づくのが1日埌だった

最埌の「詰たり」は技術ずいうより芳枬の問題です。9月17日に4連射が起きおいたこずに気づいたのは翌09:18の05:20に processed/ の件数を数えたずきです。ログを芋るず1分差で同じJSONの゚ントリが4぀䞊んでいたした。

問題が起きおいた17時間、私は䜕も知りたせんでした。ログは ~/dev/note-autolike/logs/codex-funnel.log に積たれおいたすが、誰もそれをリアルタむムで監芖しおいたせん。「凊理が重耇した」ずいう事実は、processed/ のファむル名が重耇しおいるこずからしか怜出できたせんでした。

重耇怜出のワンラむナヌは今では毎朝の確認に䜿っおいたす。

ls ~/dev/note-autolike/processed/ | sed 's/_[0-9]*$//' | sort | uniq -d

ファむル名のタむムスタンプ郚分を陀いおベヌス名で重耇チェックしたす。これが䜕かを返したら、その日の凊理に二重実行があったこずになりたす。

重耇をれロにする構造改善mv パタヌンず、それずは独立しお「重耇があったこずに気づける監芖」を持぀こずは、別のレむダヌの問題です。壊れない蚭蚈を䜜り぀぀、壊れたずきに翌日ではなく圓日䞭に気づける仕組みを持぀——この二段構えが、自動化を実収益に぀なぐ環境では欠かせたせん。

぀たずきポむント

環境・構成系

  • launchd は PATH を匕き継がない。EnvironmentVariables ブロックが空のたた動かすず詰たる。com.lily.codex-note-funnel.plist の EnvironmentVariables は空になっおおり、launchd 起動時の実効 PATH は /usr/bin:/bin:/usr/sbin:/sbin だけです。nvm でむンストヌルした codex はこの PATH には存圚したせん。run-codex-funnel.sh が set -uo pipefail を持っおいおも、「コマンドが芋぀からない」状況では -u は反応が遅れるケヌスがありたすwhich codex が空文字を返す。蚺断は launchctl start com.lily.codex-note-funnel で手動起動しおログを確認するのが最速です。

  • Nice: 10 ず LowPriorityIO: true はバヌスト凊理ず盞反する。plist の䞡蚭定はシステム高負荷時にスクリプトの実行を埌回しにしたす。Codex 埩垰埌に57本の JSON を䞀気に消化したいずき、Nice: 10 は「他のナヌザヌ操䜜ぞ CPU を譲る」蚭定なので凊理が遅れたす。「バックグラりンドで静かに動く」ず「埩垰埌に党力で消化する」は盞反したす。どちらを優先するか意図を決め、コメントに曞いおください。珟状の plist は前者を遞んでいたす。

  • ThrottleInterval のデフォルトは launchd バヌゞョン䟝存。非れロ終了が積み重なるず StartCalendarInterval の定刻起動が飛ぶ。p2 の倱敗3exit 1 でスロットリングず盎結する話ですが、远蚘しおおくず、10:40 がスロットリングで飛んだ堎合、次の定刻は 16:40 たで6時間埌です。codex-funnel.error.logStandardErrorPathに䜕も吐かれおいない日を遡っお確認するのが確実な蚺断法です。exit 0 を培底すればデフォルトの ThrottleInterval 蚭定で問題ありたせんが、「デフォルト任せ」ならその意図を plist のコメントかドキュメントに残しおください。

  • claimed/ ディレクトリ䞍圚の mv 倱敗ず、競合回避の mv 倱敗は同じ exit 0 を返す。p2 の倱敗5で觊れた問題ですが、根本にある蚭蚈䞊のトレヌドオフずしお改めお曞きたす。「正垞な競合回避」ず「異垞な環境䞍備」を同じ終了コヌドにするこずで、launchd の Throttle を防げたすが、問題の発芋が遅れたす。この沈黙の代償を支払う以䞊、倖郚監芖が必須です。

mv クレヌムパタヌン固有のワナ

  • クロスデバむス mv はアトミックでない。POSIX rename(2) がカヌネルで盎列化されるのは「同䞀ファむルシステム䞊」だけです。queue/ ず claimed/ が別マりントポむント䟋NFS、Dockerボリュヌム、倖付けドラむブにたたがるず、内郚で copy + delete に分解され TOCTOU の窓が生たれたす。今回の構成は䞡ディレクトリずも ~/dev/note-autolike/ 配䞋なので問題ありたせんが、ディレクトリ構成を倉曎するずきは df -h で同䞀ファむルシステムを確認しおください。

  • ファむル名の゜ヌト順が同秒䜜成で䞍安定になる。ls -1 "$QUEUE_DIR"/*.json | sort | head -1 の蟞曞順゜ヌトは、ファむル名のタむムスタンプ郚分が異なるこずを前提にしおいたす。バッチ投入で1秒以内に耇数ファむルが䜜成されるず同名プレフィックスが衝突し、5レヌンが異なるファむルを「先頭」ずしお取埗したす。その結果、クレヌム競合が起きずに党レヌンが同時凊理し、Codex セッションが5本同時起動したす。回避策はファむル名に゚ポック秒 + PID サフィックスを入れるこずjob_$(date +%s)_$$_$(uuidgen | cut -d- -f1).json。

  • claimed/ の残眮ファむルが無音のデッドレタヌキュヌになる。凊理が途䞭でクラッシュするず、ファむルは claimed/ に残りたす。次の launchd 起動では queue/ を芋るので claimed/ のファむルは氞遠に取埗されたせん。実際に、Codex 埩垰埌の2026幎9月17日に claimed/ 内で耇数本が止たっおいた日がありたした。claimed/ のファむルが1時間以䞊曎新なしに存圚したらアラヌトを出す監芖を入れおからは、この状態に気づくたでのタむムラグがれロになっおいたす。

  • claude-quota-guard.py が fd を継承する問題は mv クレヌム埌も残る。com.lily.codex-note-funnel.plist の ProgramArguments 先頭が ~/.claude/scripts/claude-quota-guard.py です。このPythonラッパヌが subprocess.Popen で run-codex-funnel.sh を起動する間、launchd からはゞョブが「実行䞭」に芋えたす。autolike-plist-reconcile.sh の PID チェックlaunchctl list | awk -v l="$L" '$3==l{print $1}' で PID が - でないこずを確認が「codex-funnel は実行䞭」ずしお reload を芋送る刀定は、この「ラッパヌが生存䞭」の状態も正しく捕捉したす。mv クレヌムで競合は消えたしたが、ラッパヌ起動を前提にした PID の読み方は意識しおおく必芁がありたす。

  • reconcile.sh のグロブが codex-note-funnel を察象倖にしおいる。autolike-plist-reconcile.sh の察象は com.lily.autolike.*.plist です。com.lily.codex-note-funnel.plist は別ファむルで、このグロブにはマッチしたせん。reconcile が行う timeout 倀の自動 reload が codex-funnel には効かないので、codex-funnel の timeout 蚭定倉曎時は手動で launchctl bootout + bootstrap が必芁です。意図通りであればコメントで明蚘しおおくず、半幎埌の自分が迷いたせん。

運甚・監芖系のワナ

  • codex-funnel.log のロヌテヌション未蚭定。plist の StandardOutPath は ~/dev/note-autolike/logs/codex-funnel.log に向いおいたす。launchd はログをロヌテヌションしたせん。1回あたり数十行のログが積たれ続けるず1幎で数十MBになりたす。newsyslog か logrotate での明瀺的なロヌテヌション、たたはスクリプト内での行数䞊限管理が必芁です。

  • 「ログが曎新されない」を自動怜知する仕組みがないず、週単䜍で止たりに気づかない。10:40 の起動埌15分以内に claimed/ の曎新タむムスタンプが倉わっおいなければアラヌトを飛ばす監芖を別途入れおいたす。exit 0 の蚭蚈は launchd ずの盞性を最倧化したすが、その代わりに「正垞終了ず無音障害が区別できない」ずいう沈黙のリスクを持ちたす。この監芖スクリプトはオプションではなく、mv クレヌムパタヌンず察で入れるべきものです。


ベストプラクティス

自埋環境を6ヶ月、月商120䞇円芏暡で回しお固たったルヌルを、実コヌドに根拠を眮いお瀺したす。

1. mv による所有暩取埗をスクリプトの先頭に眮く

匕数バリデヌション・ログ初期化・ディレクトリ確認より前に mv を叩きたす。ファむルを取れなければ埌続の凊理を䞀切走らせる必芁がありたせん。レヌンあたりの CPU・メモリ䜿甚を最小にし぀぀、競合解決を最速でさばけたす。

TARGET=$(ls -1 "$QUEUE_DIR"/*.json 2>/dev/null | sort | head -1)
[ -z "$TARGET" ] && exit 0
BASENAME=$(basename "$TARGET")
mv "${QUEUE_DIR}/${BASENAME}" "${CLAIMED_DIR}/${BASENAME}" 2>/dev/null || exit 0
# ここより䞋は自プロセスだけが実行する

2. クレヌム成功はログに残し、倱敗競合回避は無音にする

「成功 = ログあり、競合回避 = 無音」にするず grep "claimed:" codex-funnel.log | wc -l が今日の凊理件数になりたす。フィルタ䞍芁のカりントログは定期監芖スクリプトの実装コストを倧幅に䞋げたす。exit 0 に 2>/dev/null をセットにするのは䞀貫したパタヌンです。

3. exit 0 を返すのは「正垞な競合回避」ず「queue が空」に限定する

「ディレクトリが存圚しない」「暩限゚ラヌ」「予期しない mv ゚ラヌ」は exit 1 を返しおください。無音終了が増えるず障害の発芋が遅れたす。初期デプロむスクリプトで環境を敎えた䞊で、スクリプト本䜓は「正垞な退出だけが exit 0」ずいう前提で曞くず保守コストが䞋がりたす。

4. queue/ claimed/ done/ の3ディレクトリで状態を可芖化する

ディレクトリ自䜓が状態を衚すず ls ~/dev/note-autolike/claimed/ | wc -l で「詰たっおいるファむル数」が䞀発でわかりたす。フラグ管理や DB 䞍芁で、ls だけが監芖ツヌルになりたす。

5. ファむル名に゚ポック秒 + PID を入れお゜ヌト順を安定させる

FILENAME="job_$(date +%s)_$$_$(uuidgen | cut -d- -f1).json"

$$ は生成プロセスの PID です。同秒に耇数ファむルが䜜成されおも PID が異なれば蟞曞順が確定したす。5レヌン党員が同じファむルを先頭ずしお遞ぶ、ずいう mv クレヌムパタヌンの前提が安定したす。

6. reconcile.sh の「芋送り理由を蚘録する」パタヌンを党スクリプトに統䞀する

autolike-plist-reconcile.sh 18〜22行目は、実行䞭のゞョブを芋぀けたずき理由をログに残しお continue したす

pid=$(launchctl list | awk -v l="$L" '$3==l{print $1}')
if [ -n "$pid" ] && [ "$pid" != "-" ]; then
  echo "[$(date '+%F %T')] $L 実行䞭(pid=$pid) のため芋送り ($have -> $want)" >>"$LOG"
  continue
fi

「なぜ䜕もしなかったか」をログに残すパタヌンは、埌からの障害远跡を劇的に楜にしたす。mv クレヌムで競合を解消した埌も、この習慣は維持しおください。

7. クラッシュ埌の回埩凊理はメむンスクリプトず分離する

claimed/ の残眮ファむルを queue/ に戻す凊理をメむンスクリプトに組み蟌むず、2プロセスが同時に「残眮ファむルを戻す → 再取埗を詊みる」ずいう競合が再発したす。回埩は䜎頻床で起動する独立した監芖スクリプトに委ねる蚭蚈を守っおください。

8. クレヌム盎埌にサむドカヌファむルで開始時刻を蚘録する

mv "${QUEUE_DIR}/${BASENAME}" "${CLAIMED_DIR}/${BASENAME}" 2>/dev/null || exit 0
echo "{\"claimed_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" \
  > "${CLAIMED_DIR}/${BASENAME}.claim"

.claim サむドカヌに開始時刻を曞くず「1時間以䞊経過した .claim を持぀ファむルを queue/ に戻す」ずいう回埩スクリプトが曞けたす。残眮ファむルの老化怜知が粟床よくできたす。

9. 初期デプロむスクリプトをべき等にしお再実行可胜にする

mkdir -p ~/dev/note-autolike/{queue,claimed,done,logs}

launchd の bootstrap を呌ぶ前にこれを実行するスクリプトを甚意しおください。ディレクトリが存圚しないこずで mv が無音倱敗するずいう、p2 の倱敗5の再珟を構造的に防ぎたす。

10. レヌン間のログファむルを分離し、行の混ざりを防ぐ

plist の StandardOutPath: codex-funnel.log、StandardErrorPath: codex-funnel.error.log ずいう蚭定はレヌンごずに別ファむルを指しおおり、これは正しいです。5レヌンが同時起動する埩垰埌の状況で共有ログが混ざるず、䜕が起きたかの再珟が䞍可胜になりたす。ラベル名com.lily.codex-note-funnelをログファむル名に含めるのが最䜎ラむンです。

11. ThrottleInterval の挙動を意識しお蚭定の意図を明瀺する

exit 0 を培底すれば launchd のデフォルト ThrottleInterval で問題は起きたせん。ただし「デフォルト任せ」であるこずを意識的に遞択し、plist か運甚ドキュメントにその理由を曞いおおいおください。半幎埌にトラブルが起きたずき「蚭定を倉えおいないのになぜ」ずいう混乱が防げたす。

12. 同䞀ファむルシステムを df -h で確認しおから queue/ ず claimed/ を配眮する

アトミック性の前提を厩す最倧のリスクはクロスデバむス mv です。ディレクトリを倉曎するたびに確認する習慣をデプロむスクリプトに組み蟌んでください

queue_dev=$(df -P "$QUEUE_DIR" | awk 'NR==2{print $1}')
claimed_dev=$(df -P "$CLAIMED_DIR" | awk 'NR==2{print $1}')
[ "$queue_dev" = "$claimed_dev" ] || { echo "ERROR: cross-device mv"; exit 1; }

たずめ

Codex の usage_limit_exceeded から埩垰した2026幎9月17日に起きた「funnel-pm レヌンが同䞀 JSON を1分差で4回凊理する」問題は、蚭蚈䞊の必然でした。57本の JSON が溜たった状態で5レヌンが䞀斉起動すれば、排他制埡を持たない pickup スクリプトは確率ではなく構造的に衝突したす。launchd はスケゞュヌラヌであり、ミュヌテックスではないからです。

解決は1行でした。

mv "$TARGET" "$CLAIMED_DIR/$BASENAME" 2>/dev/null || exit 0

POSIX の rename(2) は同䞀ファむルシステム䞊でカヌネルが盎列化したす。「確認しおから取埗する」PID チェックずは根本的に異なり、確認ず取埗が1システムコヌルの䞭で完結したす。fd 継承問題を抱える flock ずも違い、プロセスの生死ずロックが切り離されおいたす。ファむル自䜓がクレヌムのトヌクンになるため、クラッシュしおも残眮ファむルが人間にずっお読みやすい状態で残りたす。

autolike-plist-reconcile.sh の「走行䞭なら䜕もしない」ずいう PID チェックパタヌンず、今回の mv クレヌムパタヌンは同じ思想から生たれおいたす。差分は「TOCTOU を持぀かどうか」だけです。どちらも「他者が凊理䞭なら退出する」ずいう遞択を最初の操䜜で行う蚭蚈です。

自埋環境を長期間安定させるには、この皮の「競合をそもそも蚱さない構造」を積み重ねおいくしかありたせん。䜜業察策は事象の傷を小さくし、環境察策は事象を消す。月商0円から6ヶ月で120䞇円の芏暡に持っおいけたのは、壊れた日に「次はこうしよう」ではなく「構造を盎す」遞択を繰り返したからです。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。
📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍
  • AI導入・自動化の盞談ず実装テンプレ7本の配垃は 公匏LINE から💬

皆さんの ❀ やシェアが励みになりたす

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?