Skip to content

Prevent Running the Same Console Command Multiple Times

Edit this page

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;
    }

    // ...
}
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version