diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php index 68017795655c..5c5c7d04a7ed 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php @@ -57,6 +57,13 @@ class ThrottlesExceptions */ protected $whenCallback; + /** + * The callbacks that determine if the job should be deleted. + * + * @var callable[] + */ + protected array $deleteWhenCallbacks = []; + /** * The prefix of the rate limiter key. * @@ -111,6 +118,10 @@ public function handle($job, $next) report($throwable); } + if ($this->shouldDelete($throwable)) { + return $job->delete(); + } + $this->limiter->hit($jobKey, $this->decaySeconds); return $job->release($this->retryAfterMinutes * 60); @@ -130,6 +141,38 @@ public function when(callable $callback) return $this; } + /** + * Add a callback that should determine if the job should be deleted. + * + * @param callable|string $callback + * @return $this + */ + public function deleteWhen(callable|string $callback) + { + $this->deleteWhenCallbacks[] = is_string($callback) + ? fn (Throwable $e) => $e instanceof $callback + : $callback; + + return $this; + } + + /** + * Run the skip / delete callbacks to determine if the job should be deleted for the given exception. + * + * @param Throwable $throwable + * @return bool + */ + protected function shouldDelete(Throwable $throwable): bool + { + foreach ($this->deleteWhenCallbacks as $callback) { + if (call_user_func($callback, $throwable)) { + return true; + } + } + + return false; + } + /** * Set the prefix of the rate limiter key. * diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php index e5b79a7d67ed..8c6b78912b0d 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php @@ -58,6 +58,10 @@ public function handle($job, $next) report($throwable); } + if ($this->shouldDelete($throwable)) { + return $job->delete(); + } + $this->limiter->acquire(); return $job->release($this->retryAfterMinutes * 60); diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index e559c41b7f8f..19f525afcc8d 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -40,6 +40,11 @@ public function testCircuitResetsAfterSuccess() $this->assertJobWasReleasedWithDelay(CircuitBreakerTestJob::class); } + public function testCircuitCanSkipJob() + { + $this->assertJobWasDeleted(CircuitBreakerSkipJob::class); + } + protected function assertJobWasReleasedImmediately($class) { $class::$handled = false; @@ -82,6 +87,27 @@ protected function assertJobWasReleasedWithDelay($class) $this->assertFalse($class::$handled); } + protected function assertJobWasDeleted($class) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('delete')->once(); + $job->shouldReceive('isDeleted')->andReturn(true); + $job->shouldReceive('isReleased')->twice()->andReturn(false); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); + + $instance->call($job, [ + 'command' => serialize($command = new $class), + ]); + + $this->assertTrue($class::$handled); + } + protected function assertJobRanSuccessfully($class) { $class::$handled = false; @@ -314,6 +340,25 @@ public function middleware() } } +class CircuitBreakerSkipJob +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function handle() + { + static::$handled = true; + + throw new Exception; + } + + public function middleware() + { + return [(new ThrottlesExceptions(2, 10 * 60))->deleteWhen(Exception::class)]; + } +} + class CircuitBreakerSuccessfulJob { use InteractsWithQueue, Queueable;