AIパートナー George が設計した BudgetGuard — コスト超過を検知して処理を止める"やめる壁"。諦める選択を実装に落とし込んだ話。株式会社ツクルンの開発現場から。
諦める勇気の実装 — BudgetGuard という"やめる壁"を作った日
今回の登場人物
George(ジョージ)
AI パートナー / 総合プロデューサー
チームの中心。コスト設計から実装まで、技術の要を担う。今回の BudgetGuard も彼の手で生まれた。
「George、コストが危ない」と気づいた朝があった。
ジョージが担当する album-sweet というプロジェクト。そこで動く非同期処理が、API を「まだか、まだか」と延々と叩き続けていた。応答待ちのポーリング処理が想定を超えた頻度でサーバーを叩き、504 エラーが積み重なり、WM アラートが鳴り続けていた。
「止めさせる壁がなかった」——ジョージはそう分析した。
BudgetGuard という解決策
ジョージが実装したのは、シンプルな「時間予算」の概念だ。処理を開始したとき、時計を押さえる。そして、経過時間が予め決めた予算(budget)を超えたら、潔く諦める。例外を投げて停止する。
コードの骨格
class BudgetExceededException extends \RuntimeException {}
class BudgetGuard {
private float $budget;
private float $startTime;
public function __construct(float $budget) {
$this->budget = $budget;
$this->startTime = microtime(true);
}
public function check(): void {
if (microtime(true) - $this->startTime > $this->budget) {
throw new BudgetExceededException(
"budget={$this->budget}s elapsed"
);
}
}
}
なぜ RuntimeException を継承するのか。 PHP の例外には「チェック例外」と「非チェック例外」の概念がある。RuntimeException は非チェック例外で、呼び出し側に catch を強制しない。BudgetGuard の「諦める」という判断は、ビジネスロジックのエラーではなく実行環境の制約だ。catch するかどうかは呼び出し側が決めればいい。Exception 基底クラスより RuntimeException を選ぶことで、その意図を明示している。
呼び出し側の実装
$guard = new BudgetGuard(30.0); // 30秒の予算
try {
while ($queue->hasNext()) {
$guard->check(); // 予算を超えたら BudgetExceededException を投げる
$item = $queue->next();
$api->fetch($item);
}
} catch (BudgetExceededException $e) {
Log::warning('budget exceeded: ' . $e->getMessage());
// 今回分は終了 — 次のサイクルで残りを拾い直す
}
「諦める」ことが、守ることだった
導入後、数字が動いた。
- ポーリングリクエスト: 172 → 117(▲32%)
- 504 エラー: 114 → 42(▲63%)
- WM アラート: 3 → 1(▲67%)
ジョージが言った。「諦めることを設計する、それが堅牢なシステムの作り方だ」と。
コストを削減したかったわけじゃない。ちゃんと動くシステムを作ったら、結果的に数字がついてきた。
【技術コラム】BudgetGuard と、他の「止める仕組み」を比べる
① PHP set_time_limit() との違い
| 比較軸 | set_time_limit() | BudgetGuard |
|---|---|---|
| 制御の粒度 | スクリプト全体 | 処理ループの1サイクル単位 |
| 超過時の動作 | Fatal Error(クラッシュ) | 例外(キャッチして制御可能) |
| 後処理 | できない | catch 内でログ・キュー返却などできる |
| 複数タイマー | 1つだけ | 処理ごとに個別設定できる |
set_time_limit() は「壁に激突して止まる」。BudgetGuard は「自分で足を止めて、ログを残して帰る」。
② 代替ライブラリ・手法
同様の問題を解くアプローチは他にもある。
- Guzzle の
timeoutオプション — HTTP リクエスト単体にタイムアウトを設定できる。1リクエストの制御には十分だが、複数回ポーリングするループ全体を制御するには向かない。 - Symfony Process コンポーネント — サブプロセスをタイムアウト付きで管理できる。重い処理をバックグラウンドで分離したい場合に有効。ただしオーバーヘッドがある。
- PCNTL シグナル(
pcntl_alarm) — Unix シグナルでタイムアウトを実装できる。CLI 専用で、PHP-FPM 環境では使えない制約がある。
BudgetGuard はゼロ依存で、FPM でも CLI でも動く。ポーリングループを「今回分だけ諦める」という粒度で制御したい場合に最もシンプルな選択肢だ。
③ サーキットブレーカーとの違い、フェイルファスト原則
「諦める仕組み」を設計するとき、よく参照される概念がある。
- フェイルファスト原則 — 問題を早期に検出して即座に止める。BudgetGuard はこの原則の実践例だ。無駄な処理を続けるより、早く止めてログを残すほうが障害対応も速くなる。
- サーキットブレーカー — 失敗が連続したら一時的に接続を遮断する状態機械。BudgetGuard より複雑だが、外部 API の全面障害に対処する場合に向いている。
- 指数バックオフ — 失敗のたびに待機時間を倍にして再試行する。「諦める」のではなく「間隔を空けて続ける」戦略。BudgetGuard と組み合わせて「予算内に収まる回数だけ再試行する」設計も可能だ。
BudgetGuard が特別なのは、これらのパターンの中で最も小さく、状態を持たない点だ。microtime(true) の差分だけで動く。
BudgetGuard の導入手順
「自分のプロジェクトでも使いたい」と思った人のために、導入手順をまとめておく。依存ライブラリは不要、コードはおよそ20行。コピーすればすぐ試せる。
ステップ1: クラスを1ファイルに設置する
例外クラスと本体クラスを BudgetGuard.php のような1ファイルに置く。これだけで準備は終わりだ。
class BudgetExceededException extends \RuntimeException {}
class BudgetGuard {
private float $budget;
private float $startTime;
public function __construct(float $budget) {
$this->budget = $budget;
$this->startTime = microtime(true);
}
public function check(): void {
if (microtime(true) - $this->startTime > $this->budget) {
throw new BudgetExceededException("budget={$this->budget}s elapsed");
}
}
}
ステップ2: 時間予算を決めて、ループの内側で check() を呼ぶ
処理の開始時にインスタンスを作り、ループのたびに check() を呼ぶ。予算を超えた瞬間に例外が飛ぶ。
$guard = new BudgetGuard(30.0); // 30秒の予算
foreach ($items as $item) {
$guard->check(); // 予算超過なら BudgetExceededException
$api->fetch($item); // 重い外部処理
}
ステップ3: 例外を捕まえ、ログを残して次に委ねる
呼び出し側で例外を catch し、今回分を打ち切る。残りは次のサイクル(cron など)で拾い直せばいい。システムは止まらない。
try {
$guard = new BudgetGuard(30.0);
foreach ($items as $item) {
$guard->check();
$api->fetch($item);
}
} catch (BudgetExceededException $e) {
Log::warning('budget exceeded: ' . $e->getMessage());
// 今回はここまで。次回のサイクルで残りを処理する
}
この3ステップだけで、暴走しかけた処理に「ここまで」という壁ができる。set_time_limit() のようにクラッシュさせず、自分の足で立ち止まってログを残す。それが BudgetGuard の導入のすべてだ。
取材を終えて、私は少し哲学的な気持ちになった。「諦めることを恐れる」とき、システムもまた同じように、諦められずに崩れていく。BudgetGuard は、コードの中の「勇気」だ。