diff --git a/src/Illuminate/Broadcasting/UniqueBroadcastEvent.php b/src/Illuminate/Broadcasting/UniqueBroadcastEvent.php index b99af6f843d5..3e1916da45e0 100644 --- a/src/Illuminate/Broadcasting/UniqueBroadcastEvent.php +++ b/src/Illuminate/Broadcasting/UniqueBroadcastEvent.php @@ -29,8 +29,6 @@ class UniqueBroadcastEvent extends BroadcastEvent implements ShouldBeUnique */ public function __construct($event) { - $this->uniqueId = get_class($event); - if (method_exists($event, 'uniqueId')) { $this->uniqueId .= $event->uniqueId(); } elseif (property_exists($event, 'uniqueId')) { diff --git a/src/Illuminate/Bus/UniqueLock.php b/src/Illuminate/Bus/UniqueLock.php index c1d74c636f1e..df2caf8f81fa 100644 --- a/src/Illuminate/Bus/UniqueLock.php +++ b/src/Illuminate/Bus/UniqueLock.php @@ -69,6 +69,10 @@ public static function getKey($job) ? $job->uniqueId() : ($job->uniqueId ?? ''); - return 'laravel_unique_job:'.get_class($job).':'.$uniqueId; + $jobName = method_exists($job, 'displayName') + ? $job->displayName() + : get_class($job); + + return 'laravel_unique_job:'.$jobName.':'.$uniqueId; } } diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php index 307842d2e6de..32ea76cbf7db 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php @@ -256,7 +256,11 @@ protected function getKey($job) return $this->prefix.$job->job->uuid(); } - return $this->prefix.hash('xxh128', get_class($job)); + $jobName = method_exists($job, 'displayName') + ? $job->displayName() + : get_class($job); + + return $this->prefix.hash('xxh128', $jobName); } /** diff --git a/src/Illuminate/Queue/Middleware/WithoutOverlapping.php b/src/Illuminate/Queue/Middleware/WithoutOverlapping.php index 42fabdaa3303..0f9c40680817 100644 --- a/src/Illuminate/Queue/Middleware/WithoutOverlapping.php +++ b/src/Illuminate/Queue/Middleware/WithoutOverlapping.php @@ -154,8 +154,14 @@ public function shared() */ public function getLockKey($job) { - return $this->shareKey - ? $this->prefix.$this->key - : $this->prefix.get_class($job).':'.$this->key; + if ($this->shareKey) { + return $this->prefix.$this->key; + } + + $jobName = method_exists($job, 'displayName') + ? $job->displayName() + : get_class($job); + + return $this->prefix.$jobName.':'.$this->key; } } diff --git a/tests/Integration/Broadcasting/BroadcastManagerTest.php b/tests/Integration/Broadcasting/BroadcastManagerTest.php index 485f2757ecda..872c11c680be 100644 --- a/tests/Integration/Broadcasting/BroadcastManagerTest.php +++ b/tests/Integration/Broadcasting/BroadcastManagerTest.php @@ -75,7 +75,35 @@ public function testUniqueEventsCanBeBroadcast() Bus::assertNotDispatched(UniqueBroadcastEvent::class); Queue::assertPushed(UniqueBroadcastEvent::class); - $lockKey = 'laravel_unique_job:'.UniqueBroadcastEvent::class.':'.TestEventUnique::class; + $lockKey = 'laravel_unique_job:'.TestEventUnique::class.':'; + $this->assertFalse($this->app->get(Cache::class)->lock($lockKey, 10)->get()); + } + + public function testUniqueEventsCanBeBroadcastWithUniqueIdFromProperty() + { + Bus::fake(); + Queue::fake(); + + Broadcast::queue(new TestEventUniqueWithIdProperty); + + Bus::assertNotDispatched(UniqueBroadcastEvent::class); + Queue::assertPushed(UniqueBroadcastEvent::class); + + $lockKey = 'laravel_unique_job:'.TestEventUniqueWithIdProperty::class.':unique-id-property'; + $this->assertFalse($this->app->get(Cache::class)->lock($lockKey, 10)->get()); + } + + public function testUniqueEventsCanBeBroadcastWithUniqueIdFromMethod() + { + Bus::fake(); + Queue::fake(); + + Broadcast::queue(new TestEventUniqueWithIdMethod); + + Bus::assertNotDispatched(UniqueBroadcastEvent::class); + Queue::assertPushed(UniqueBroadcastEvent::class); + + $lockKey = 'laravel_unique_job:'.TestEventUniqueWithIdMethod::class.':unique-id-method'; $this->assertFalse($this->app->get(Cache::class)->lock($lockKey, 10)->get()); } @@ -178,6 +206,16 @@ public function broadcastOn() } } +class TestEventUniqueWithIdProperty extends TestEventUnique +{ + public string $uniqueId = 'unique-id-property'; +} + +class TestEventUniqueWithIdMethod extends TestEventUnique +{ + public string $uniqueId = 'unique-id-method'; +} + class TestEventRescue implements ShouldBroadcast, ShouldRescue { /** diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index 34667768208b..74dab53838ff 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -5,6 +5,7 @@ use Exception; use Illuminate\Bus\Dispatcher; use Illuminate\Bus\Queueable; +use Illuminate\Cache\RateLimiter; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Queue\Job; use Illuminate\Queue\CallQueuedHandler; @@ -345,6 +346,85 @@ public function release() $middleware->report(fn () => false); $middleware->handle($job, $next); } + + public function testUsesJobClassNameForCacheKey() + { + $rateLimiter = $this->mock(RateLimiter::class); + + $job = new class + { + public $released = false; + + public function release() + { + $this->released = true; + + return $this; + } + }; + + $expectedKey = 'laravel_throttles_exceptions:'.hash('xxh128', get_class($job)); + + $rateLimiter->shouldReceive('tooManyAttempts') + ->once() + ->with($expectedKey, 10) + ->andReturn(false); + + $rateLimiter->shouldReceive('hit') + ->once() + ->with($expectedKey, 600); + + $next = function ($job) { + throw new RuntimeException('Whoops!'); + }; + + $middleware = new ThrottlesExceptions(); + $middleware->handle($job, $next); + + $this->assertTrue($job->released); + } + + public function testUsesDisplayNameForCacheKeyWhenAvailable() + { + $rateLimiter = $this->mock(RateLimiter::class); + + $job = new class + { + public $released = false; + + public function release() + { + $this->released = true; + + return $this; + } + + public function displayName(): string + { + return 'App\\Actions\\ThrottlesExceptionsTestAction'; + } + }; + + $expectedKey = 'laravel_throttles_exceptions:'.hash('xxh128', 'App\\Actions\\ThrottlesExceptionsTestAction'); + + $rateLimiter->shouldReceive('tooManyAttempts') + ->once() + ->with($expectedKey, 10) + ->andReturn(false); + + $rateLimiter->shouldReceive('hit') + ->once() + ->with($expectedKey, 600); + + $next = function ($job) { + throw new RuntimeException('Whoops!'); + }; + + $middleware = new ThrottlesExceptions(); + $middleware->handle($job, $next); + + $this->assertTrue($job->released); + } } class CircuitBreakerTestJob diff --git a/tests/Integration/Queue/UniqueJobTest.php b/tests/Integration/Queue/UniqueJobTest.php index 5ec58efdec2b..f82b525339a9 100644 --- a/tests/Integration/Queue/UniqueJobTest.php +++ b/tests/Integration/Queue/UniqueJobTest.php @@ -4,6 +4,7 @@ use Exception; use Illuminate\Bus\Queueable; +use Illuminate\Bus\UniqueLock; use Illuminate\Container\Container; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Queue\ShouldBeUnique; @@ -169,6 +170,62 @@ protected function getLockKey($job) { return 'laravel_unique_job:'.(is_string($job) ? $job : get_class($job)).':'; } + + public function testLockUsesDisplayNameWhenAvailable() + { + Bus::fake(); + + $lockKey = 'laravel_unique_job:App\\Actions\\UniqueTestAction:'; + + dispatch(new UniqueTestJobWithDisplayName); + $this->runQueueWorkerCommand(['--once' => true]); + Bus::assertDispatched(UniqueTestJobWithDisplayName::class); + + $this->assertFalse( + $this->app->get(Cache::class)->lock($lockKey, 10)->get() + ); + + Bus::assertDispatchedTimes(UniqueTestJobWithDisplayName::class); + dispatch(new UniqueTestJobWithDisplayName); + $this->runQueueWorkerCommand(['--once' => true]); + Bus::assertDispatchedTimes(UniqueTestJobWithDisplayName::class); + + $this->assertFalse( + $this->app->get(Cache::class)->lock($lockKey, 10)->get() + ); + } + + public function testUniqueLockCreatesKeyWithClassName() + { + $this->assertEquals( + 'laravel_unique_job:'.UniqueTestJob::class.':', + UniqueLock::getKey(new UniqueTestJob) + ); + } + + public function testUniqueLockCreatesKeyWithIdAndClassName() + { + $this->assertEquals( + 'laravel_unique_job:'.UniqueIdTestJob::class.':unique-id-1', + UniqueLock::getKey(new UniqueIdTestJob) + ); + } + + public function testUniqueLockCreatesKeyWithDisplayNameWhenAvailable() + { + $this->assertEquals( + 'laravel_unique_job:App\\Actions\\UniqueTestAction:unique-id-2', + UniqueLock::getKey(new UniqueIdTestJobWithDisplayName) + ); + } + + public function testUniqueLockCreatesKeyWithIdAndDisplayNameWhenAvailable() + { + $this->assertEquals( + 'laravel_unique_job:App\\Actions\\UniqueTestAction:unique-id-2', + UniqueLock::getKey(new UniqueIdTestJobWithDisplayName) + ); + } } class UniqueTestJob implements ShouldQueue, ShouldBeUnique @@ -239,3 +296,32 @@ public function uniqueVia(): Cache return Container::getInstance()->make(Cache::class); } } + +class UniqueIdTestJob extends UniqueTestJob +{ + public function uniqueId(): string + { + return 'unique-id-1'; + } +} + +class UniqueTestJobWithDisplayName extends UniqueTestJob +{ + public function displayName(): string + { + return 'App\\Actions\\UniqueTestAction'; + } +} + +class UniqueIdTestJobWithDisplayName extends UniqueTestJob +{ + public function uniqueId(): string + { + return 'unique-id-2'; + } + + public function displayName(): string + { + return 'App\\Actions\\UniqueTestAction'; + } +} diff --git a/tests/Integration/Queue/WithoutOverlappingJobsTest.php b/tests/Integration/Queue/WithoutOverlappingJobsTest.php index 98eea03acce6..22d166ec3eeb 100644 --- a/tests/Integration/Queue/WithoutOverlappingJobsTest.php +++ b/tests/Integration/Queue/WithoutOverlappingJobsTest.php @@ -151,6 +151,31 @@ public function testGetLock() (new WithoutOverlapping('key'))->withPrefix('prefix:')->shared()->getLockKey($job) ); } + + public function testGetLockUsesDisplayName() + { + $job = new OverlappingTestJobWithDisplayName; + + $this->assertSame( + 'laravel-queue-overlap:App\\Actions\\WithoutOverlappingTestAction:key', + (new WithoutOverlapping('key'))->getLockKey($job) + ); + + $this->assertSame( + 'laravel-queue-overlap:key', + (new WithoutOverlapping('key'))->shared()->getLockKey($job) + ); + + $this->assertSame( + 'prefix:App\\Actions\\WithoutOverlappingTestAction:key', + (new WithoutOverlapping('key'))->withPrefix('prefix:')->getLockKey($job) + ); + + $this->assertSame( + 'prefix:key', + (new WithoutOverlapping('key'))->withPrefix('prefix:')->shared()->getLockKey($job) + ); + } } class OverlappingTestJob @@ -221,3 +246,11 @@ public function middleware() return [(new WithoutOverlapping)->shared()]; } } + +class OverlappingTestJobWithDisplayName extends OverlappingTestJob +{ + public function displayName(): string + { + return 'App\\Actions\\WithoutOverlappingTestAction'; + } +}