MaxAttemptsExceededException とは
Laravel には Job Queue https://laravel.com/docs/12.x/queues という、非同期でやるべき仕事を登録する便利な機能があります。
その機能を使用していると、MaxAttemptsExceededException
という一見よくわからない例外が発生する場合があります。多くの方は「よくわからんけどたまにしか起こらないからまあいいか」と受け流してるかと思います。
私はそういうのは受け流せない人なので、MaxAttemptsExceededException
について調べてみました。
この例外は Job の再試行回数(retries)を超えたことを意味するわけですが、具体的には次の2つの場合に発生しえます。
- (ケース1)Job 処理中に例外が発生した。
- (ケース2)Job 処理中にシステムダウンなどにより処理が中断した。
ケース1の場合は、try .. catch
などでその例外の扱い方を制御できますので、MaxAttemptsExceededException
を抑制することは比較的簡単です。
問題はケース2の場合です。この場合、Laravel が関与できない神の領域で起こった理由により Job の処理も止まったわけですから、Laravel がやれることは、次に Laravel が再開したときに、Job を再試行する、再試行回数を超えた場合はそのことを報告することになります。それが MaxAttemptsExceededException
の役割です。
そういうわけで MaxAttemptsExceededException
自体は合理的なシステムですが、問題はその所作、立ち振る舞いです。
MaxAttemptsExceededException
も例外の一種なので、Laravel はログにそのスタックトレース情報を吐き出しますが、ケース1の場合は、原因となった例外自体のスタックトレース情報は原因追及に役立ちますが、MaxAttemptsExceededException
のスタックトレース情報を知ったところで何の役にも立ちません。
また、ケース2の場合は、Laravel が関与できない神の領域で起こった出来事に Job が巻き込まれただけなので Laravel のスタックトレース情報が役立つことはありません。
以上のことから、 MaxAttemptsExceededException
のスタックトレース情報は非表示にしてしまうのが吉ということになります。次のようにします。
return Application::configure(basePath: dirname(__DIR__))
->withExceptions(function (Exceptions $exceptions) {
$exceptions->report(function (MaxAttemptsExceededException $e) {
$jobName = property_exists($e, 'job') && $e->job ? get_class($e->job) : 'UnknownJob';
Log::warning("MaxAttemptsExceededException: {$jobName}");
// true を返すことで、標準の長文レポート処理(スタックトレース)を抑制する
return true;
});
})->create();
まったく何も出さないというのもあれなので、最低限のログを出すようにしました。
(おまけ)Laravel の Job Queue に思うところ
前述のように Laravel の Job Queue は大変便利な機能ですが、Job の chain 実行(連鎖的実行)は無用の長物な気がします。理由は次の通りです。
- Job は PHP CLI として実行されるので、実行時間制限がない。すなわち Job を小分けにする意味がないどころかソースが複雑になるだけ。なお、念のため実行時間制限を容れたいのなら
set_time_limit()
を容れるなどすればよい。 - chain 実行では、Job が失敗すると後続の Job は実行されず終了してしまう。かといって batch にすると、QuereWorker が複数起動していると並行実行されてしまい連鎖性を担保できない。
また、既述のように Laravel の Job Queue には Job が失敗した場合に再試行する機能が標準で備わっているわけですが、ケース1の場合は自前で再試行処理を書けばいいだけといえますので、Laravel の Job 標準の再試行機能を使う理由は主としてケース2に対応するためといえそうです。
しかしながら、「たとえシステムがダウンしたとしてもシステム再開後に漏れなく絶対に Job を実行したいのです!1回でも抜け落ちたら困るんです!」くらいの要件の Job が必要なケースがそうそうあるとは思えないわけです。
以上を踏まえると Job の設定のほとんどは、
class MyJob implements \Illuminate\Contracts\Queue\ShouldQueue
{
:
public $timeout = 0; // 無制限。本来は retry_after 以下にする必要があるが tries = 1 なので 0 でよい
public $tries = 1; // 再試行なし。0 は「無制限に再試行する」という意味なので注意。
:
}
で十分な気がする今日この頃です。
この設定を基礎にして、あとは再試行なり時間制限なりのようなものは自前で実装すればいいやん、というわけです。
みなさんの Laravel Life に少しでもお役に立てれば幸いです。