Add a PdoStore in lock
This commit is contained in:
parent
f96753b9ab
commit
46fe1b0712
@ -1,6 +1,11 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
4.2.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added the PDO Store
|
||||||
|
|
||||||
3.4.0
|
3.4.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class MemcachedStore implements StoreInterface
|
|||||||
*/
|
*/
|
||||||
public function save(Key $key)
|
public function save(Key $key)
|
||||||
{
|
{
|
||||||
$token = $this->getToken($key);
|
$token = $this->getUniqueToken($key);
|
||||||
$key->reduceLifetime($this->initialTtl);
|
$key->reduceLifetime($this->initialTtl);
|
||||||
if (!$this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) {
|
if (!$this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) {
|
||||||
// the lock is already acquired. It could be us. Let's try to put off.
|
// the lock is already acquired. It could be us. Let's try to put off.
|
||||||
@ -80,13 +80,13 @@ class MemcachedStore implements StoreInterface
|
|||||||
public function putOffExpiration(Key $key, $ttl)
|
public function putOffExpiration(Key $key, $ttl)
|
||||||
{
|
{
|
||||||
if ($ttl < 1) {
|
if ($ttl < 1) {
|
||||||
throw new InvalidArgumentException(sprintf('%s() expects a TTL greater or equals to 1. Got %s.', __METHOD__, $ttl));
|
throw new InvalidArgumentException(sprintf('%s() expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface defines a float value but Store required an integer.
|
// Interface defines a float value but Store required an integer.
|
||||||
$ttl = (int) ceil($ttl);
|
$ttl = (int) ceil($ttl);
|
||||||
|
|
||||||
$token = $this->getToken($key);
|
$token = $this->getUniqueToken($key);
|
||||||
|
|
||||||
list($value, $cas) = $this->getValueAndCas($key);
|
list($value, $cas) = $this->getValueAndCas($key);
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ class MemcachedStore implements StoreInterface
|
|||||||
*/
|
*/
|
||||||
public function delete(Key $key)
|
public function delete(Key $key)
|
||||||
{
|
{
|
||||||
$token = $this->getToken($key);
|
$token = $this->getUniqueToken($key);
|
||||||
|
|
||||||
list($value, $cas) = $this->getValueAndCas($key);
|
list($value, $cas) = $this->getValueAndCas($key);
|
||||||
|
|
||||||
@ -144,13 +144,10 @@ class MemcachedStore implements StoreInterface
|
|||||||
*/
|
*/
|
||||||
public function exists(Key $key)
|
public function exists(Key $key)
|
||||||
{
|
{
|
||||||
return $this->memcached->get((string) $key) === $this->getToken($key);
|
return $this->memcached->get((string) $key) === $this->getUniqueToken($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function getUniqueToken(Key $key): string
|
||||||
* Retrieve an unique token for the given key.
|
|
||||||
*/
|
|
||||||
private function getToken(Key $key): string
|
|
||||||
{
|
{
|
||||||
if (!$key->hasState(__CLASS__)) {
|
if (!$key->hasState(__CLASS__)) {
|
||||||
$token = base64_encode(random_bytes(32));
|
$token = base64_encode(random_bytes(32));
|
||||||
|
361
src/Symfony/Component/Lock/Store/PdoStore.php
Normal file
361
src/Symfony/Component/Lock/Store/PdoStore.php
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Lock\Store;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Doctrine\DBAL\DBALException;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||||
|
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||||
|
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||||
|
use Symfony\Component\Lock\Key;
|
||||||
|
use Symfony\Component\Lock\StoreInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PdoStore is a StoreInterface implementation using a PDO connection.
|
||||||
|
*
|
||||||
|
* Lock metadata are stored in a table. You can use createTable() to initialize
|
||||||
|
* a correctly defined table.
|
||||||
|
|
||||||
|
* CAUTION: This store relies on all client and server nodes to have
|
||||||
|
* synchronized clocks for lock expiry to occur at the correct time.
|
||||||
|
* To ensure locks don't expire prematurely; the ttl's should be set with enough
|
||||||
|
* extra time to account for any clock drift between nodes.
|
||||||
|
*
|
||||||
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||||
|
*/
|
||||||
|
class PdoStore implements StoreInterface
|
||||||
|
{
|
||||||
|
private $conn;
|
||||||
|
private $dsn;
|
||||||
|
private $driver;
|
||||||
|
private $table = 'lock_keys';
|
||||||
|
private $idCol = 'key_id';
|
||||||
|
private $tokenCol = 'key_token';
|
||||||
|
private $expirationCol = 'key_expiration';
|
||||||
|
private $username = '';
|
||||||
|
private $password = '';
|
||||||
|
private $connectionOptions = array();
|
||||||
|
private $gcProbability;
|
||||||
|
private $initialTtl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can either pass an existing database connection as PDO instance or
|
||||||
|
* a Doctrine DBAL Connection or a DSN string that will be used to
|
||||||
|
* lazy-connect to the database when the lock is actually used.
|
||||||
|
*
|
||||||
|
* List of available options:
|
||||||
|
* * db_table: The name of the table [default: lock_keys]
|
||||||
|
* * db_id_col: The column where to store the lock key [default: key_id]
|
||||||
|
* * db_token_col: The column where to store the lock token [default: key_token]
|
||||||
|
* * db_expiration_col: The column where to store the expiration [default: key_expiration]
|
||||||
|
* * db_username: The username when lazy-connect [default: '']
|
||||||
|
* * db_password: The password when lazy-connect [default: '']
|
||||||
|
* * db_connection_options: An array of driver-specific connection options [default: array()]
|
||||||
|
*
|
||||||
|
* @param \PDO|Connection|string $connOrDsn A \PDO or Connection instance or DSN string or null
|
||||||
|
* @param array $options An associative array of options
|
||||||
|
* @param float $gcProbability Probability expressed as floating number between 0 and 1 to clean old locks
|
||||||
|
* @param int $initialTtl The expiration delay of locks in seconds
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException When first argument is not PDO nor Connection nor string
|
||||||
|
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
|
||||||
|
* @throws InvalidArgumentException When namespace contains invalid characters
|
||||||
|
* @throws InvalidArgumentException When the initial ttl is not valid
|
||||||
|
*/
|
||||||
|
public function __construct($connOrDsn, array $options = array(), float $gcProbability = 0.01, int $initialTtl = 300)
|
||||||
|
{
|
||||||
|
if ($gcProbability < 0 || $gcProbability > 1) {
|
||||||
|
throw new InvalidArgumentException(sprintf('"%s" requires gcProbability between 0 and 1, "%f" given.', __METHOD__, $gcProbability));
|
||||||
|
}
|
||||||
|
if ($initialTtl < 1) {
|
||||||
|
throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($connOrDsn instanceof \PDO) {
|
||||||
|
if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
|
||||||
|
throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __METHOD__));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->conn = $connOrDsn;
|
||||||
|
} elseif ($connOrDsn instanceof Connection) {
|
||||||
|
$this->conn = $connOrDsn;
|
||||||
|
} elseif (\is_string($connOrDsn)) {
|
||||||
|
$this->dsn = $connOrDsn;
|
||||||
|
} else {
|
||||||
|
throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, \is_object($connOrDsn) ? \get_class($connOrDsn) : \gettype($connOrDsn)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->table = $options['db_table'] ?? $this->table;
|
||||||
|
$this->idCol = $options['db_id_col'] ?? $this->idCol;
|
||||||
|
$this->tokenCol = $options['db_token_col'] ?? $this->tokenCol;
|
||||||
|
$this->expirationCol = $options['db_expiration_col'] ?? $this->expirationCol;
|
||||||
|
$this->username = $options['db_username'] ?? $this->username;
|
||||||
|
$this->password = $options['db_password'] ?? $this->password;
|
||||||
|
$this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
|
||||||
|
|
||||||
|
$this->gcProbability = $gcProbability;
|
||||||
|
$this->initialTtl = $initialTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function save(Key $key)
|
||||||
|
{
|
||||||
|
$key->reduceLifetime($this->initialTtl);
|
||||||
|
|
||||||
|
$sql = "INSERT INTO $this->table ($this->idCol, $this->tokenCol, $this->expirationCol) VALUES (:id, :token, {$this->getCurrentTimestampStatement()} + $this->initialTtl)";
|
||||||
|
$stmt = $this->getConnection()->prepare($sql);
|
||||||
|
|
||||||
|
$stmt->bindValue(':id', $this->getHashedKey($key));
|
||||||
|
$stmt->bindValue(':token', $this->getUniqueToken($key));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt->execute();
|
||||||
|
if ($key->isExpired()) {
|
||||||
|
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (DBALException $e) {
|
||||||
|
// the lock is already acquired. It could be us. Let's try to put off.
|
||||||
|
$this->putOffExpiration($key, $this->initialTtl);
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
// the lock is already acquired. It could be us. Let's try to put off.
|
||||||
|
$this->putOffExpiration($key, $this->initialTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($key->isExpired()) {
|
||||||
|
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->gcProbability > 0 && (1.0 === $this->gcProbability || (random_int(0, PHP_INT_MAX) / PHP_INT_MAX) <= $this->gcProbability)) {
|
||||||
|
$this->prune();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function waitAndSave(Key $key)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException(sprintf('The store "%s" does not supports blocking locks.', __METHOD__));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function putOffExpiration(Key $key, $ttl)
|
||||||
|
{
|
||||||
|
if ($ttl < 1) {
|
||||||
|
throw new InvalidArgumentException(sprintf('%s() expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
|
||||||
|
}
|
||||||
|
|
||||||
|
$key->reduceLifetime($ttl);
|
||||||
|
|
||||||
|
$sql = "UPDATE $this->table SET $this->expirationCol = {$this->getCurrentTimestampStatement()} + $ttl, $this->tokenCol = :token WHERE $this->idCol = :id AND ($this->tokenCol = :token OR $this->expirationCol <= {$this->getCurrentTimestampStatement()})";
|
||||||
|
$stmt = $this->getConnection()->prepare($sql);
|
||||||
|
|
||||||
|
$stmt->bindValue(':id', $this->getHashedKey($key));
|
||||||
|
$stmt->bindValue(':token', $this->getUniqueToken($key));
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
// If this method is called twice in the same second, the row wouldn't be updated. We have to call exists to know if we are the owner
|
||||||
|
if (!$stmt->rowCount() && !$this->exists($key)) {
|
||||||
|
throw new LockConflictedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($key->isExpired()) {
|
||||||
|
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function delete(Key $key)
|
||||||
|
{
|
||||||
|
$sql = "DELETE FROM $this->table WHERE $this->idCol = :id AND $this->tokenCol = :token";
|
||||||
|
$stmt = $this->getConnection()->prepare($sql);
|
||||||
|
|
||||||
|
$stmt->bindValue(':id', $this->getHashedKey($key));
|
||||||
|
$stmt->bindValue(':token', $this->getUniqueToken($key));
|
||||||
|
$stmt->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function exists(Key $key)
|
||||||
|
{
|
||||||
|
$sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND $this->tokenCol = :token AND $this->expirationCol > {$this->getCurrentTimestampStatement()}";
|
||||||
|
$stmt = $this->getConnection()->prepare($sql);
|
||||||
|
|
||||||
|
$stmt->bindValue(':id', $this->getHashedKey($key));
|
||||||
|
$stmt->bindValue(':token', $this->getUniqueToken($key));
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
return (bool) $stmt->fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an hashed version of the key.
|
||||||
|
*/
|
||||||
|
private function getHashedKey(Key $key): string
|
||||||
|
{
|
||||||
|
return hash('sha256', $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getUniqueToken(Key $key): string
|
||||||
|
{
|
||||||
|
if (!$key->hasState(__CLASS__)) {
|
||||||
|
$token = base64_encode(random_bytes(32));
|
||||||
|
$key->setState(__CLASS__, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $key->getState(__CLASS__);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \PDO|Connection
|
||||||
|
*/
|
||||||
|
private function getConnection()
|
||||||
|
{
|
||||||
|
if (null === $this->conn) {
|
||||||
|
$this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
|
||||||
|
$this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the table to store lock keys which can be called once for setup.
|
||||||
|
*
|
||||||
|
* @throws \PDOException When the table already exists
|
||||||
|
* @throws DBALException When the table already exists
|
||||||
|
* @throws \DomainException When an unsupported PDO driver is used
|
||||||
|
*/
|
||||||
|
public function createTable(): void
|
||||||
|
{
|
||||||
|
// connect if we are not yet
|
||||||
|
$conn = $this->getConnection();
|
||||||
|
$driver = $this->getDriver();
|
||||||
|
|
||||||
|
if ($conn instanceof Connection) {
|
||||||
|
$schema = new Schema();
|
||||||
|
$table = $schema->createTable($this->table);
|
||||||
|
$table->addColumn($this->idCol, 'string', array('length' => 64));
|
||||||
|
$table->addColumn($this->tokenCol, 'string', array('length' => 44));
|
||||||
|
$table->addColumn($this->expirationCol, 'integer', array('unsigned' => true));
|
||||||
|
$table->setPrimaryKey(array($this->idCol));
|
||||||
|
|
||||||
|
foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) {
|
||||||
|
$conn->exec($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($driver) {
|
||||||
|
case 'mysql':
|
||||||
|
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(44) NOT NULL, $this->expirationCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB";
|
||||||
|
break;
|
||||||
|
case 'sqlite':
|
||||||
|
$sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->tokenCol TEXT NOT NULL, $this->expirationCol INTEGER)";
|
||||||
|
break;
|
||||||
|
case 'pgsql':
|
||||||
|
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(64) NOT NULL, $this->expirationCol INTEGER)";
|
||||||
|
break;
|
||||||
|
case 'oci':
|
||||||
|
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR2(64) NOT NULL, $this->expirationCol INTEGER)";
|
||||||
|
break;
|
||||||
|
case 'sqlsrv':
|
||||||
|
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(64) NOT NULL, $this->expirationCol INTEGER)";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new \DomainException(sprintf('Creating the lock table is currently not implemented for PDO driver "%s".', $driver));
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->exec($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanups the table by removing all expired locks.
|
||||||
|
*/
|
||||||
|
private function prune(): void
|
||||||
|
{
|
||||||
|
$sql = "DELETE FROM $this->table WHERE $this->expirationCol <= {$this->getCurrentTimestampStatement()}";
|
||||||
|
|
||||||
|
$stmt = $this->getConnection()->prepare($sql);
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDriver(): string
|
||||||
|
{
|
||||||
|
if (null !== $this->driver) {
|
||||||
|
return $this->driver;
|
||||||
|
}
|
||||||
|
|
||||||
|
$con = $this->getConnection();
|
||||||
|
if ($con instanceof \PDO) {
|
||||||
|
$this->driver = $con->getAttribute(\PDO::ATTR_DRIVER_NAME);
|
||||||
|
} else {
|
||||||
|
switch ($this->driver = $con->getDriver()->getName()) {
|
||||||
|
case 'mysqli':
|
||||||
|
case 'pdo_mysql':
|
||||||
|
case 'drizzle_pdo_mysql':
|
||||||
|
$this->driver = 'mysql';
|
||||||
|
break;
|
||||||
|
case 'pdo_sqlite':
|
||||||
|
$this->driver = 'sqlite';
|
||||||
|
break;
|
||||||
|
case 'pdo_pgsql':
|
||||||
|
$this->driver = 'pgsql';
|
||||||
|
break;
|
||||||
|
case 'oci8':
|
||||||
|
case 'pdo_oracle':
|
||||||
|
$this->driver = 'oci';
|
||||||
|
break;
|
||||||
|
case 'pdo_sqlsrv':
|
||||||
|
$this->driver = 'sqlsrv';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->driver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a SQL function to get the current timestamp regarding the current connection's driver.
|
||||||
|
*/
|
||||||
|
private function getCurrentTimestampStatement(): string
|
||||||
|
{
|
||||||
|
switch ($this->getDriver()) {
|
||||||
|
case 'mysql':
|
||||||
|
return 'UNIX_TIMESTAMP()';
|
||||||
|
case 'sqlite':
|
||||||
|
return 'strftime(\'%s\',\'now\')';
|
||||||
|
case 'pgsql':
|
||||||
|
return 'CAST(EXTRACT(epoch FROM NOW()) AS INT)';
|
||||||
|
case 'oci':
|
||||||
|
return '(SYSDATE - TO_DATE(\'19700101\',\'yyyymmdd\'))*86400 - TO_NUMBER(SUBSTR(TZ_OFFSET(sessiontimezone), 1, 3))*3600';
|
||||||
|
case 'sqlsrv':
|
||||||
|
return 'DATEDIFF(s, \'1970-01-01\', GETUTCDATE())';
|
||||||
|
default:
|
||||||
|
return time();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -60,7 +60,7 @@ class RedisStore implements StoreInterface
|
|||||||
';
|
';
|
||||||
|
|
||||||
$key->reduceLifetime($this->initialTtl);
|
$key->reduceLifetime($this->initialTtl);
|
||||||
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($this->initialTtl * 1000)))) {
|
if (!$this->evaluate($script, (string) $key, array($this->getUniqueToken($key), (int) ceil($this->initialTtl * 1000)))) {
|
||||||
throw new LockConflictedException();
|
throw new LockConflictedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ class RedisStore implements StoreInterface
|
|||||||
';
|
';
|
||||||
|
|
||||||
$key->reduceLifetime($ttl);
|
$key->reduceLifetime($ttl);
|
||||||
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($ttl * 1000)))) {
|
if (!$this->evaluate($script, (string) $key, array($this->getUniqueToken($key), (int) ceil($ttl * 1000)))) {
|
||||||
throw new LockConflictedException();
|
throw new LockConflictedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ class RedisStore implements StoreInterface
|
|||||||
end
|
end
|
||||||
';
|
';
|
||||||
|
|
||||||
$this->evaluate($script, (string) $key, array($this->getToken($key)));
|
$this->evaluate($script, (string) $key, array($this->getUniqueToken($key)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,7 +118,7 @@ class RedisStore implements StoreInterface
|
|||||||
*/
|
*/
|
||||||
public function exists(Key $key)
|
public function exists(Key $key)
|
||||||
{
|
{
|
||||||
return $this->redis->get((string) $key) === $this->getToken($key);
|
return $this->redis->get((string) $key) === $this->getUniqueToken($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -143,10 +143,7 @@ class RedisStore implements StoreInterface
|
|||||||
throw new InvalidArgumentException(sprintf('%s() expects been initialized with a Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, \is_object($this->redis) ? \get_class($this->redis) : \gettype($this->redis)));
|
throw new InvalidArgumentException(sprintf('%s() expects been initialized with a Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, \is_object($this->redis) ? \get_class($this->redis) : \gettype($this->redis)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function getUniqueToken(Key $key): string
|
||||||
* Retrieves an unique token for the given key.
|
|
||||||
*/
|
|
||||||
private function getToken(Key $key): string
|
|
||||||
{
|
{
|
||||||
if (!$key->hasState(__CLASS__)) {
|
if (!$key->hasState(__CLASS__)) {
|
||||||
$token = base64_encode(random_bytes(32));
|
$token = base64_encode(random_bytes(32));
|
||||||
|
@ -22,6 +22,8 @@ trait BlockingStoreTestTrait
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @see AbstractStoreTest::getStore()
|
* @see AbstractStoreTest::getStore()
|
||||||
|
*
|
||||||
|
* @return StoreInterface
|
||||||
*/
|
*/
|
||||||
abstract protected function getStore();
|
abstract protected function getStore();
|
||||||
|
|
||||||
@ -39,8 +41,6 @@ trait BlockingStoreTestTrait
|
|||||||
// Amount a microsecond used to order async actions
|
// Amount a microsecond used to order async actions
|
||||||
$clockDelay = 50000;
|
$clockDelay = 50000;
|
||||||
|
|
||||||
/** @var StoreInterface $store */
|
|
||||||
$store = $this->getStore();
|
|
||||||
$key = new Key(uniqid(__METHOD__, true));
|
$key = new Key(uniqid(__METHOD__, true));
|
||||||
$parentPID = posix_getpid();
|
$parentPID = posix_getpid();
|
||||||
|
|
||||||
@ -51,6 +51,7 @@ trait BlockingStoreTestTrait
|
|||||||
// Wait the start of the child
|
// Wait the start of the child
|
||||||
pcntl_sigwaitinfo(array(SIGHUP), $info);
|
pcntl_sigwaitinfo(array(SIGHUP), $info);
|
||||||
|
|
||||||
|
$store = $this->getStore();
|
||||||
try {
|
try {
|
||||||
// This call should failed given the lock should already by acquired by the child
|
// This call should failed given the lock should already by acquired by the child
|
||||||
$store->save($key);
|
$store->save($key);
|
||||||
@ -72,6 +73,8 @@ trait BlockingStoreTestTrait
|
|||||||
} else {
|
} else {
|
||||||
// Block SIGHUP signal
|
// Block SIGHUP signal
|
||||||
pcntl_sigprocmask(SIG_BLOCK, array(SIGHUP));
|
pcntl_sigprocmask(SIG_BLOCK, array(SIGHUP));
|
||||||
|
|
||||||
|
$store = $this->getStore();
|
||||||
try {
|
try {
|
||||||
$store->save($key);
|
$store->save($key);
|
||||||
// send the ready signal to the parent
|
// send the ready signal to the parent
|
||||||
|
@ -48,7 +48,7 @@ trait ExpiringStoreTestTrait
|
|||||||
$store->putOffExpiration($key, $clockDelay / 1000000);
|
$store->putOffExpiration($key, $clockDelay / 1000000);
|
||||||
$this->assertTrue($store->exists($key));
|
$this->assertTrue($store->exists($key));
|
||||||
|
|
||||||
usleep(2 * $clockDelay);
|
usleep(3 * $clockDelay);
|
||||||
$this->assertFalse($store->exists($key));
|
$this->assertFalse($store->exists($key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
61
src/Symfony/Component/Lock/Tests/Store/PdoDbalStoreTest.php
Normal file
61
src/Symfony/Component/Lock/Tests/Store/PdoDbalStoreTest.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Lock\Tests\Store;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\DriverManager;
|
||||||
|
use Symfony\Component\Lock\Store\PdoStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||||
|
*
|
||||||
|
* @requires extension pdo_sqlite
|
||||||
|
*/
|
||||||
|
class PdoDbalStoreTest extends AbstractStoreTest
|
||||||
|
{
|
||||||
|
use ExpiringStoreTestTrait;
|
||||||
|
|
||||||
|
protected static $dbFile;
|
||||||
|
|
||||||
|
public static function setupBeforeClass()
|
||||||
|
{
|
||||||
|
self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_lock');
|
||||||
|
|
||||||
|
$store = new PdoStore(DriverManager::getConnection(array('driver' => 'pdo_sqlite', 'path' => self::$dbFile)));
|
||||||
|
$store->createTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function tearDownAfterClass()
|
||||||
|
{
|
||||||
|
@unlink(self::$dbFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function getClockDelay()
|
||||||
|
{
|
||||||
|
return 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getStore()
|
||||||
|
{
|
||||||
|
return new PdoStore(DriverManager::getConnection(array('driver' => 'pdo_sqlite', 'path' => self::$dbFile)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAbortAfterExpiration()
|
||||||
|
{
|
||||||
|
$this->markTestSkipped('Pdo expects a TTL greater than 1 sec. Simulating a slow network is too hard');
|
||||||
|
}
|
||||||
|
}
|
60
src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php
Normal file
60
src/Symfony/Component/Lock/Tests/Store/PdoStoreTest.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Lock\Tests\Store;
|
||||||
|
|
||||||
|
use Symfony\Component\Lock\Store\PdoStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||||
|
*
|
||||||
|
* @requires extension pdo_sqlite
|
||||||
|
*/
|
||||||
|
class PdoStoreTest extends AbstractStoreTest
|
||||||
|
{
|
||||||
|
use ExpiringStoreTestTrait;
|
||||||
|
|
||||||
|
protected static $dbFile;
|
||||||
|
|
||||||
|
public static function setupBeforeClass()
|
||||||
|
{
|
||||||
|
self::$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_lock');
|
||||||
|
|
||||||
|
$store = new PdoStore('sqlite:'.self::$dbFile);
|
||||||
|
$store->createTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function tearDownAfterClass()
|
||||||
|
{
|
||||||
|
@unlink(self::$dbFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function getClockDelay()
|
||||||
|
{
|
||||||
|
return 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getStore()
|
||||||
|
{
|
||||||
|
return new PdoStore('sqlite:'.self::$dbFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAbortAfterExpiration()
|
||||||
|
{
|
||||||
|
$this->markTestSkipped('Pdo expects a TTL greater than 1 sec. Simulating a slow network is too hard');
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,8 @@
|
|||||||
"psr/log": "~1.0"
|
"psr/log": "~1.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"predis/predis": "~1.0"
|
"predis/predis": "~1.0",
|
||||||
|
"doctrine/dbal": "~2.4"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": { "Symfony\\Component\\Lock\\": "" },
|
"psr-4": { "Symfony\\Component\\Lock\\": "" },
|
||||||
|
Reference in New Issue
Block a user