Dealing with Concurrency with Locks =================================== When a program runs concurrently, some parts of code that modify shared resources should not be accessed by multiple processes at the same time. Symfony's :doc:`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:: $lock = $lockFactory->createLock('pdf-creation'); if (!$lock->acquire()) { return; } // critical section of code $service->method(); $lock->release(); Installing ---------- In applications using :ref:`Symfony Flex `, run this command to install the Lock component: .. code-block:: terminal $ composer require symfony/lock Configuring ----------- By default, Symfony provides a :ref:`Semaphore ` when available, or a :ref:`Flock ` otherwise. You can configure this behavior by using the ``lock`` key like: .. configuration-block:: .. code-block:: yaml # 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: 'zookeeper://localhost01,localhost02:2181' 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)%' # using an existing service lock: 'snc_redis.default' # named locks lock: invoice: ['semaphore', 'redis://r2.docker'] report: 'semaphore' .. code-block:: xml flock flock:///path/to/file semaphore memcached://m1.docker memcached://m1.docker memcached://m2.docker redis://r1.docker redis://r1.docker redis://r2.docker zookeeper://z1.docker zookeeper://z1.docker,z2.docker zookeeper://localhost01,localhost02:2181 sqlite:///%kernel.project_dir%/var/lock.db mysql:host=127.0.0.1;dbname=app pgsql:host=127.0.0.1;dbname=app pgsql+advisory:host=127.0.0.1;dbname=app sqlsrv:server=127.0.0.1;Database=app oci:host=127.0.0.1;dbname=app mongodb://127.0.0.1/app?collection=lock %env(LOCK_DSN)% snc_redis.default semaphore redis://r2.docker semaphore .. code-block:: php // config/packages/lock.php use Symfony\Config\FrameworkConfig; use function Symfony\Component\DependencyInjection\Loader\Configurator\env; return static function (FrameworkConfig $framework): void { $framework->lock() ->resource('default', ['flock']) ->resource('default', ['flock:///path/to/file']) ->resource('default', ['semaphore']) ->resource('default', ['memcached://m1.docker']) ->resource('default', ['memcached://m1.docker', 'memcached://m2.docker']) ->resource('default', ['redis://r1.docker']) ->resource('default', ['redis://r1.docker', 'redis://r2.docker']) ->resource('default', ['zookeeper://z1.docker']) ->resource('default', ['zookeeper://z1.docker,z2.docker']) ->resource('default', ['zookeeper://localhost01,localhost02:2181']) ->resource('default', ['sqlite:///%kernel.project_dir%/var/lock.db']) ->resource('default', ['mysql:host=127.0.0.1;dbname=app']) ->resource('default', ['pgsql:host=127.0.0.1;dbname=app']) ->resource('default', ['pgsql+advisory:host=127.0.0.1;dbname=app']) ->resource('default', ['sqlsrv:server=127.0.0.1;Database=app']) ->resource('default', ['oci:host=127.0.0.1;dbname=app']) ->resource('default', ['mongodb://127.0.0.1/app?collection=lock']) ->resource('default', [env('LOCK_DSN')]) // using an existing service ->resource('default', ['snc_redis.default']) // named locks ->resource('invoice', ['semaphore', 'redis://r2.docker']) ->resource('report', ['semaphore']) ; }; .. versionadded:: 7.2 The option to use an existing service as the lock/semaphore was introduced in Symfony 7.2. Locking a Resource ------------------ To lock the default resource, autowire the lock factory using :class:`Symfony\\Component\\Lock\\LockFactory`:: // src/Controller/PdfController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Lock\LockFactory; class PdfController extends AbstractController { #[Route('/download/terms-of-use.pdf')] public function downloadPdf(LockFactory $factory, MyPdfGeneratorService $pdf): Response { $lock = $factory->createLock('pdf-creation'); $lock->acquire(true); // heavy computation $myPdf = $pdf->getOrCreatePdf(); $lock->release(); // ... } } .. warning:: 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``:: // src/Controller/PdfController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Lock\LockFactory; class PdfController extends AbstractController { #[Route('/download/{version}/terms-of-use.pdf')] public function downloadPdf($version, LockFactory $lockFactory, MyPdfGeneratorService $pdf): Response { $lock = $lockFactory->createLock('pdf-creation-'.$version); $lock->acquire(true); // heavy computation $myPdf = $pdf->getOrCreatePdf($version); $lock->release(); // ... } } .. _lock-named-locks: Naming Locks ------------ If the application needs different kinds of stores alongside each other, Symfony provides :ref:`named lock `: .. configuration-block:: .. code-block:: yaml # config/packages/lock.yaml framework: lock: invoice: ['semaphore', 'redis://r2.docker'] report: 'semaphore' .. code-block:: xml semaphore redis://r2.docker semaphore .. code-block:: php // config/packages/lock.php use Symfony\Config\FrameworkConfig; return static function (FrameworkConfig $framework): void { $framework->lock() ->resource('invoice', ['semaphore', 'redis://r2.docker']) ->resource('report', ['semaphore']); ; }; After having configured one or more named locks, you have two ways of injecting them in any service or controller: **(1) Use a specific argument name** Type-hint your constructor/method argument with ``LockFactory`` and name the argument using this pattern: "lock name in camelCase" + ``LockFactory`` suffix. For example, to inject the ``invoice`` package defined earlier:: use Symfony\Component\Lock\LockFactory; class SomeService { public function __construct( private LockFactory $invoiceLockFactory ) { // ... } } **(2) Use the ``#[Target]`` attribute** When :ref:`dealing with multiple implementations of the same type ` the ``#[Target]`` attribute helps you select which one to inject. Symfony creates a target called ``lock.`` + "lock name" + ``.factory``. For example, to select the ``invoice`` lock defined earlier:: // ... use Symfony\Component\DependencyInjection\Attribute\Target; class SomeService { public function __construct( #[Target('lock.invoice.factory')] private LockFactory $lockFactory ) { // ... } }