fix(queue): raise retry_after above the longest job timeout
The database queue connection used retry_after=90s while several jobs declare timeouts far above it (up to 600s). When a job outlives its reservation window the queue hands it to a second worker; a tries=1 job then fails with MaxAttemptsExceededException and re-runs its side effects (double model billing, reset progress) — exactly what CategorizeUncategorizedTransactionsJob's tries=1 was meant to prevent. Raise the default to 900s (env-overridable via DB_QUEUE_RETRY_AFTER) so it exceeds the longest job timeout, and add a guard test asserting every app/Jobs $timeout stays below retry_after so a new long job cannot silently reintroduce the bug. Fixes PHP-LARAVEL-2D
This commit is contained in:
parent
26875bbfff
commit
e7e319a5b8
|
|
@ -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,
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* The database queue driver releases a job back to the queue once its
|
||||
* reservation is older than retry_after. If a job runs longer than that, a
|
||||
* second worker picks it up: a tries=1 job then dies with
|
||||
* MaxAttemptsExceededException and re-runs its side effects. Guard the
|
||||
* invariant so a new long-running job can never silently reintroduce it.
|
||||
*/
|
||||
test('every queued job timeout stays below the database queue retry_after', function () {
|
||||
$retryAfter = (int) config('queue.connections.database.retry_after');
|
||||
|
||||
$offenders = collect(File::allFiles(app_path('Jobs')))
|
||||
->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.'
|
||||
);
|
||||
});
|
||||
Loading…
Reference in New Issue