タイトル: 無限ループが検出されました
SEOタイトル: 無限ループ (Maximum execution time exceeded) の原因と対処
| この記事の要点 |
|
エラー内容
PHP
Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/html/app/Service.php on line 42
# または
Fatal error: Maximum execution time of 60 seconds exceeded in ...
# Laravel での例
Symfony\Component\ErrorHandler\Error\FatalError:
Maximum execution time of 30 seconds exceeded
JavaScript (ブラウザ)
このページは応答していません。
ページを終了するか、引き続き待機します。
# Chrome DevTools コンソール
[Violation] 'click' handler took 5234ms
原因の典型パターン
1. while の終了条件忘れ
// ❌ 終了条件が変わらない
$i = 0;
while ($i < 10) {
echo $i;
// $i++ を忘れた!
}
// ✅ 正しい
$i = 0;
while ($i < 10) {
echo $i;
$i++;
}
2. 配列ポインタを進めない
// ❌ next() し忘れ
$arr = [1, 2, 3];
reset($arr);
while (($v = current($arr)) !== false) {
echo $v;
// next() を忘れた → 永遠に最初の要素
}
// ✅ foreach に書き換える
foreach ($arr as $v) {
echo $v;
}
3. 再帰の base case 忘れ
// ❌ 終了条件なし
function factorial($n) {
return $n * factorial($n - 1); // 永遠に再帰
}
// ✅ base case あり
function factorial($n) {
if ($n <= 1) return 1; // ★ 終了条件
return $n * factorial($n - 1);
}
// 終了条件があっても入力が悪いと無限再帰
factorial(-1); // -1 * -2 * -3 ... → スタックオーバーフローまで暴走
// → 入力検証も必要
function factorial(int $n): int {
if ($n < 0) throw new InvalidArgumentException();
if ($n <= 1) return 1;
return $n * factorial($n - 1);
}
4. ループ内でループカウンタを変更
// ❌ ループ内で $i をリセット
for ($i = 0; $i < 10; $i++) {
if (someCondition()) {
$i = 0; // ★ 何度も最初に戻る
}
}
// ✅ break / continue
for ($i = 0; $i < 10; $i++) {
if (someCondition()) {
break;
}
}
5. 浮動小数の比較
// ❌ 浮動小数誤差で終わらない
for ($x = 0.0; $x != 1.0; $x += 0.1) {
echo $x;
}
// → 0.1 + 0.2 + ... が 1.0 ぴったりにならない(浮動小数の宿命)
// ✅ < で比較 / 誤差許容
for ($x = 0.0; $x < 1.0 - 1e-9; $x += 0.1) {
echo $x;
}
// ✅ 整数で回す
for ($i = 0; $i < 10; $i++) {
$x = $i * 0.1;
echo $x;
}
6. ループ条件の変数を更新しない
// ❌ DB ループで last_id を更新しない
$lastId = 0;
while ($rows = DB::select('SELECT * FROM logs WHERE id > ? LIMIT 1000', [$lastId])) {
foreach ($rows as $row) {
// 処理
}
// ★ $lastId を更新し忘れ → 同じデータを永遠に取り続ける
}
// ✅
$lastId = 0;
while ($rows = DB::select('SELECT * FROM logs WHERE id > ? ORDER BY id LIMIT 1000', [$lastId])) {
foreach ($rows as $row) {
// 処理
}
$lastId = end($rows)->id; // ★ 最大 id を更新
}
原因の調査方法
方法1: エラーログ + デバッグ
// register_tick_function でループ内に挿入
declare(ticks=1);
$count = 0;
function watchdog() {
global $count;
if (++$count > 100000) {
error_log('Possible infinite loop: ' . print_r(debug_backtrace(), true));
exit(1);
}
}
register_tick_function('watchdog');
// 怪しいコード実行
problematicCode();
方法2: Xdebug でステップ実行
# Xdebug インストール
pecl install xdebug
# php.ini
[xdebug]
zend_extension=xdebug.so
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=localhost
xdebug.client_port=9003
# VS Code: PHP Debug 拡張で接続
# IDE でブレークポイント設定 → ループ内で何度も止めて状態確認
方法3: xhprof / Tideways で関数別プロファイル
xhprof_enable(XHPROF_FLAGS_CPU);
problematicCode();
$data = xhprof_disable();
include "xhprof_lib/utils/xhprof_lib.php";
include "xhprof_lib/utils/xhprof_runs.php";
$runs = new XHProfRuns_Default();
$runId = $runs->save_run($data, "test");
// → ブラウザ http://example.com/xhprof_html/?run=$runId&source=test
// → 累積時間が異常に多い関数 = 無限ループの主犯
// 簡易版: microtime で囲む
$start = microtime(true);
problematicCode();
$elapsed = microtime(true) - $start;
error_log("elapsed: {$elapsed}s");
対処1: 緊急回避 (時間制限解除)
// ファイル単位で時間制限解除
set_time_limit(0); // 0 = 無制限
// 1 操作ごとに延長
set_time_limit(60); // この呼出から 60 秒延長
// CLI は元々制限なし(max_execution_time = 0)
// → ブラウザ実行のみ制限がかかる
// php.ini
max_execution_time = 60 // デフォルト 30
max_input_time = 60
memory_limit = 256M
注意: 時間制限解除は無限ループの隠蔽になります。CLI バッチ等の正当な長時間処理にのみ使用してください。
対処2: ループ自体に上限を入れる
// ✅ 防衛的プログラミング: 上限を設定
$maxIter = 1_000_000;
$i = 0;
while (someCondition()) {
if (++$i > $maxIter) {
throw new RuntimeException("possible infinite loop: $i iterations");
}
// 処理
}
// 関数: 上限付き再帰
function recurse($n, $depth = 0) {
if ($depth > 100) {
throw new RuntimeException('max recursion depth exceeded');
}
if ($n <= 0) return;
recurse($n - 1, $depth + 1);
}
JavaScript の無限ループ
// ❌ ブラウザのメインスレッドを止める
while (true) {
// タブが応答しなくなる、最終的にブラウザが kill
}
// ❌ Promise 内で await を await し続ける
async function infinite() {
while (true) {
await Promise.resolve(); // ★ イベントループに戻らない(Microtask)
}
}
// ✅ setTimeout でループするとブラウザに制御を返せる
function poll() {
if (done()) return;
process();
setTimeout(poll, 100); // 次回 100ms 後
}
poll();
// ✅ requestAnimationFrame
function tick() {
if (done()) return;
update();
requestAnimationFrame(tick);
}
tick();
// ✅ Web Worker で別スレッドへ
const worker = new Worker('worker.js');
worker.postMessage({ data });
Node.js の場合
// ❌ イベントループを止める
while (true) {
// ★ 他のイベント(HTTP, ファイル I/O)が処理されない
}
// ✅ setImmediate / setTimeout でイベントループに譲る
function loop() {
if (done()) return;
work();
setImmediate(loop);
}
loop();
// ✅ async / await + setTimeout
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
async function run() {
while (!done()) {
await work();
await sleep(10); // ★ 他処理に譲る
}
}
// CPU 集約処理は worker_threads
const { Worker } = require('worker_threads');
new Worker('./worker.js');
FAQ
Q: Laravel のジョブが「Maximum execution time exceeded」になる
A: ジョブ用に $timeout プロパティ設定。php artisan queue:work --timeout=300 で 300 秒に。それでも長いなら処理を分割。
Q: PHP-FPM で max_execution_time 効かない
A: nginx / Apache 側のタイムアウト(fastcgi_read_timeout / ProxyTimeout)が先に発火する可能性。両方確認。
Q: 無限ループを止める Ctrl+C が効かない
A: PHP の pcntl_signal() でシグナルハンドラ登録 + declare(ticks=1) を併用。Web 経由は CLI 終了で。