Easy locks with Symfony

Easy locks with Symfony

The Symfony documentation describes quite well how locks actually work, that's why this article will not describe why you should use a lock and how it actually works. But it says nothing about the short time installation it requires to set it up and run!

Let's see that together.

Install the lock component with composer just like any other Symfony component:

composer require lock

This component is one of the rare components that will not come with a recipe, you need to add the configuration yourself, and it's quite easy.

# In the file packages/lock.yaml
framework:
    lock: 'redis://localhost'

This is an example to configure a redis, but it could work with your database directly: lock: '%env(DATABASE_URL)%'

Here is a command that uses our lock, it's that simple!

class LockTestCommand extends Command
{
    protected static $defaultName = 'app:lock-test';

    // In Symfony 5.2 you can inject directly the lock factory
    // because the RetryTillSaveStore (see bellow) is no more required
    private PersistingStoreInterface $lockStore;

    public function __construct(PersistingStoreInterface $lockStore)
    {
        $this->lockStore = $lockStore;
        parent::__construct();
    }

    protected function configure()
    {
        $this
            ->setDescription('This is a simple lock test')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        // Depending on what store you uses, you may need to decorate the store
        // to be able to use the `BlockingStoreInterface` feature
        // and it's required for the redis store.
        $store = new RetryTillSaveStore($this->lockStore, retrySleep: 100, retryCount: 1000); // 100ms 1000 times try
        
        // In the case we decorate the store we need to create by hand the factory
        $lockFactory = new LockFactory($store);
        $lock = $this->lockFactory->createLock('test-lock');

        // Passing "true" will enforce the waiting for the lock to be available
        if (!$lock->acquire(blocking: true)) {
            $io->success("Cannot acquire the lock");
            return 1;
        }

        $io->success("Lock acquired");
        $this->task();

        $lock->release();

        return 0;
    }

    private function task()
    {
        sleep(10);
    }
}

The previous configuration only registers the default lock (and its store and factory) that will be automatically injected to your services. But you can actually define many locks (and keeping the default one!). Here is how:

framework:
    lock:
        default: '%env(DATABASE_URL)%'
        redis_high_availability: ['redis://r1.docker', 'redis://r2.docker']

This will generate a lock factory as a service named lock.redis_high_availability.factory, and you will need to specify it explicitely in your services configuration to use it.

I hope it helps! Please let us know in the comments if there's any mistake or a better way to do it.

Follow us on Twitter! https://twitter.com/swagdotind