Skip to content

Instantly share code, notes, and snippets.

@GromNaN
Created January 31, 2022 08:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GromNaN/53066a2f6d41fe43de8a13b623006481 to your computer and use it in GitHub Desktop.
Save GromNaN/53066a2f6d41fe43de8a13b623006481 to your computer and use it in GitHub Desktop.
MysqlStore for Symfony Lock using `GET_LOCK`
<?php
namespace PMD\ONE\CoreBundle\Lock\Store;
use Doctrine\DBAL\Connection;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\PersistingStoreInterface;
/**
* @author Jérôme TAMARELLE <jerome@tamarelle.net>
*/
class MysqlStore implements PersistingStoreInterface
{
/**
* @var \PDO|Connection
*/
private $connection;
/**
* @var int
*/
private $waitTimeout;
/**
* @param \PDO|Connection $connection A \PDO or Connection instance
* @param int $waitTimeout Time in seconds to wait for a lock to be released, for non-blocking lock.
*/
public function __construct($connection, $waitTimeout = 0)
{
if ($connection instanceof \PDO) {
if ('mysql' !== $driver = $connection->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
throw new InvalidArgumentException(sprintf('%s requires a "mysql" connection. "%s" given.', __CLASS__, $driver));
}
} elseif ($connection instanceof Connection) {
if ('pdo_mysql' !== $driver = $connection->getDriver()->getName()) {
throw new InvalidArgumentException(sprintf('%s requires a "pdo_mysql" connection. "%s" given.', __CLASS__, $driver));
}
} else {
throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance, "%s" given.', __CLASS__, is_object($connection) ? get_class($connection) : gettype($connection)));
}
if ($waitTimeout < 0) {
throw new InvalidArgumentException(sprintf('"%s" requires a positive wait timeout, "%d" given. For infine wait, acquire a "blocking" lock.', __CLASS__, $waitTimeout));
}
$this->connection = $connection;
$this->waitTimeout = $waitTimeout;
}
/**
* {@inheritdoc}
*/
public function save(Key $key)
{
$this->lock($key, false);
}
/**
* {@inheritdoc}
*/
public function putOffExpiration(Key $key, $ttl)
{
// the GET_LOCK locks forever, until the session terminates.
$stmt = $this->connection->prepare('SET SESSION wait_timeout=GREATEST(@@wait_timeout, :ttl)');
$stmt->bindValue(':ttl', $ttl, \PDO::PARAM_INT);
$stmt->execute();
}
/**
* {@inheritdoc}
*/
public function delete(Key $key)
{
if (!$key->hasState(__CLASS__)) {
return;
}
$storedKey = $key->getState(__CLASS__);
$stmt = $this->connection->prepare('DO RELEASE_LOCK(:key)');
$stmt->bindValue(':key', $storedKey, \PDO::PARAM_STR);
$stmt->execute();
$key->removeState(__CLASS__);
}
/**
* {@inheritdoc}
*/
public function exists(Key $key)
{
if (!$key->hasState(__CLASS__)) {
return false;
}
$storedKey = $key->getState(__CLASS__);
$stmt = $this->connection->prepare('SELECT IF(IS_USED_LOCK(:key) = CONNECTION_ID(), 1, 0)');
$stmt->bindValue(':key', $storedKey, \PDO::PARAM_STR);
$stmt->setFetchMode(\PDO::FETCH_COLUMN, 0);
$stmt->execute();
return '1' === $stmt->fetchColumn();
}
private function lock(Key $key, bool $blocking)
{
// the lock is maybe already acquired.
if ($key->hasState(__CLASS__)) {
return;
}
// no timeout for impatient
$timeout = $blocking ? -1 : $this->waitTimeout;
// Hash the key to guarantee it contains between 1 and 64 characters
$storedKey = hash('sha256', $key);
$stmt = $this->connection->prepare('SELECT IF(IS_USED_LOCK(:key) = CONNECTION_ID(), -1, GET_LOCK(:key, :timeout))');
$stmt->bindValue(':key', $storedKey, \PDO::PARAM_STR);
$stmt->bindValue(':timeout', $timeout, \PDO::PARAM_INT);
$stmt->setFetchMode(\PDO::FETCH_COLUMN, 0);
$stmt->execute();
// 1: Lock successful
// 0: Already locked by another session
// -1: Already locked by the same session
$success = $stmt->fetchColumn();
if ('-1' === $success) {
throw new LockConflictedException('Lock already acquired with by same connection.');
}
if ('1' !== $success) {
throw new LockConflictedException();
}
$key->setState(__CLASS__, $storedKey);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment