diff --git a/config/queue.php b/config/queue.php index 4fe599aa..cfa5fbc2 100644 --- a/config/queue.php +++ b/config/queue.php @@ -40,7 +40,12 @@ return [ 'connection' => env('DB_QUEUE_CONNECTION'), 'table' => env('DB_QUEUE_TABLE', 'jobs'), 'queue' => env('DB_QUEUE', 'default'), - 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + // retry_after must exceed the longest job $timeout on this connection + // (currently 600s). Otherwise a long job outlives its reservation, the + // queue hands it to a second worker, and a tries=1 job dies with + // MaxAttemptsExceededException (and re-runs its side effects). The + // worker --timeout must sit between the longest timeout and this value. + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 900), 'after_commit' => true, ], diff --git a/tests/Feature/QueueConfigTest.php b/tests/Feature/QueueConfigTest.php new file mode 100644 index 00000000..e510cc45 --- /dev/null +++ b/tests/Feature/QueueConfigTest.php @@ -0,0 +1,37 @@ +map(function ($file): string { + $relative = Str::of($file->getPathname()) + ->after(app_path().DIRECTORY_SEPARATOR) + ->replace(DIRECTORY_SEPARATOR, '\\') + ->replaceLast('.php', ''); + + return 'App\\'.$relative; + }) + ->filter(fn (string $class): bool => class_exists($class)) + ->mapWithKeys(function (string $class): array { + $timeout = (new ReflectionClass($class))->getDefaultProperties()['timeout'] ?? null; + + return [$class => $timeout]; + }) + ->filter(fn ($timeout): bool => is_int($timeout) && $timeout >= $retryAfter); + + expect($offenders->all())->toBe( + [], + 'These jobs have a $timeout >= retry_after ('.$retryAfter.'s); raise DB_QUEUE_RETRY_AFTER above the longest job timeout.' + ); +});