Prevent Running the Same Console Command Multiple Times
You can use locks to prevent the same command from running multiple times on the same server. The Lock component provides multiple classes to create locks based on the filesystem (FlockStore), shared memory (SemaphoreStore) and even databases and Redis servers.
In addition, the Console component provides a PHP trait called LockableTrait
that adds two convenient methods to lock and release commands:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
// ...
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LockableTrait;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(name: 'contents:update')]
class UpdateContentsCommand
{
use LockableTrait;
public function __invoke(SymfonyStyle $io): int
{
if (!$this->lock()) {
$io->writeln('The command is already running in another process.');
return Command::SUCCESS;
}
// If you prefer to wait until the lock is released, use this:
// $this->lock(null, true);
// ...
// if not released explicitly, Symfony releases the lock
// automatically when the execution of the command ends
$this->release();
return Command::SUCCESS;
}
}
The LockableTrait uses the SemaphoreStore if available and the
FlockStore otherwise. These stores aren't scoped to your project, so
different projects (or parallel test processes) that run a command with the
same name on the same server share the same lock.
In Symfony applications, you can change the store used by all the commands
that use the LockableTrait by configuring a
named lock called console:
1 2 3 4 5 6 7
# config/packages/lock.yaml
framework:
lock:
default: '%env(LOCK_DSN)%'
# stores the lock files inside the project instead of the
# shared temporary directory of the system
console: 'flock://%kernel.project_dir%/var/lock'
Symfony injects the factory of this lock into autowired commands through the
setLockFactory() method of the trait. If you don't define a lock called
console, commands keep using the default stores explained above (they
don't use the default lock).
Note
Keep the default lock when adding named locks. Otherwise, you can't
autowire the LockFactory service without the #[Target] attribute.
8.2
The autowiring of the console named lock in the LockableTrait
was introduced in Symfony 8.2.
You can also set the $lockFactory property in the constructor of your
command. This lock factory takes precedence over the console named lock:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// ...
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LockableTrait;
use Symfony\Component\Lock\LockFactory;
#[AsCommand(name: 'contents:update')]
class UpdateContentsCommand
{
use LockableTrait;
// don't use PHP constructor property promotion here because the
// LockableTrait already defines the `$lockFactory` property in this class
public function __construct(LockFactory $lockFactory)
{
$this->lockFactory = $lockFactory;
}
// ...
}