【ITニュース解説】15 launchd Jobs and One Quota Circuit Breaker: Deciding What to Re-run Once the Circuit Closes
2026年09月18日に「Dev.to」が公開したITニュース「15 launchd Jobs and One Quota Circuit Breaker: Deciding What to Re-run Once the Circuit Closes」について初心者にもわかりやすく解説しています。
ITニュース概要
システムの使用量上限で止まったプログラムを再開する際、何をどう実行するかという設計が重要だ。記事では、適切なジョブを選び、タイムアウト処理で確実に停止させ、無駄なリソース消費を防ぎながら効率的に再開する方法を紹介する。多くのテストで不具合を防ぐ工夫もポイントだ。
ITニュース解説
このニュース記事は、AIサービスの利用制限(クォータ)に達した際にシステムが自動的に処理を停止する「サーキットブレーカー」を導入した後、さらに発生する複雑な課題について解説している。システムエンジニアを目指す初心者の皆さんも、将来システム運用に携わる際に直面する可能性のある、現実的で重要な問題なので、ぜひ理解してほしい。
まず、「サーキットブレーカー」とは、まるで電気回路のブレーカーのように、システムの負荷が高まりすぎたり、外部サービスの利用制限(クォータ)に達したりした際に、それ以上の処理を自動的に停止させる仕組みのことだ。これにより、システム全体がダウンするのを防ぎ、サービスの安定性を保つことができる。しかし、記事が指摘しているのは、ジョブ(プログラムの実行)を停止するだけでは「問題の半分しか解決していない」ということだ。
なぜ停止だけでは不十分なのか。記事の筆者は、claude-quota-guard.pyというスクリプトでクォータ枯渇を検知し、関連する15のジョブを一律に停止させていた。これは一見正しい対応に見えるが、問題はその後だ。クォータが回復し、停止状態(「サーキットが開いている」と表現されている)が解除され、「サーキットが閉じる」と、停止していたジョブは自動的に再開されない。もし、朝9時に実行予定だったジョブがクォータ不足でスキップされ、クォータが午前10時に回復しても、そのジョブの次の実行予定が「翌日の朝9時」だった場合、丸一日分の処理が失われてしまうことになる。この「スキップされたジョブを、クォータ回復後にいつ、どれだけ再実行するか」を決めるのが、新しく開発されたquota-catchup.pyというスクリプトの役割だ。
quota-catchup.pyは、再実行すべきジョブの候補を選ぶために、三つの条件をすべて満たすものだけを対象としている。
一つ目の条件は「このプログラム設定ファイル(plist)はガードされているか?」。macOSではlaunchdという機能を使ってプログラムを定期実行するが、その設定はplistというファイルに記述される。quota-catchup.pyは、claude-quota-guard.pyを経由して実行されるジョブだけを再実行の対象とする。これは、クォータ制限と関係のない通常の定期実行ジョブを誤って再実行してしまうことを防ぐためだ。具体的には、plistファイル内の実行コマンドにclaude-quota-guardという文字列が含まれているかどうかで判断している。
二つ目の条件は「今日の最新ログはスキップされているか?」。ジョブの実行結果はログファイルに記録される。quota-catchup.pyは、ジョブのログを調べて、その日の最新の実行記録が「スキップされた」ことを示している場合にのみ、再実行の候補とする。これにより、すでに正常に実行されたジョブや、何日も前の古いスキップ記録を持つジョブを誤って再実行してしまうことを防ぐ。過去のスキップを毎日再実行してしまうと、「ゾンビ状態」と呼ばれる無駄な処理が発生してしまうからだ。
三つ目の条件は「すでに実行予定時刻が過ぎているか?」。launchdジョブには、大きく分けて二種類の実行スケジュールがある。一つは「StartInterval」で、例えば「30分おき」のように一定間隔で実行されるもの。もう一つは「StartCalendarInterval」で、「毎日8時0分、10時30分」のように特定の日時に実行されるものだ。StartIntervalのジョブは、次に予定された時間になれば自然と再実行されるため、quota-catchup.pyによる特別な再実行は不要と判断される。一方、StartCalendarIntervalのジョブが再実行候補となるのは、「今日、少なくとも一つ以上の実行予定時刻がすでに過ぎている場合」だ。
この三つ目の条件であるcalendar_slot_passedの判断は特に重要で、複数の実行予定時刻がある場合のロジックが巧妙に設計されている。例えば、あるジョブが「午前9時0分」と「午後6時0分」に設定されているとする。もし現在が午後1時0分で、午前9時の実行がスキップされていた場合、このジョブは再実行の候補となるべきだ。しかし、もし「最初に見つかった実行予定時刻で判断する」というシンプルなロジックにしてしまうと、午前9時をスキップして、次に午後6時を見つけ、「これはまだ未来だから再実行しない」と誤判断する可能性がある。そのため、quota-catchup.pyは、今日設定されているすべての実行予定時刻をチェックし、その中に一つでも「すでに過ぎ去った時刻」があれば、そのジョブを再実行候補とする。これにより、正しい判断ができるようになる。
候補となるジョブが選定されたら、quota-catchup.pyはそれらを順次再実行していく。この際、launchctl kickstartというコマンドでジョブを起動し、その完了を待つ仕組みになっている。しかし、ここでも実際の運用で課題が見つかった。当初、ジョブの完了を待つタイムアウト時間は「1200秒(20分)」に設定されていたが、一部のAI生成系のジョブは実際に20分以上かかることが判明した。タイムアウトに達しても、単に「待つのをやめる」だけで、ジョブ自体は裏で動き続けてしまう。すると、次の再実行候補のジョブが起動され、前のジョブと同時に走ってしまう。これが繰り返されると、AI生成のような負荷の高い処理が同時に多数実行され、システムの「ロードアベレージ」(CPU負荷の指標)が40を超えるという深刻な事態が発生した。ロードアベレージ40は、システムが非常に高負荷状態であることを示しており、正常な動作が困難になるレベルだ。
この問題を解決するため、タイムアウト時間を「2700秒(45分)」に延長しただけでなく、タイムアウトした際には、単に待つのをやめるだけでなく、実行中のジョブを強制的に停止させる仕組みが導入された。具体的には、まず「SIGTERM」という、ジョブに「そろそろ終了してください」と優しく促すシグナルを送り、それでも終了しない場合は、「SIGKILL」という、ジョブを強制的に停止させるシグナルを送る。これにより、再実行するジョブが必ず一つずつ直列に処理されるようになり、システム負荷の異常な上昇を防ぐことに成功した。
このような複雑なロジックを開発するにあたり、筆者は18ものユニットテストを作成した。個人的な自動化スクリプトとしては多い数に思えるかもしれないが、これは「ナイーブな(素朴な)実装では間違えやすい境界条件」を事前に特定し、品質を保証するために非常に役立った。例えば、「再実行候補が一つもない日には、クォータが回復したかを確認するプローブ(調査処理)を起動してはいけない」というテストがある。これは、クォータ回復の確認自体がAIサービスへのアクセスを伴うため、空打ちでクォータを消費してしまうという自己矛盾を防ぐためのものだ。また、「古いスキップのログを再実行の対象にしない」ことや、「複数の実行予定時刻がある場合に正しい判断ができるか」、そして「スキップと成功のログが混在している場合に、最新のログで判断できるか」といった、細かいが非常に重要な判断ロジックが、これらのテストによって保証されている。特に、ログの最新性を判断するためには、ログファイルを逆順にスキャンすることで、常に最新の情報を取得できるように工夫されていた。
最終的に、この取り組みからいくつかの重要な教訓が得られた。クォータ制限によるサーキットブレーカーは、ジョブを「停止する」だけでなく、制限解除後に「何をどのように再実行するか」という設計まで含めて初めて完成する。再実行候補の選定には、関連のないジョブを巻き込まず、古すぎる情報に惑わされず、本当に再実行が必要なジョブだけを選び出すための精密な三段階フィルターが必要である。ジョブのタイムアウト時間は実測値に基づいて設定し、タイムアウト時には、単に待つのをやめるだけでなく、必ずそのジョブを強制終了させることで、システム全体の安定性を保つべきである。再実行候補が一つもない日には、回復確認のためのプローブ処理すら実行せず、無駄なクォータ消費を避けるべきである。そして、このような自動化スクリプトであっても、ユニットテストをしっかり書くことは、将来の変更で意図しないバグを混入させたり、自分で過去のミスを繰り返したりすることを防ぐために、非常に有効であるということだ。
このニュース記事は、システム開発において「これでよし」と一旦完成したかに見える機能でも、実際に運用してみると新たな課題が見つかること、そしてそれをいかに深く掘り下げて解決していくかという、システムエンジニアリングのリアルな側面を示している。