Dealing with Concurrency with Locks
When a program runs concurrently, some part of code which modify shared resources should not be accessed by multiple processes at the same time. Symfony's Lock component provides a locking mechanism to ensure that only one process is running the critical section of code at any point of time to prevent race conditions from happening.
The following example shows a typical usage of the lock:
1 2 3 4 5 6 7 8 9
$lock = $lockFactory->createLock('pdf-creation');
if (!$lock->acquire()) {
return;
}
// critical section of code
$service->method();
$lock->release();
Installing
In applications using Symfony Flex, run this command to install the Lock component:
1
$ composer require symfony/lock
Configuring
By default, Symfony provides a Semaphore
when available, or a Flock otherwise. You can configure
this behavior by using the lock
key like:
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
# config/packages/lock.yaml
framework:
lock: ~
lock: 'flock'
lock: 'flock:///path/to/file'
lock: 'semaphore'
lock: 'memcached://m1.docker'
lock: ['memcached://m1.docker', 'memcached://m2.docker']
lock: 'redis://r1.docker'
lock: ['redis://r1.docker', 'redis://r2.docker']
lock: 'rediss://r1.docker?ssl[verify_peer]=1&ssl[cafile]=...'
lock: 'zookeeper://z1.docker'
lock: 'zookeeper://z1.docker,z2.docker'
lock: 'sqlite:///%kernel.project_dir%/var/lock.db'
lock: 'mysql:host=127.0.0.1;dbname=app'
lock: 'pgsql:host=127.0.0.1;dbname=app'
lock: 'pgsql+advisory:host=127.0.0.1;dbname=app'
lock: 'sqlsrv:server=127.0.0.1;Database=app'
lock: 'oci:host=127.0.0.1;dbname=app'
lock: 'mongodb://127.0.0.1/app?collection=lock'
lock: '%env(LOCK_DSN)%'
# named locks
lock:
invoice: ['semaphore', 'redis://r2.docker']
report: 'semaphore'
Locking a Resource
To lock the default resource, autowire the lock factory using LockFactory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// src/Controller/PdfController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockFactory;
class PdfController extends AbstractController
{
/**
* @Route("/download/terms-of-use.pdf")
*/
public function downloadPdf(LockFactory $factory, MyPdfGeneratorService $pdf)
{
$lock = $factory->createLock('pdf-creation');
$lock->acquire(true);
// heavy computation
$myPdf = $pdf->getOrCreatePdf();
$lock->release();
// ...
}
}
Caution
The same instance of LockInterface
won't block when calling acquire
multiple times inside the same process. When several services use the
same lock, inject the LockFactory
instead to create a separate lock
instance for each service.
Locking a Dynamic Resource
Sometimes the application is able to cut the resource into small pieces in order
to lock a small subset of processes and let others through. The previous example
showed how to lock the $pdf->getOrCreatePdf()
call for everybody,
now let's see how to lock a $pdf->getOrCreatePdf($version)
call only for
processes asking for the same $version
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// src/Controller/PdfController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockFactory;
class PdfController extends AbstractController
{
/**
* @Route("/download/{version}/terms-of-use.pdf")
*/
public function downloadPdf($version, LockFactory $lockFactory, MyPdfGeneratorService $pdf)
{
$lock = $lockFactory->createLock('pdf-creation-'.$version);
$lock->acquire(true);
// heavy computation
$myPdf = $pdf->getOrCreatePdf($version);
$lock->release();
// ...
}
}
Naming Locks
If the application needs different kind of Stores alongside each other, Symfony provides named lock:
1 2 3 4 5
# config/packages/lock.yaml
framework:
lock:
invoice: ['semaphore', 'redis://r2.docker']
report: 'semaphore'
An autowiring alias is created for each named lock with a name using the camel
case version of its name suffixed by LockFactory
.
For instance, the invoice
lock can be injected by naming the argument
$invoiceLockFactory
and type-hinting it with
LockFactory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// src/Controller/PdfController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockFactory;
class PdfController extends AbstractController
{
#[Route('/download/terms-of-use.pdf')]
public function downloadPdf(LockFactory $invoiceLockFactory, MyPdfGeneratorService $pdf)
{
// ...
}
}
Blocking Store
If you want to use the RetryTillSaveStore
for non-blocking locks,
you can do it by decorating the store service:
1 2 3 4
lock.default.retry_till_save.store:
class: Symfony\Component\Lock\Store\RetryTillSaveStore
decorates: lock.default.store
arguments: ['@.inner', 100, 50]