diff --git a/components/lock.rst b/components/lock.rst new file mode 100644 index 00000000000..cf9c174781a --- /dev/null +++ b/components/lock.rst @@ -0,0 +1,258 @@ +.. index:: + single: Lock + single: Components; Lock + +The Lock Component +================== + + The Lock Component creates and manages `locks`_, a mechanism to provide + exclusive access to a shared resource. + +.. versionadded:: 3.3 + The Lock component was introduced in Symfony 3.3. + +Installation +------------ + +You can install the component in 2 different ways: + +* :doc:`Install it via Composer ` (``symfony/lock`` on `Packagist`_); +* Use the official Git repository (https://github.com/symfony/lock). + +.. include:: /components/require_autoload.rst.inc + +Usage +----- + +Locks are used to guarantee exclusive access to some shared resource. In +Symfony applications, you can use locks for example to ensure that a command is +not executed more than once at the same time (on the same or different servers). + +In order to manage the state of locks, a ``Store`` needs to be created first +and then use the :class:`Symfony\\Component\\Lock\\Factory` class to actually +create the lock for some resource:: + + use Symfony\Component\Lock\Factory; + use Symfony\Component\Lock\Store\SemaphoreStore; + + $store = new SemaphoreStore(); + $factory = new Factory($store); + +Then, a call to the :method:`Symfony\\Component\\Lock\\LockInterface::acquire` +method will try to acquire the lock. Its first argument is an arbitrary string +that represents the locked resource:: + + // ... + $lock = $factory->createLock('pdf-invoice-generation'); + + if ($lock->acquire()) { + // The resource "pdf-invoice-generation" is locked. + // You can compute and generate invoice safely here. + + $lock->release(); + } + +If the lock can not be acquired, the method returns ``false``. The ``acquire()`` +method can be safely called repeatedly, even if the lock is already acquired. + +.. note:: + + Unlike other implementations, the Lock Component distinguishes locks + instances even when they are created for the same resource. If a lock has + to be used by several services, they should share the same ``Lock`` instance + returned by the ``Factory::createLock`` method. + +Blocking Locks +-------------- + +By default, when a lock cannot be acquired, the ``acquire`` method returns +``false`` immediately. To wait (indefinitely) until the lock +can be created, pass ``true`` as the argument of the ``acquire()`` method. This +is called a **blocking lock** because the execution of your application stops +until the lock is acquired. + +Some of the built-in ``Store`` classes support this feature. When they don't, +they can be decorated with the ``RetryTillSaveStore`` class:: + + use Symfony\Component\Lock\Factory; + use Symfony\Component\Lock\Store\RedisStore; + use Symfony\Component\Lock\Store\RetryTillSaveStore; + + $store = new RedisStore(new \Predis\Client('tcp://localhost:6379')); + $store = new RetryTillSaveStore($store); + $factory = new Factory($store); + + $lock = $factory->createLock('notification-flush'); + $lock->acquire(true); + +Expiring Locks +-------------- + +Locks created remotely are difficult to manage because there is no way for the +remote ``Store`` to know if the locker process is still alive. Due to bugs, +fatal errors or segmentation faults, it cannot be guaranteed that ``release()`` +method will be called, which would cause the resource to be locked infinitely. + +The best solution in those cases is to create **expiring locks**, which are +released automatically after some amount of time has passed (called TTL for +*Time To Live*). This time, in seconds, is configured as the second argument of +the ``createLock()`` method. If needed, these locks can also be released early +with the ``release()`` method. + +The trickiest part when working with expiring locks is choosing the right TTL. +If it's too short, other processes could acquire the lock before finishing the +job; it it's too long and the process crashes before calling the ``release()`` +method, the resource will stay locked until the timeout:: + + // ... + // create an expiring lock that lasts 30 seconds + $lock = $factory->createLock('charts-generation', 30); + + $lock->acquire(); + try { + // perform a job during less than 30 seconds + } finally { + $lock->release(); + } + +.. tip:: + + To avoid letting the lock in a locking state, it's recommended to wrap the + job in a try/catch/finally block to always try to release the expiring lock. + +In case of long-running tasks, it's better to start with a not too long TTL and +then use the :method:`Symfony\\Component\\Lock\\LockInterface::refresh` method +to reset the TTL to its original value:: + + // ... + $lock = $factory->createLock('charts-generation', 30); + + $lock->acquire(); + try { + while (!$finished) { + // perform a small part of the job. + + // renew the lock for 30 more seconds. + $lock->refresh(); + } + } finally { + $lock->release(); + } + +Available Stores +---------------- + +Locks are created and managed in ``Stores``, which are classes that implement +:class:`Symfony\\Component\\Lock\\StoreInterface`. The component includes the +following built-in store types: + + +============================================ ====== ======== ======== +Store Scope Blocking Expiring +============================================ ====== ======== ======== +:ref:`FlockStore ` local yes no +:ref:`MemcachedStore ` remote no yes +:ref:`RedisStore ` remote no yes +:ref:`SemaphoreStore ` local yes no +============================================ ====== ======== ======== + +.. _lock-store-flock: + +FlockStore +~~~~~~~~~~ + +The FlockStore uses the file system on the local computer to create the locks. +It does not support expiration, but the lock is automatically released when the +PHP process is terminated:: + + use Symfony\Component\Lock\Store\FlockStore; + + // the argument is the path of the directory where the locks are created + $store = new FlockStore(sys_get_temp_dir()); + +.. caution:: + + Beware that some file systems (such as some types of NFS) do not support + locking. In those cases, it's better to use a directory on a local disk + drive or a remote store based on Redis or Memcached. + +.. _lock-store-memcached: + +MemcachedStore +~~~~~~~~~~~~~~ + +The MemcachedStore saves locks on a Memcached server, it requires a Memcached +connection implementing the ``\Memcached`` class. This store does not +support blocking, and expects a TTL to avoid stalled locks:: + + use Symfony\Component\Lock\Store\MemcachedStore; + + $memcached = new \Memcached(); + $memcached->addServer('localhost', 11211); + + $store = new MemcachedStore($memcached); + +.. note:: + + Memcached does not support TTL lower than 1 second. + +.. _lock-store-redis: + +RedisStore +~~~~~~~~~~ + +The RedisStore saves locks on a Redis server, it requires a Redis connection +implementing the ``\Redis``, ``\RedisArray``, ``\RedisCluster`` or +``\Predis`` classes. This store does not support blocking, and expects a TTL to +avoid stalled locks:: + + use Symfony\Component\Lock\Store\RedisStore; + + $redis = new \Redis(); + $redis->connect('localhost'); + + $store = new RedisStore($redis); + +.. _lock-store-semaphore: + +SemaphoreStore +~~~~~~~~~~~~~~ + +The SemaphoreStore uses the `PHP semaphore functions`_ to create the locks:: + + use Symfony\Component\Lock\Store\SemaphoreStore; + + $store = new SemaphoreStore(); + +.. _lock-store-combined: + +CombinedStore +~~~~~~~~~~~~~ + +The CombinedStore is designed for High Availability applications because it +manages several stores in sync (for example, several Redis servers). When a lock +is being acquired, it forwards the call to all the managed stores, and it +collects their responses. If a simple majority of stores have acquired the lock, +then the lock is considered as acquired; otherwise as not acquired:: + + use Symfony\Component\Lock\Strategy\ConsensusStrategy; + use Symfony\Component\Lock\Store\CombinedStore; + use Symfony\Component\Lock\Store\RedisStore; + + $stores = []; + foreach (array('server1', 'server2', 'server3') as $server) { + $redis= new \Redis(); + $redis->connect($server); + + $stores[] = new RedisStore($redis); + } + + $store = new CombinedStore($stores, new ConsensusStrategy()); + +Instead of the simple majority strategy (``ConsensusStrategy``) an +``UnanimousStrategy`` can be used to require the lock to be acquired in all +the stores. + +.. _`locks`: https://en.wikipedia.org/wiki/Lock_(computer_science) +.. _Packagist: https://packagist.org/packages/symfony/lock +.. _`PHP semaphore functions`: http://php.net/manual/en/book.sem.php