Lock & Semaphore
Jérémy DERUSSÉ
Solution Architect at WebMD
@symfony core team
@jderusse
function updateUser(string $userId, array $changes)
{
$data = $this->client->request('GET', "/users/$userId")->toArray();
$data = array_merge($data, $changes);
$this->client->request('PUT', "/users/$userId", ['body' => $data]);
}
function updateUser(string $userId, array $changes)
{
$this->limitToOneProcess();
$data = $this->client->request('GET', "/users/$userId")->toArray();
$data = array_merge($data, $changes);
$this->client->request('PUT', "/users/$userId", ['body' => $data]);
$this->removeLimit();
}
race condition
symfony/lock
symfony/semaphore
“To make a quick comparison with a lock:
- A lock allows only 1 process to access a resource
- A semaphore allow N process to access a resource
Basically, a lock is a semaphore where N = 1.”
@lyrixx
symfony/lock
final class Key
{
/** @var string */
private $resource;
/** @var array */
private $state = [];
}
interface PersistingStoreInterface
{
public function save(Key $key);
public function delete(Key $key);
public function exists(Key $key);
// ...
}
final class Lock implements LockInterface
{
/** @var Key */
private $key;
/** @var PersistingStoreInterface */
private $store;
public function acquire() {}
public function release() {}
// ...
}
symfony/semaphore
final class Key
{
/** @var string */
private $resource;
/** @var int */
private $limit;
/** @var array */
private array $state = [];
}
interface PersistingStoreInterface
{
public function save(Key $key);
public function delete(Key $key);
public function exists(Key $key);
// ...
}
final class Semaphore implements SemaphoreInterface
{
/** @var Key */
private $key;
/** @var PersistingStoreInterface */
private $store;
public function acquire();
public function release();
// ...
}
available stores
Flock
Memcached
MongoDb
Pdo
Postgres
Redis
Semaphore
Zookeeper
lock
5.2
semaphore
5.2
remote
expiring
blocking
sharing
5.2
how to use it?
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\Store\FlockStore;
$store = new FlockStore();
$factory = new LockFactory($store);
$lock = $factory->createLock('pdf-invoice-generation');
if ($lock->acquire()) {
try {
generateInvoice();
} finally {
$lock->release();
}
}
auto-release
function __invoke()
{
$lock = $this->lockFactory->createLock('pdf-invoice-generation');
if (!$lock->acquire()) {
return;
}
$this->task();
}
function updateUser(string $userId, array $changes)
{
$lock = $this->lockFactory
->createLock("/users/$userId");
$lock->acquire(true);
$data = $this->client->request('GET', "/users/$userId")->toArray();
$data = array_merge($data, $changes);
$this->client->request('PUT', "/users/$userId", ['body' => $data]);
}
blocking
function generateInvoices()
{
$lock = $this->lockFactory->createLock('generate_invoices', 30);
if (!$lock->acquire()) {
return;
}
foreach ($this->getPendingInvoices() as $invoice) {
$lock->refresh();
$this->generateInvoice($invoice);
}
}
expiration
class GenerateThumbHandler implements MessageHandlerInterface
{
private const LIMIT = 10;
public function __invoke(GenerateThumb $message): void
{
$semaphore = $this->semaphoreFactory
->createSemaphore('generate-thumb', self::LIMIT);
if (!$semaphore->acquire()) {
throw new RecoverableException('Semaphore not available');
}
$this->thumbGenerator->generate($message);
}
}
semaphore
class GenerateThumbHandler implements MessageHandlerInterface
{
private const LIMIT = 10;
public function __invoke(GenerateThumb $message): void
{
$semaphore = $this->semaphoreFactory
->createSemaphore('generate-thumb', self::LIMIT);
if (!$semaphore->acquire()) {
throw new RecoverableException('Semaphore not available');
}
$this->thumbGenerator->generate($message);
}
}
semaphore
pro tips
function addItem(Order $order, Item $item)
{
$order->items->add($item);
$this->updateOrder($order);
$this->updateShipping($order);
$this->updateTotalAmount($order);
}
shared locks
function displayOrderAction(string $orderId)
{
$order = $this->orderRepository
->fetch($orderId);
return $this->render(
'order.html.twig',
['order' => $order]
);
}
function addItem(Order $order, Item $item)
{
$lock = $this->lockFactory
->createLock($order->getId());
$lock->acquire(true);
$order->items->add($item);
$this->updateOrder($order);
$this->updateShipping($order);
$this->updateTotalAmount($order);
}
5.2
function displayOrderAction(string $orderId)
{
$lock = $this->lockFactory
->createLock($orderId);
$lock->acquireRead(true);
$order = $this->orderRepository
->fetch($orderId);
return $this->render(
'order.html.twig',
['order' => $order]
);
}
advanced usage
$store = new CombinedStore(
[
StoreFactory::createStore('redis://node1.acme.fr'),
StoreFactory::createStore('redis://node2.acme.fr'),
StoreFactory::createStore('redis://node3.acme.fr'),
],
new ConsensusStrategy()
);
high availability
class RestrictedAreaController extends AbstractController
{
private const MAXIMUM_MEMBERS = 5;
public function __invoke(SessionInterface $session)
{
$key = $session->get('key') ?? new Key(
$this->getCurrentUser()->getFamilyId(), self::MAXIMUM_MEMBERS
);
$semaphore = $this->factory->createLockFromKey($key, 300, false);
if (!$semaphore->acquire()) {
return $this->redirectToRoute('waiting_room.html.twig');
}
$semaphore->refresh();
$session->set('key', $key);
return $this->render('restricted_area.html.twig');
}
}
cross process
common pitfalls
function __invoke(): void
{
$this->lock->acquire();
// ⚠ result not checked
// ...
}
function __invoke(): void
{
if (!$this->lock->acquire()) {
return;
}
// ...
}
function __invoke(): void
{
$this->lock->acquire(true); // blocking mode
// ...
}
function __invoke(): void
{
if (!$this->lockFactory
->createLock('resource')
->acquire()
) {
return;
}
// ...
}
function __invoke(): void
{
$lock = $this->lockFactory
->createLock('resource');
if (!$lock->acquire()) {
return;
}
// ...
}
function __construct(LockInterface $lock)
{
$this->lock = $lock;
}
function __construct(LockFactory $factory)
{
$this->factory = $factory;
}
!
function __invoke()
{
if (!$this->lock->acquire()) {
return;
}
$this->lock->acquire(); // true
$this->lock->acquire(); // true
}
function __invoke()
{
$lock = $this->factory->createLock(__CLASS__);
if (!$lock->acquire()) {
return;
}
}
Is it reliable?
- flock + NFS
- memcached/redis + reboot
- pdo/redis + replication
- synchronized clocks
- ntp update
- semaphore + non-system user
- postgres/zookeep + network
- memcached/redis + LRU
- memcached/redis + flush
- memcached + loadbalancer
reliability
Is it reliable?
YES, if you read the doc!
Thank you!
@jderusse
slides
Lock & Semaphore - Symfony World 2020
By Jérémy Derussé
Lock & Semaphore - Symfony World 2020
- 10,838