Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Illuminate/Bus/UniqueLock.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function release($job)
* @param mixed $job
* @return string
*/
protected function getKey($job)
public static function getKey($job)
{
$uniqueId = method_exists($job, 'uniqueId')
? $job->uniqueId()
Expand Down
9 changes: 9 additions & 0 deletions src/Illuminate/Foundation/Bus/PendingDispatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Foundation\Queue\InteractsWithUniqueJobs;

class PendingDispatch
{
use InteractsWithUniqueJobs;

/**
* The job.
*
Expand Down Expand Up @@ -207,12 +210,18 @@ public function __call($method, $parameters)
*/
public function __destruct()
{
$this->addUniqueJobInformationToContext($this->job);

if (! $this->shouldDispatch()) {
$this->removeUniqueJobInformationFromContext($this->job);

return;
} elseif ($this->afterResponse) {
app(Dispatcher::class)->dispatchAfterResponse($this->job);
} else {
app(Dispatcher::class)->dispatch($this->job);
}

$this->removeUniqueJobInformationFromContext($this->job);
}
}
55 changes: 55 additions & 0 deletions src/Illuminate/Foundation/Queue/InteractsWithUniqueJobs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Illuminate\Foundation\Queue;

use Illuminate\Bus\UniqueLock;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Support\Facades\Context;

trait InteractsWithUniqueJobs
{
/**
* Store unique job information in the context in case we can't resolve the job on the queue side.
*
* @param mixed $job
* @return void
*/
public function addUniqueJobInformationToContext($job): void
{
if ($job instanceof ShouldBeUnique) {
Context::addHidden([
'laravel_unique_job_cache_store' => $this->getUniqueJobCacheStore($job),
'laravel_unique_job_key' => UniqueLock::getKey($job),
]);
}
}

/**
* Remove the unique job information from the context.
*
* @param mixed $job
* @return void
*/
public function removeUniqueJobInformationFromContext($job): void
{
if ($job instanceof ShouldBeUnique) {
Context::forgetHidden([
'laravel_unique_job_cache_store',
'laravel_unique_job_key',
]);
}
}

/**
* Determine the cache store used by the unique job to acquire locks.
*
* @param mixed $job
* @return string|null
*/
protected function getUniqueJobCacheStore($job): ?string
{
return method_exists($job, 'uniqueVia')
? $job->uniqueVia()->getName()
: config('cache.default');
}
}
33 changes: 33 additions & 0 deletions src/Illuminate/Queue/CallQueuedHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
use Illuminate\Bus\Batchable;
use Illuminate\Bus\UniqueLock;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Contracts\Cache\Factory as CacheFactory;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Contracts\Queue\Job;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Log\Context\Repository as ContextRepository;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Queue\Attributes\DeleteWhenMissingModels;
use ReflectionClass;
Expand Down Expand Up @@ -227,13 +229,44 @@ protected function handleModelNotFound(Job $job, $e)
$shouldDelete = false;
}

$this->ensureUniqueJobLockIsReleasedViaContext();

if ($shouldDelete) {
return $job->delete();
}

return $job->fail($e);
}

/**
* Ensure the lock for a unique job is released via context.
*
* This is required when we can't unserialize the job due to missing models.
*
* @return void
*/
protected function ensureUniqueJobLockIsReleasedViaContext()
{
if (! $this->container->bound(ContextRepository::class) ||
! $this->container->bound(CacheFactory::class)) {
return;
}

$context = $this->container->make(ContextRepository::class);

[$store, $key] = [
$context->getHidden('laravel_unique_job_cache_store'),
$context->getHidden('laravel_unique_job_key'),
];

if ($store && $key) {
$this->container->make(CacheFactory::class)
->store($store)
->lock($key)
->forceRelease();
}
}

/**
* Call the failed method on the job instance.
*
Expand Down
37 changes: 37 additions & 0 deletions tests/Integration/Queue/UniqueJobTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Auth\User;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Bus;
use Orchestra\Testbench\Attributes\WithMigration;
use Orchestra\Testbench\Factories\UserFactory;

#[WithMigration]
#[WithMigration('cache')]
Expand Down Expand Up @@ -130,6 +134,28 @@ public function testLockCanBeReleasedBeforeProcessing()
$this->assertTrue($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get());
}

public function testLockIsReleasedOnModelNotFoundException()
{
UniqueTestSerializesModelsJob::$handled = false;

/** @var \Illuminate\Foundation\Auth\User */
$user = UserFactory::new()->create();
$job = new UniqueTestSerializesModelsJob($user);

$this->expectException(ModelNotFoundException::class);

try {
$user->delete();
dispatch($job);
$this->runQueueWorkerCommand(['--once' => true]);
unserialize(serialize($job));
} finally {
$this->assertFalse($job::$handled);
$this->assertModelMissing($user);
$this->assertTrue($this->app->get(Cache::class)->lock($this->getLockKey($job), 10)->get());
}
}

protected function getLockKey($job)
{
return 'laravel_unique_job:'.(is_string($job) ? $job : get_class($job)).':';
Expand Down Expand Up @@ -185,3 +211,14 @@ class UniqueUntilStartTestJob extends UniqueTestJob implements ShouldBeUniqueUnt
{
public $tries = 2;
}

class UniqueTestSerializesModelsJob extends UniqueTestJob
{
use SerializesModels;

public $deleteWhenMissingModels = true;

public function __construct(public User $user)
{
}
}