0
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?

🔄 「倱敗したらもう1回」は壊れたリトラむ――埅぀・諊める・戻すの3節で曞き盎す

0
Posted at

月商120䞇の自動化環境が、ある朝5本同時に止たっおいたした。ログを開いたら党郚に同じ行が䞊んでいた。「倱敗したのでもう1回実行したした」。もう1回も倱敗。もう1回も倱敗。最初の倱敗から63分が経っお、残っおいるのは最初の倱敗ずたったく同じ結果でした。

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

スクリプトをはじめお曞いたずき、リトラむはこういう圢をしおいるず思っおいたした。

for attempt in range(3):
    try:
        result = do_something()
        break
    except Exception as e:
        time.sleep(2)

これで合っおいたす。ネットワヌクが䞀瞬切れた、サヌバヌが䞀時的に重くなった、そういう倱敗なら確かにこれで盎る。問題は「次の詊行が成功するはずだ」ずいう前提を、すべおの倱敗に同じく圓おはめおいるずころです。

皌働しおいるゞョブが増えおくるず、この前提が厩れる倱敗が必ず珟れたす。

わたしが運甚しおいるSNS自動化環境で、2026幎8月8日から9日にかけおの2日間で、5本のレヌンから同時にリトラむ修正が䞊がっおきたした。症状はどれも䌌おいたしたが、原因はバラバラでした。

note-autolike のリラむトが3日間れロ件でした。 最終成功は 2026-08-06T09:03:20Z。蚘事タむトルに58字䞊限のバリデヌションがあるのに、生成プロンプトには字数制限が䞀蚀も曞かれおいなかったので、モデルは毎回63字のタむトルを返しおいた。3回投げお3回ずも63字。4回目は詊されるこずなく諊め凊理に入りたす。

ai-portraits の画像生成が63分空回りしおいたした。 モデレヌションでリク゚ストが匟かれたずき、プロンプトを段階的に穏圓にしお再詊行するフォヌルバックを3段実装しおいたした。1抂念あたり最倧21分、3抂念なら最倧63分。実枬では、察象コンテンツ自䜓がモデレヌションの匕っかかる原因のずき、Tierを䞋げおも壁は動きたせん。3段党郚を詊したずころで結果は1段目ず同じでした。

browser-slot.sh はグロヌバル枠3本に察しお1時間あたり18〜34本が起動を詊みたす。 1 runは14〜62分かかるので枠は垞に埋たっおいる。修正前の実装は、acquire に倱敗した瞬間に SKIP: global limit reached を出しお exit 0 しおいたした。ログに残っおいた数字は、09時台9件・11時台8件・12時台12件が起動すらしおいなかったずいうものでした。あるアカりントは圓日2回ずもskipで、いいねもフォロヌも0でした。

3぀ずも「もう1回」が実装されおいたしたが、どれも盎っおいたせん。

倱敗の皮類が違うからです。63字のタむトルは、同じプロンプトを同じモデルに投げれば同じ63字が返りたす。決定論的な倱敗です。モデレヌションブロックも、コンテンツが原因なら条件を倉えなければ䜕段詊しおも倉わらない。枠の䞍足は、誰かが枠を空けるたで埅぀しかない。

この分類がないリトラむ実装では、3皮類の倱敗がすべお「3回ルヌプ→終了」ずいう同䞀の凊理に吞い蟌たれたす。結果、盎せた倱敗も盎せない倱敗も同数カりントされお、最終的に同じ出口から出おきたす。

逆偎の壊れ方もありたす。「倱敗した」ずログに残っおいるのに、実際には䞀過性の障害だったケヌスです。わたしの watch-arb スクリプトは、商品ペヌゞの取埗に倱敗するず extract_failed ずだけ蚘録しおいたした。翌朝画面を芋るず「取埗できない店が16件ある」ず芋える。実際には HTTP 429 が返っおいただけで、同じ店を1件ず぀ゆっくり叩くプロヌバでは正しく取れおいたした。同期間䞭に確認した類䌌事象を䞊べるず、同じ429がログ䞊で7぀の別の名前で残っおいたした。

レヌン ログに残っおいた名前 実際の真因
watch-arb 商品ペヌゞ extract_failed HTTP 429
watch-arb bootstrap FATAL undici の10秒 ConnectTimeout
social-autolike ig-3 coverage 0/11 → 党゜ヌス取埗倱敗 → exit 1 IG private API の 429
social-autolike ig-sug following map取埗倱敗 → run停止 同じ 429
outreach-multi enrich å…šä»¶ error → qualify å…šä»¶ drop → 通算0通 web_profile_info が垞時 429
outreach-multi igJson unparsable json レスポンスが512,558文字あるのに400,000文字でslice

extract_failed ず蚘録されおいれば「この店の構造が特殊」ず読みたす。実際は「速く叩きすぎた」ずいう問題で、翌日ゆっくり叩けば取れる。名前が間違っおいたので、修正の方向を間違え続けおいたした。

これを敎理するず、正しいリトラむ蚭蚈は3぀の問いに答えるこずに収束したす。

(1) 埅おば盎るか。
(2) 䜕回で芋切るか。
(3) 次の詊行に䜕を倉えお枡すか。

「倱敗したらもう1回」はこの3぀をすべお省略した実装です。䞊の5本が同時に止たっおいた朝は、3぀ずも答えが曞かれおいたせんでした。

月商120䞇の自動化環境は、毎日コヌドを曞いおいるから動いおいるのではありたせん。自分が寝おいる間に環境が自己修正しお動き続けるから成立しおいたす。その前提に立぀ず、リトラむ蚭蚈の芁件が倉わりたす。「倱敗しおも次は盎る」ではなく、「倱敗の皮類ごずに次の行動を決める」こずが芁件になりたす。この蚭蚈がある実装ず無い実装では、障害の埩旧に自分が関䞎する必芁があるかどうかが根本的に倉わりたす。

党䜓の流れ

3぀の問いをひず぀の図で敎理したす。

倱敗が発生した
      │
      ▌
┌─────────────────────────────────────┐
│ (1) 埅おば盎るか                   │
│                                     │
│  枠䞍足 → 空くたで埅぀               │
│  ネットワヌク断 → バックオフしお再詊行 │
│  429 → Retry-After 優先で埅぀        │
│  ログむン切れ → 埅っおも盎らない      │
│  バリデヌション違反 → 埅っおも盎らない │
│  モデレヌション → 段による埌述    │
└─────────────────────────────────────┘
      │ 埅っおも盎らない
      ▌
┌─────────────────────────────────────┐
│ (2) 䜕回で芋切るか                 │
│                                     │
│  回数䞊限 or 受付から3時間超 → 打ち切り │
│  ただ詊せる → (3)ぞ                  │
└─────────────────────────────────────┘
      │ ただ詊せる
      ▌
┌─────────────────────────────────────┐
│ (3) 䜕を倉えお枡すか               │
│                                     │
│  同じ内容を枡す → 決定論的倱敗なら同じ結果 │
│  ゚ラヌ内容を添えお枡す → 修正できる倱敗なら回埩 │
│  Tier降栌瞊→ 条件を穏圓にしお再詊行  │
│  別課題に差し替え暪→ 課題ごず倉える  │
└─────────────────────────────────────┘

3節は順番に凊理するのではなく、倱敗の皮類によっお適甚する組み合わせが倉わりたす。ネットワヌク断なら(1)だけで十分です。決定論的なバリデヌション違反は(1)をスキップしお(3)から入りたす。モデレヌションブロックは(1)のスキップ・(2)の閟倀・(3)の倉え方が党郚関係したす。

(1) 埅぀ — 実装の骚栌

browser-slot.sh の修正で加えたコアは以䞋の圢です。

BROWSER_SLOT_WAIT_SEC="${BROWSER_SLOT_WAIT_SEC:-600}"

wait_for_slot() {
  local waited=0
  while ! try_acquire_slot; do
    local jitter=$(( RANDOM % 31 + 15 ))   # 15〜45秒ランダム
    sleep "$jitter"
    waited=$(( waited + jitter ))
    if [[ $waited -ge $BROWSER_SLOT_WAIT_SEC ]]; then
      log "RESULT waited=${waited}s SKIP: slot timeout"
      return 1
    fi
  done
  return 0
}

再詊行間隔を15〜45秒のランダムにしたす。 固定間隔にするず、枠を埅っおいる党ゞョブが同じ瞬間に殺到したすthundering herd。ランダムゞッタを入れるこずで、ゞョブが互いにずれお自然に分散したす。実枬で、固定間隔では党ゞョブが盎列化しおかえっお詰たるこずを確認しおから倉曎したした。

埅機䞭はガヌドを掎みっぱなしにしたせん。 取埗刀定の䞀瞬だけロックを持っお、刀定埌は必ず離したす。ここを間違えるず埅機䞭のゞョブが党郚ロックを持ち合っお党郚止たりたす。

ログの RESULT 行に waited=600s を残したす。 「600秒埅ったうえで諊めた」ず「即捚おた」は別の事象で、区別できないず埌から評䟡できたせん。

BROWSER_SLOT_WAIT_SEC=0 で修正前の即skip動䜜ず完党䞀臎したす。30本以䞊のlaunchdゞョブがこのスクリプトを共有しおいるので、埌方互換を維持しながら段階的に切り替えられるようにしたした。

䞀方、䞀過性に芋えお実は即再詊行で盎るケヌスもありたす。Cloudflare の Authentication error [code: 10000] で3日連続ゞョブが倱敗しおいた案件があり、「OAuth が倱効した → 人間の操䜜が芁る」に芋えおいたした。ずころがシェルず launchd 最小環境の䞡方でプロヌブ5本を実走するず党お exit 0。倱敗日のログを読み盎すず、アクセストヌクン再発行盎埌の0.5秒埌に同䞀トヌクンで別゚ンドポむントが 200 を返しおいた。トヌクン再発行盎埌の䞀過性 401 で、リトラむが1回も無いのが真因でした。倱敗率は 3/9日33%。「認蚌゚ラヌだから人間案件」ず即断するず、機械で盎せるものを人間の埅ち行列に積みたす。

(2) 諊める — 終端を必ず眮く

ai-portraits の Tier ラダヌを䟋にしたす。モデレヌションブロックの゚ラヌには moderation_stage ずいうフィヌルドがありたす。output で止たったものはTier降栌で通る実瞟がある。input から始たったものは、同期間䞭9詊行党滅でした。

for tier in [1, 2, 3]:
    result = generate(concept, tier=tier)

    if result.get("moderation_stage") == "input":
        # input段 → Tierを倉えおも壁は動かない、残りをbreak
        log(f"input-stage block, skip remaining tiers for this concept")
        break

    if result.get("moderation_blocked"):
        # output段 → Tier降栌で通る可胜性がある
        continue

    if result.get("success"):
        return result

梯子を降りる䟡倀があるかを1段目の゚ラヌ本文で刀定したす。 moderation_stage == "input" で break するだけで、63分の空走りが0分になりたす。同様に、gsheets_retry.py では 401/403/404 は即 raise するように蚭蚈されおいたす。

RETRYABLE_HTTP = {429, 500, 502, 503, 504}

def sheets_call_with_retry(fn, max_attempts=3):
    delay = 2
    for attempt in range(max_attempts):
        try:
            return fn()
        except HttpError as e:
            if e.resp.status not in RETRYABLE_HTTP:
                raise  # 401/403/404 は即 raise — リトラむで隠さない
            if attempt == max_attempts - 1:
                raise
            time.sleep(delay)
            delay *= 3  # 2秒 → 6秒合蚈最倧8秒
        except (socket.timeout, ConnectionError, TimeoutError):
            if attempt == max_attempts - 1:
                raise
            time.sleep(delay)
            delay *= 3

「合蚈最倧8秒を絶察に超えない」を先に決めたす。 指数バックオフを回数で決めるず、呌び出し偎のスロット時間を静かに食いたす。秒数で䞊限を固定しおから回数を逆算する順番が安党です。

回数だけで「諊める」を実装するず、時刻軞の障害に察応できたせん。kotonoha のゞョブキュヌで 39時間 pending になる事故がありたした。䞊限6回で䞭断しおも requeue が成功するので倱敗が䞀床も立たず、監芖も鳎らず、利甚者の画面は「䜜成䞭」のたたでした。

修正埌の刀定匏はこうなりたす。

function shouldGiveUpForQuota({ quotaHits, createdAtMs, resumeAtMs }) {
  const THREE_HOURS_MS = 3 * 60 * 60 * 1000;
  return (
    quotaHits >= 2 ||                                  // 2回目の䞊限
    resumeAtMs > createdAtMs + THREE_HOURS_MS          // 受付から3時間超
  );
}

「回数」ず「受付からの絶察時刻」の二重で打ち止めを眮きたす。 事故圓日の実倀受付 2026-08-19T00:36:57Z・再開 06:37:00Zを境界ケヌスずしおテストに入れるず、同じ事故は二床ず通らないこずが機械で保蚌されたす。

(3) 戻す — 次の詊行に倉えた内容を枡す

note-autolike の䟋です。リラむトが3日間れロ件だった原因は、同䞀プロンプトを3回投げ盎すだけの実装でした。゚ラヌを機械可読にしお、2回目以降の詊行には修正指瀺を付けお枡したす。

// ゚ラヌに code / actual / limit を茉せる
throw new ValidationError(`タむトルは1〜58字にしおください実際: ${actual}字`, {
  code: "title-too-long",
  actual,
  limit: 58,
});

// 2回目以降は修正指瀺を付けお投げる
function rewriteRetryInstruction(error) {
  if (error.code === "title-too-long") {
    return [
      `前回の出力は次の理由で䞍採甚でした`,
      `タむトルが${error.actual}字あり、䞊限の${error.limit}字を超えおいたす。`,
      `同じ内容・同じ構成のたた、その点だけを満たす圢ぞ盎しお、生JSONのみを再出力しおください。`,
      `タむトルは絵文字蟌みで${error.limit}字以内前回は${error.actual}字に必ず収めおください。`,
    ].join("\n");
  }
}

async function askClaudeWithRetry(prompt, retries = 3) {
  let currentPrompt = prompt;
  for (let i = 0; i < retries; i++) {
    try {
      const result = await callClaude(currentPrompt);
      validateRewrite(result);
      return result;
    } catch (e) {
      if (e instanceof ValidationError) {
        // 決定論的な倱敗 — スリヌプなし、修正指瀺を付けお即再詊行
        currentPrompt = prompt + "\n\n" + rewriteRetryInstruction(e);
      } else {
        // プロセス系の倱敗 — 30秒埅぀
        await sleep(30_000);
      }
    }
  }
}

決定論的な倱敗はスリヌプを挟たず即再詊行したす。 バリデヌション違反に2秒埅぀のは、2秒を捚おおいるだけです。「埅぀」ず「盎しお投げ盎す」を同じリトラむで混ぜるず、盎せる倱敗にも埅ち時間を払いたす。倱敗の皮類で分岐しお、スリヌプを入れる堎所を遞びたす。

LLMぞ返す修正指瀺は「機械の識別子」ではなく「人間語の指瀺珟物の文字列」で枡したす。"title-too-long" をそのたた枡すず3連敗したす。「タむトルが63字あり䞊限の58字を超えおいたす、58字以内に収めおください」ず枡すず回埩したす。これは別のスクリプトで先に実蚌枈みの事実です。

3節の党容はここで䞀旊止めたす。次は各節の「壊れ方」ず「盎し方」を、5レヌン同時障害から拟ったケヌス別に分解しおいきたす。

実装の詳现

「リトラむを曞いた」は完了ではない ── フォルト泚入を同じコミットに入れる

前半で蚭蚈した3節は、曞いた時点では「動くはずのコヌド」でしかありたせん。間欠障害は再珟手段がないので、リトラむがその経路を実際に通るかどうかを確かめる方法がデフォルトでは存圚したせん。最初の4本の実装はそれで終わっおいたした。git diff を芋お確認した気になっお、完了にしおいたした。

実際に効いたのは、フォルト泚入を実装本䜓ず同じコミットに同梱するこずでした。

// watch-arb/collector/lib/api.mjs
let _failOnceRemaining = parseInt(process.env.WA_API_FAIL_ONCE ?? '0', 10);

async function req(path, opts = {}) {
  if (_failOnceRemaining > 0) {
    _failOnceRemaining--;
    throw new TypeError('fetch failed');  // 合成の障害、本番では絶察に発火しない
  }
  // ... 本来のfetch凊理
}

カりンタはモゞュヌルスコヌプで党 req 呌び出し共通です。プロセス起動埌の最初のN回だけ、fetch を呌ぶ前に合成の TypeError を投げたす。

完了条件は2本セットで必ず䞡方確認したす。

  • ① WA_API_FAIL_ONCE=2 で起動しお api retry attempt=1/3 ず 2/3 が出たうえで FATAL なく exit 0 で完走する
  • ② 環境倉数未蚭定なら retry ログが1行も出ない

②があるこずで「フォルト泚入が本番に挏れおいない」こずを同じコマンドで瀺せたす。実枬で出たログはこうなりたした。

[INFO] api retry attempt=1/3 path=/api/collector/shops reason=fetch failed
[INFO] api retry attempt=2/3 path=/api/collector/shops reason=fetch failed
[INFO] 32件を ingest しお exit 0

この型は watch-arb から zaiko-radar・lily-line-funnel・social-autolike ず暪展開しお4本になりたした。「リトラむを曞いた」は「フォルト泚入蟌みで走ったずころを芋た」でなければ完了にしない、ずいうルヌルに倉わりたした。

再詊行する察象を1関数に閉じる

リトラむを曞くずき、最初に決めるこずは「䜕回詊すか」ではなく「䜕を再詊行するか」です。

// 刀定を1関数に集玄する
function isTransientStatus(status) {
  return status === 429 || (status >= 500 && status <= 504);
}

async function req(path, opts = {}) {
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
    try {
      const res = await fetchWithTimeout(path, opts);
      if (!res.ok) {
        if (!isTransientStatus(res.status)) throw new HttpError(res.status);
        if (attempt === MAX_ATTEMPTS) throw new HttpError(res.status);
        await sleep(backoff(attempt));
        continue;
      }
      return await res.json();
    } catch (e) {
      if (!isNetworkError(e)) throw e;  // ネットワヌク䟋倖以倖は即throw
      if (attempt === MAX_ATTEMPTS) throw e;
      await sleep(backoff(attempt));
    }
  }
}

401/403/404 を即 throw するのは省力化ではなく、別の事故を防ぐための蚭蚈です。暩限゚ラヌをリトラむで包むず、同じリク゚ストを3回投げお3回ずも同じ倱敗が返りたす。ログには「3回倱敗した」ずだけ残り、「暩限が無い」ずいう真因は埋もれたす。

transient-failure-recorded-as-permanent.md で蚘録した「7぀の名前で残った429」ず逆向きの事故です。429が extract_failed に化けるのず同じ構造で、401/403 が「たたに遅い」に化けたす。

isTransientStatus を1関数に出す理由は、刀断が耇数の呌び出し箇所に散るず堎所ごずに埮劙に違う定矩が生たれるからです。gsheets_retry.py の RETRYABLE_HTTP = {429, 500, 502, 503, 504} ずいう定数セットも同じ発想で、「どのステヌタスがリトラむ察象か」ずいう知識を1箇所に閉じおいたす。

リトラむを入れるず䟋倖の䌝わり方が倉わる

kotonoha に withRetry を実装した翌コミットで、副䜜甚の修正が3件入りたした。「機胜を远加した」ず思っおいたしたが、実際には「䟋倖の䌝わり方を倉えた」倉曎でした。

倉曎前、purge() は 5xx が返っおも埌続の「手元の䜜業ディレクトリ30日掃陀」たで凊理が進んでいたした。res.ok が false でも次の行ぞ進む実装になっおいたからです。withRetry を入れお5xx の瞬間に throw するようになるず、掃陀のコヌドがそこで到達䞍胜になりたす。実害は「䜜業ディレクトリが30日分蓄積する」ずいう、定期的に手動掃陀しなければ気づかない皮類の静かな劣化でした。

async function purge(jobId) {
  try {
    await uploadResult(jobId);
  } catch (e) {
    log(`purge upload failed: ${e.message}`);
    // ここで throw しない — 眮き堎の倱敗は蚘録しお続行する
  }
  await cleanWorkDir();  // 䟋倖の有無にかかわらず必ず走らせる
}

もう1件は setContentSafely で、1回目ず2回目の再詊行の間が0秒でした。リ゜ヌス逌迫で萜ちた盎埌に同条件で即再詊行しおも、圧力が残っおいる間は同じ結果になりたす。前半で「決定論的な倱敗はスリヌプを挟たず即再詊行」ず曞きたしたが、その逆偎です。資源逌迫が原因の倱敗は、埅たないずリトラむに意味がありたせん。

詊行回数は「倱敗したずき䜕が起きるか」で非察称に蚭定しおいたす。report()結果報告は倱敗するず利甚者の画面が「実行䞭」のたた氞久に止たるので attempts=5、claim()予玄取埗は attempts=3。䞀埋の回数はコストず被害の非察称を無芖したす。タむムアりトも操䜜の重さで分けおいお、putObject ず getStream は120秒、getText ず listKeys は30秒です。タむムアりト蚭定のない fetch は「氞久に吊られる」ずいう最悪のリトラむ䞍胜状態を䜜りたす。

ストリヌムを枡すずリトラむできない

実装するたで意識しおいなかった制玄がありたす。リトラむ可胜にするには、入力が「再実行可胜」でなければなりたせん。

// NG: ストリヌムは䞀床消費したら再生できない
async function retryWithStream(stream) {
  for (let i = 0; i < 3; i++) {
    await fetch(url, { body: stream });  // 2回目はストリヌムが空
  }
}

// OK: Buffer か string に限定する
async function retryWithBuffer(buffer) {
  for (let i = 0; i < 3; i++) {
    await fetch(url, { body: buffer });  // 䜕床でも同じデヌタを送れる
  }
}

putObject に枡す body を Buffer か string に限定したした。型制玄をコメントで残しおおかないず、埌から呌び出し偎がストリヌムを枡しお「リトラむが動いおいるのに毎回倱敗する」バグになりたす。ストリヌムをリトラむに枡すず2回目以降は空のボディを送り続けたす。゚ラヌも出ずに毎回倱敗したす。


私が詰たった話

防埡のために眮いた定数が、成功率0%の壁になっおいた

outreach-multi の DM 受信箱パヌス凊理で、unparsable json ずいう゚ラヌが毎日出おいたした。セレクタが壊れたか、JSON の構造が倉わったかず疑っお3日間修正し続けたした。

実態は、受信箱取埗のコヌドに text.slice(0, 400_000) ずいう行がありたした。巚倧なレスポンスが来たずきにメモリを保護するために曞かれた䞊限でした。IG の DM 受信箱の実サむズを蚈枬したら 512,558文字 でした。䞊限が 400,000 なので、毎回必ず途䞭で切れたす。切れた JSON は JSON.parse で毎回必ず倱敗したす。成功率は0%です。

3日間同じ倱敗を芋続けおいたのは、text.slice の行を忘れお「パヌスに倱敗しおいる → 構造が壊れた」ず読んでいたからです。症状が出始めたのも「今日から」ではありたせんでした。受信箱のサむズが 400,000字を超えた日から静かに倱敗し始めお、ある日気づいたら通算0件になっおいた圢です。

修正は page.evaluate の䞭で JSON.parse たで完了させ、倱敗したずきだけ先頭300字を蚺断甚に返す圢に倉えたした。

この件から埗た蚭蚈ルヌルは「䞊限を眮くなら、䞊限に圓たったこず自䜓をログに出す」です。text.slice はサむレントに切り萜ずすだけで、切れたこずを誰にも知らせたせん。防埡の定数がその定数を超えた瞬間から恒久的な倱敗に倉わっおいおも、ログには䜕も残りたせん。

quarantine が氞久ルヌプになっおいた

unfollow-core.js のアンフォロヌ凊理に、3回倱敗するず7日間の保留quarantineに入る仕組みがありたした。

問題は「quarantine から埩掻したらどうなるか」です。埩掻したアカりントは候補リストに戻りたす。再床3回倱敗するず、たた7日間の保留に入りたす。これが無限に回りたす。

候補が 2,605件あっお、scanned: 8 / unfollowed: 2 ずいう数字を芋お2週間悩みたした。候補がほが凊理されおいない。「なぜ進たないのか」を調べたら「同じアカりントが quarantine に入っおは出るを繰り返しおいお、前に進んでいない」でした。

ログには circuit-break ず出おいたした。circuit-break が機胜しおいるのに実行数が増えないのは矛盟したす。もっず早く気づけたはずでしたが、「circuit-break が動いおいる = 保護されおいる」ず読んでいたので疑いたせんでした。

修正は、环蚈倱敗回数UNFOLLOW_GIVEUP_FAILS = 6、quarantine 倱敗閟倀の2倍に達したアカりントを台垳から物理削陀するこずでした。

「保留」は溜たるだけで枛りたせん。 捚おる終端を必ず眮かなければ、台垳は凊理できないアカりントで埋たり続けたす。ただし捚おる前に「自分の凊理が正垞に動いおいるか」の確認が先です。セッションが切れおいただけで、正垞なアカりントを党郚捚おおしたう逆事故があり埗たす。kotonoha のペヌゞに曞いた「捚おる前に自分の正垞性を確認する」ずいう原則がここにも適甚されたす。

監芖が39時間鳎らなかった

kotonoha のゞョブが39時間ずっず「䜜成䞭」のたた止たっおいたした。監芖は䞀床も鳎りたせんでした。ゞョブは倱敗ずしお蚘録されおいたせんでした。

原因は「回数を状態に持っおいないリトラむには、打ち止めが眮けない」でした。

ゞョブがクォヌタ䞊限に圓たるず requeue で pending に戻したす。戻したゞョブは「䜕回目か」ずいうカりンタを持っおいたせんでした。7回目のゞョブも1回目のゞョブも実装から区別できたせん。requeue が成功するので倱敗も立ちたせん。監芖の閟倀にも圓たりたせん。利甚者の画面には「䜜成䞭」が延々ず衚瀺され続けたす。

修正は2段階でした。たず「回数を状態に持たせる」。次に「回数ず受付からの絶察時刻の二重で打ち止めを眮く」。前半で瀺した shouldGiveUpForQuota 関数を境界ケヌス付きのテストに通すずき、事故圓日の実倀受付 2026-08-19T00:36:57Z・再開予定 06:37:00Zを1ケヌス入れたした。同じ事故は二床ず通らないこずが機械で保蚌されたす。

回数だけで打ち止めを眮いおいるず「1回目だが再開が12時間先」を通しおしたいたす。時刻だけだず「再開は短いが䞊限が連続しおいる」を通しおしたいたす。今回の事故は時刻条件の偎で1回目に捕たる圢でした。二重にした意味が実デヌタで枬れた件です。

「ワヌカヌが埅぀」ず「そのゞョブを諊める」は別の刀断です。クォヌタは埅おば盎る倱敗なのでワヌカヌは埅ちたす。でも利甚者を3時間以䞊埅たせる根拠にはなりたせん。

「認蚌゚ラヌ」ず即断しお3日間人間埅ちにしおいた

com.lily.line-pdca が Cloudflare の Authentication error [code: 10000] で3日連続止たりたした。認蚌゚ラヌなので「OAuth が倱効した、手動で wrangler login が芁る」ず刀断したした。環境倉数の蚭定挏れも疑いたした。

確かめる前に「人間のGUI操䜜が芁る案件」ずしお分類したのが間違いでした。

実際に shell ず launchd 最小環境の䞡方で wrangler whoami・kv key list・deploy --dry-run を実走するず党お exit 0 でした。出力バむト数たで同䞀1,516 bytes。倱敗日のログを読み盎すず、アクセストヌクンを再発行した 0.5秒埌 に KV keys が 401 を返しおいたした。その同じトヌクンで0.5秒埌の /user は 200 を返しおいたした。トヌクン再発行盎埌の䞀過性 401 で、リトラむが1回もないのが真因でした。

倱敗率は 3/9日玄33%でした。毎日出ない、でも3日に1回は出る。この頻床が「人間案件に芋える」原因でもありたした。

刀断を倉えた基準は「同じ手順を今もう䞀床叩いたら通るか」を実際に叩いお確かめるこずでした。゚ラヌの皮類401・Authentication errorだけで「絶察に盎らない」ず分類しない。transient-failure-recorded-as-permanent.md に蚘録した「429が7぀の名前で残っおいた」ず同じ型で、今床は「認蚌゚ラヌ」ずいう名前が䞀過性の倱敗を人間案件に芋せおいたした。

この件以降、認蚌゚ラヌが出たらプロヌブを1本走らせおから刀断するルヌルに倉わりたした。「機械で盎せるものを人間の䜜業埅ちキュヌに積たない」の前提は「機械で叩いお確かめた」です。確かめないたた即断するのは、叩かずに「盎らない」ず決め぀けおいるのず倉わりたせん。

぀たずきポむント

前段で取り䞊げた個別事䟋の裏にある構造的なパタヌンをたずめたす。「詰たった堎所の名前」が違うだけで、根っこは同じ型です。

倱敗を「ひず぀の名前」に圧瞮しお蚘録する

watch-arbの商品ペヌゞはextract_failed、bootstrap実行はFATAL、DM受信箱はunparsable jsonず蚘録されおいたした。3぀ずも実際の真因はHTTP 429です。transient-failure-recorded-as-permanent.mdに蚘録したこの事象を敎理するず、同期間䞭で同じ429が7皮類の別名で残っおいたした。名前の時点でHTTPステヌタスが消えるず、「埅おば盎る」なのか「構造を盎す必芁がある」のかの刀断ができなくなりたす。修正は1行です。蚘録する前に429/503 → rate_limited / 401/403 → auth_error / それ以倖 → extract_failedず分類したす。この3分岐が「埅぀・諊める・戻す」の分岐の手前にある前提条件です。

゚ラヌ本文を捚おおいる

ai-portraitsのgenerate.pyがcodex execのstdout/stderrを捚おおいたため、画像が出なかった理由がmissingずしか残りたせんでした。moderation_blockedもsafety_violations: ["sexual"]も芋えなければ、Tier降栌で回埩するのかinput段で即打ち切るべきかの刀断が立ちたせん。゚ラヌを保持しない実装では「埅぀/諊める/戻す」の3節がそもそも成立しないので、゚ラヌ保持は蚭蚈の前提であっお機胜远加ではありたせん。

moderation_stageを確認せずに党Tierを詊す

moderation_stage: inputで止たった抂念は、T1からT3たで党段詊しお9詊行党滅でした。moderation_stage: outputで止たったものはTier降栌で通る実瞟がありたすpartial 01_feet_up_windowはT2、03_legs_on_bed_topdownはT3。T1のmoderation_stageフィヌルド1語で「梯子を降りる䟡倀があるか」が決たるのに、フィヌルドを読たずに党段を詊すず1抂念あたり最倧21分、3抂念で最倧63分が無駄になりたす。「安党偎フォヌルバックが䜕段あるか」より「1段目の゚ラヌ本文で降りる䟡倀があるか」を先に刀定したす。

降り先の容量を枬らずに配線する

social-autolikeのfollow-source-followersでフォヌルバックを远加する前に、UIモヌダルのfollowing偎が408ä»¶/63秒で䌞びるのに察しおfollowers偎が12件で止たるこずを先に実枬したした。容量がある偎だけをフォヌルバックに採甚したのが理由です。枬らずに配線するず、容量のない降り先に党トラフィックが流れお「降り先を远加したのに成功率が倉わらない」ずいう状況になりたす。outreach-multiのpk解決も、IGが70件前埌で絞るずいう既知の壁を織り蟌んで1run 30件制限にしおいたす。

恒久的な倱敗を「あず1回」の枠に数える

outreach-multiのDM送信でno メッセヌゞ buttonDM䞍可の蚭定が返るず、exhausted after 3 attemptsに乗っお送信枠を1件あたり3回分消費しおいたした。盞手のアカりント蚭定はこちらがリトラむしおも倉わりたせん。sent.jsonlのreasonフィヌルドを読んで、no メッセヌゞ button / profile gone / このペヌゞは存圚したせんを含む宛先はpermanentlyFailed Setに入れ、shouldSkip()でexhaustedの手前に刀定を眮きたす。これは前段で觊れた「䞀過性を恒久ずしお蚘録する逆向きの事故」ず同じ根で、倱敗の分類の蚭蚈がリトラむ回数の蚭蚈より先に芁りたす。

429のあずも同じ速床で叩き続ける

social-autolikeのig-3は0.3〜0.5秒間隔で17゜ヌスを数秒で撃ち蟌んで14時間れロ成果でした。429を受けた埌も間隔を倉えなければ、制限䞭に同じリク゚ストを積み䞊げお自分でスロットリングを延長したす。retry-and-giveup-design.mdに蚘録した修正では、3連続429でそのプロセスのAPI呌び出しを打ち切るラッチず、429を1床でも芋た店のconcurrencyを1に萜ずす蚭蚈を入れおいたす。埅っおから再詊行するだけでなく、速床そのものを萜ずすこずがセットです。

リトラむが2局に重なっおいる

watch-arbのingestShopが独自に3回ルヌプを持ち、req()偎にも3回ルヌプがありたした。最悪3×3=9回になり、ログに「retry attempt 3/3」が2床䞊んで、䞊限がどこにあるのか、今䜕回目なのかが読めたせん。フォルト泚入を入れおも「どちらの局が動いおいるか」が分からなくなりたす。修正はingestShop偎の独自ルヌプを削陀しおreq()の1局に䞀本化したした。呌び出し偎は即throwするだけにしたす。

フォヌルバックの成功を打ち切りカりンタに数える

「3連続429でAPI呌び出しを打ち切る」ラッチが、UIフォヌルバックが成功した件もカりントに含めおいたした。逃げ道を䜜っおも3件で止たっおいたした。打ち切りカりンタは「党経路が倱敗したずき」だけ増やしたす。フォヌルバック先で成功した件は、メむンの倱敗カりントに茉せたせん。

「取れなかった異垞」ず「取りに行く時間がなかった正垞な打ち切り」を同じれロ件で刀定する

follow-source-followersは゜ヌス取埗だけで時間予算を䜿い切っお候補れロ件になった堎合、既存の「党゜ヌス取埗倱敗→誀操䜜防止で停止」経路に入っおしたいたす。取れなかったのか時間がなかっただけなのかは別の事象です。同じれロ件で刀定するず毎日誀報が鳎り、本物の倱敗を芋分けられなくなりたす。time-budgetずいう別出口を甚意しお区別したす。

諊める刀定匏をcatchブロックにベタ曞きする

打ち止めの条件がcatchの䞭に曞かれおいるず、境界ケヌスのテストに茉せられたせん。玔粋関数に出すず事故圓日の実倀を1ケヌス入れられ、同じ条件が二床ず通らないこずを機械で保蚌できたす。ロゞックがcatchの䞭にある限り、テストは「たたたたケヌスが通らなかった」状態で合栌したす。

゚ラヌメッセヌゞに機械可読な情報が茉っおいない

ValidationErrorを文字列だけで投げるず、再詊行時に「䜕が悪かったか」をコヌドから取り出せたせん。2回目以降に同じプロンプトを投げるず同じ結果になるのは「戻す」が実装されおいないからで、code: "title-too-long", actual: 63, limit: 58のような構造を茉せるず修正指瀺を動的に組み立おられたす。LLMぞ返すずきは識別子"title-too-long"でなく人間語の指瀺文ず実枬倀を枡したす。識別子をそのたた枡すず3連敗したす。


ベストプラクティス

5レヌン同時障害から蒞留したルヌルを列挙したす。数が倚いですが、党郚「実際に詰たったから入れた」ものです。

① 倱敗は蚘録する前にHTTPステヌタスず䟋倖の型で分類する

429/503 → rate_limited / 401/403 → auth_error / それ以倖 → 凊理系゚ラヌ。この3分岐を先に眮くだけで、画面に出る「倱敗しおいる店の䞀芧」の意味が倉わりたす。watch-arbはこの分類を加えた埌、shops.last_errorの文蚀も「レヌト制限で取埗できず」ず「構造が取れない」に分けたした。

② リトラむを曞いたら、フォルト泚入を同じコミットに入れる

let _failOnceRemaining = parseInt(process.env.WA_API_FAIL_ONCE ?? '0', 10);
// 起動埌の最初のN回だけ合成の゚ラヌを投げる

完了条件は2本セットです。=2でretry attempt=1/3ず2/3が出おexit 0で完走するこず、未蚭定なら retry ログが1行も出ないこず。②がないず「フォルト泚入が本番に挏れおいない」こずを同じコマンドで確認できたせん。

③ 打ち止めを「回数」ず「受付からの絶察時刻」の二重で眮く

回数だけだず「1回目だが再開が12時間先」を通したす。絶察時刻だけだず「再開は短いが䞊限が連続しおいる」を通したす。kotonohaの事故は時刻条件の偎で1回目に捕たる圢でした受付2026-08-19T00:36:57Z・再開06:37:00Zが受付+3時間の倖。二重にした意味が実デヌタで枬れた件です。

④ 埅ちの合蚈䞊限を秒で先に決め、回数を逆算する

gsheets_retry.pyは「合蚈最倧8秒を絶察に超えない」を䟝頌文に固定しおから、2秒→6秒3回を逆算したした。指数バックオフを回数で先に決めるず、呌び出し偎のスロット時間が静かに䟵食されたす。

â‘€ 401/403/404は即throwしおリトラむで隠さない

if e.resp.status not in RETRYABLE_HTTP:
    raise  # 暩限゚ラヌは即 raise

暩限゚ラヌをリトラむで包むず「3回倱敗した」ずだけ残り、「暩限がない」ずいう真因が埋もれたす。transient-failure-recorded-as-permanent.mdで蚘録した「429がextract_failedに化ける」ず同じ構造で、今床は401が「たたに遅い」に化けたす。

⑥ moderation_stageで梯子を降りる䟡倀があるか刀定しおからTierを詊す

if result.get("moderation_stage") == "input":
    log("input段の拒吊。残りTierをbreak")
    break

1行加えるだけで63分の空走りが0分になりたす。outputで止たったものはラダヌを回す䟡倀がありたす。inputで止たったものは抂念ごず捚おお次に進みたす。

⑩ リトラむの局を1箇所に集玄する

呌び出し偎が独自ルヌプを持ちラむブラリ偎にも再詊行があるず最悪N×M回になりたす。重耇を芋぀けたら倖偎のルヌプを削陀しおreq()の1局に䞀本化したす。

⑧ 降り先の容量を枬っおから配線する

フォヌルバックを远加する前に、実枬で「この降り先が䜕件たで返すか」「どこで詰たるか」を確認したす。容量のない降り先に党トラフィックを流しおも成功率は倉わりたせん。

⑹ 恒久的な倱敗は氞続リストに1回で蚘録しお陀倖する

no メッセヌゞ button / profile goneのような盞手偎の蚭定に起因する倱敗は、リトラむ回数を消費する前に陀倖したす。shouldSkip()の刀定をexhaustedの手前に眮きたす。

⑩ 「今もう䞀床叩いたら通るか」を実際に叩いおから人間案件に分類する

Cloudflare Authentication errorを「OAuthが倱効した」ず即断しお3日間人間の埅ち行列に積んでいたした。shell ず launchd 最小環境の䞡方でプロヌブを実走するず党おexit 0でした。゚ラヌの名前だけで「絶察に盎らない」ず分類しない。確かめおから刀断したす。

⑪ 䞊限定数が圓たったこず自䜓をログに出す

text.slice(0, 400_000)はサむレントに切り萜ずすだけでした。防埡の定数が恒久的な故障の匕き金になっおいおも、ログには䜕も残りたせん。JSON.parseたで内郚で完了させ、倱敗したずきだけ蚺断甚に先頭300字を返す圢にするず、「切れた」ずいう事実が蚘録に残りたす。

⑫ リトラむ導入埌は䟋倖の䌝播範囲の倉化を確認する

withRetryを入れるず「握り぀ぶし→throw」に倉わった箇所の䞋流にある必須凊理が到達䞍胜になるこずがありたす。リトラむは機胜の远加ではなく、䟋倖の䌝わり方を倉える倉曎です。䞋流に「倱敗しおも必ずやるべきこず」がある箇所は個別に囲み盎したす。

⑬ 資源逌迫が原因の倱敗は、埅っおから再詊行する

setContentSafelyの再詊行間隔がれロ秒では、高負荷で萜ちた盎埌に同条件でもう䞀床萜ちたす。決定論的な倱敗はスリヌプなし即再詊行が正解ですが、その逆偎ずしお、資源を埅たないずリトラむに意味がありたせん。倱敗の皮類でスリヌプを入れる堎所を遞びたす。

⑭ 諊める刀定匏を玔粋関数に出しおテストに事故圓日の実倀を入れる

shouldGiveUpForQuota({quotaHits, createdAtMs, resumeAtMs})ぞ切り出しお、受付2026-08-19T00:36:57Z・再開06:37:00Zを1ケヌスずしお入れるず、同じ事故は二床ず通りたせん。catchブロックにベタ曞きされた刀定は境界ケヌスをテストできたせん。

⑮ 埅った事実・諊めた事実・打ち切った事実をログの1行に残す

waited=600s SKIP: slot timeoutずSKIP: global limit reached即捚おは別の事象です。どちらも同じ「SKIP」に芋えるず、「枠の競合が増えた」なのか「もずもず即捚おしおいた」なのかを埌から区別できたせん。投資した埅ち時間は数倀でログに出したす。


たずめ

「倱敗したらもう1回」ず曞いたずき、そのコヌドには3぀の問いの答えがありたせん。埅おば盎るか、䜕回で芋切るか、次の詊行に䜕を倉えお枡すか。この3぀が揃っおいない実装は、決定論的な倱敗の前で資源を焌き続けお止たりたす。

5レヌンが同時に止たった朝に䞊べお気づいたのは、障害の埩旧に自分が関䞎する必芁があるかどうかが、この蚭蚈の有無でほが決たるずいう点です。蚭蚈があれば、スクリプトは倱敗の皮類ごずに次の行動を自分で決めたす。蚭蚈がなければ、同じ倱敗が3回蚘録されるだけで、翌朝も同じ堎所に詰たりたす。

月商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 から💬

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

0
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
0
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?