-
Notifications
You must be signed in to change notification settings - Fork 11.6k
Closed
Labels
Description
Laravel Version
10.42.0
PHP Version
8.3.2
Database Driver & Version
Redis 7.2.4
Description
Queued jobs with ShouldBeUnique may cause stale unique locks in the case when the dependent model is missing.
According to the source code of the CallQueuedHandler::call() unique lock cleanup may never be reached in case if the job depends on the missing model AND the job is configured to be deleted when the model is missing (public $deleteWhenMissingModels = true).
Steps To Reproduce
The PoC is made using Redis queue, a similar approach may work with other drivers.
- Create a new project:
laravel new poc. - Ensure Redis is configured & reachable in your
.envfile. WARNING: PoC will flush all keys. - Create poc.php file with the following contents:
<?php
use Illuminate\Bus\Queueable;
use Illuminate\Queue\WorkerOptions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Redis;
use Illuminate\Queue\SerializesModels;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
require 'vendor/autoload.php';
$app = require __DIR__.'/bootstrap/app.php';
$cli = $app->make(Kernel::class);
$cli->bootstrap();
config([
'database.default' => 'sqlite',
'queue.default' => 'redis',
'cache.default' => 'redis',
]);
@unlink(config('database.connections.sqlite.database'));
$cli->call('migrate', ['--force' => true]);
DB::unprepared("
CREATE TABLE IF NOT EXISTS main (
id INTEGER NOT NULL PRIMARY KEY,
name VARCHAR(255) UNIQUE
)
");
DB::unprepared("
CREATE TABLE IF NOT EXISTS secondary (
id INTEGER NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL
)
");
// ensure there are no keys in the DB
Redis::flushdb();
class Main extends Model {
public $table = 'main';
public $timestamps = false;
public $fillable = ['name'];
}
class Secondary extends Model {
public $table = 'secondary';
public $timestamps = false;
public $fillable = ['name'];
}
class MyJob implements ShouldQueue, ShouldBeUnique {
use Dispatchable, Queueable, SerializesModels;
public $tries = 1;
public $maxExceptions = 1;
public $deleteWhenMissingModels = true;
public function __construct(
public Main $main,
public Secondary $secondary,
)
{
}
public function handle() {
}
public function uniqueId() {
return 'job-for-' . $this->main->id;
}
}
// create 2 instances
$main = Main::create(['name' => 'main model']);
$secondary = Secondary::create(['name' => 'secondary model']);
// and schedule a job for them
MyJob::dispatch($main, $secondary);
// delete the secondary model
$secondary->delete();
// run the job
app('queue.worker')->runNextJob('redis', 'default', new WorkerOptions);
echo 'Queue size: ', Queue::size('default'), PHP_EOL; // no tasks left
echo 'Redis contents:', PHP_EOL;
dump(Redis::keys('*')); // unique id is still there- Execute the poc.php
php poc.php - Inspect the output to see the stale unique lock
Output
Queue size: 0
Redis contents:
array:1 [
0 => "laravel_database_laravel_cache_:laravel_unique_job:MyJobjob-for-1"
] // poc.php:95
Expected result
The unique lock has been removed.
Actual result
A stale unique lock still exists.