재 업데이트

This commit is contained in:
2020-01-28 01:17:00 +09:00
parent a8b3befca9
commit ddb971e41e
413 changed files with 13446 additions and 16025 deletions
+27 -23
View File
@@ -16,37 +16,37 @@ use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
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\Strategy\StrategyInterface;
use Symfony\Component\Lock\PersistingStoreInterface;
use Symfony\Component\Lock\StoreInterface;
use Symfony\Component\Lock\Strategy\StrategyInterface;
/**
* CombinedStore is a StoreInterface implementation able to manage and synchronize several StoreInterfaces.
* CombinedStore is a PersistingStoreInterface implementation able to manage and synchronize several StoreInterfaces.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class CombinedStore implements StoreInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
use ExpiringStoreTrait;
/** @var StoreInterface[] */
/** @var PersistingStoreInterface[] */
private $stores;
/** @var StrategyInterface */
private $strategy;
/**
* @param StoreInterface[] $stores The list of synchronized stores
* @param StrategyInterface $strategy
* @param PersistingStoreInterface[] $stores The list of synchronized stores
*
* @throws InvalidArgumentException
*/
public function __construct(array $stores, StrategyInterface $strategy)
{
foreach ($stores as $store) {
if (!$store instanceof StoreInterface) {
throw new InvalidArgumentException(sprintf('The store must implement "%s". Got "%s".', StoreInterface::class, get_class($store)));
if (!$store instanceof PersistingStoreInterface) {
throw new InvalidArgumentException(sprintf('The store must implement "%s". Got "%s".', PersistingStoreInterface::class, \get_class($store)));
}
}
@@ -62,14 +62,14 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
{
$successCount = 0;
$failureCount = 0;
$storesCount = count($this->stores);
$storesCount = \count($this->stores);
foreach ($this->stores as $store) {
try {
$store->save($key);
++$successCount;
} catch (\Exception $e) {
$this->logger->warning('One store failed to save the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
$this->logger->warning('One store failed to save the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
++$failureCount;
}
@@ -78,11 +78,13 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
}
}
$this->checkNotExpired($key);
if ($this->strategy->isMet($successCount, $storesCount)) {
return;
}
$this->logger->warning('Failed to store the "{resource}" lock. Quorum has not been met.', array('resource' => $key, 'success' => $successCount, 'failure' => $failureCount));
$this->logger->warning('Failed to store the "{resource}" lock. Quorum has not been met.', ['resource' => $key, 'success' => $successCount, 'failure' => $failureCount]);
// clean up potential locks
$this->delete($key);
@@ -90,9 +92,15 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
throw new LockConflictedException();
}
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.4.
*/
public function waitAndSave(Key $key)
{
throw new NotSupportedException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
}
/**
@@ -102,13 +110,13 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
{
$successCount = 0;
$failureCount = 0;
$storesCount = count($this->stores);
$storesCount = \count($this->stores);
$expireAt = microtime(true) + $ttl;
foreach ($this->stores as $store) {
try {
if (0.0 >= $adjustedTtl = $expireAt - microtime(true)) {
$this->logger->warning('Stores took to long to put off the expiration of the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'ttl' => $ttl));
$this->logger->warning('Stores took to long to put off the expiration of the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'ttl' => $ttl]);
$key->reduceLifetime(0);
break;
}
@@ -116,7 +124,7 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
$store->putOffExpiration($key, $adjustedTtl);
++$successCount;
} catch (\Exception $e) {
$this->logger->warning('One store failed to put off the expiration of the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
$this->logger->warning('One store failed to put off the expiration of the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
++$failureCount;
}
@@ -125,15 +133,13 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
}
}
if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
}
$this->checkNotExpired($key);
if ($this->strategy->isMet($successCount, $storesCount)) {
return;
}
$this->logger->warning('Failed to define the expiration for the "{resource}" lock. Quorum has not been met.', array('resource' => $key, 'success' => $successCount, 'failure' => $failureCount));
$this->logger->warning('Failed to define the expiration for the "{resource}" lock. Quorum has not been met.', ['resource' => $key, 'success' => $successCount, 'failure' => $failureCount]);
// clean up potential locks
$this->delete($key);
@@ -150,9 +156,7 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
try {
$store->delete($key);
} catch (\Exception $e) {
$this->logger->notice('One store failed to delete the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
} catch (\Throwable $e) {
$this->logger->notice('One store failed to delete the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
$this->logger->notice('One store failed to delete the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
}
}
}
@@ -164,7 +168,7 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
{
$successCount = 0;
$failureCount = 0;
$storesCount = count($this->stores);
$storesCount = \count($this->stores);
foreach ($this->stores as $store) {
if ($store->exists($key)) {
+30
View File
@@ -0,0 +1,30 @@
<?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 Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Key;
trait ExpiringStoreTrait
{
private function checkNotExpired(Key $key)
{
if ($key->isExpired()) {
try {
$this->delete($key);
} catch (\Exception $e) {
// swallow exception to not hide the original issue
}
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
}
}
}
+5 -4
View File
@@ -11,6 +11,7 @@
namespace Symfony\Component\Lock\Store;
use Symfony\Component\Lock\BlockingStoreInterface;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockStorageException;
@@ -18,7 +19,7 @@ use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
/**
* FlockStore is a StoreInterface implementation using the FileSystem flock.
* FlockStore is a PersistingStoreInterface implementation using the FileSystem flock.
*
* Original implementation in \Symfony\Component\Filesystem\LockHandler.
*
@@ -27,7 +28,7 @@ use Symfony\Component\Lock\StoreInterface;
* @author Romain Neutron <imprec@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class FlockStore implements StoreInterface
class FlockStore implements StoreInterface, BlockingStoreInterface
{
private $lockPath;
@@ -64,7 +65,7 @@ class FlockStore implements StoreInterface
$this->lock($key, true);
}
private function lock(Key $key, $blocking)
private function lock(Key $key, bool $blocking)
{
// The lock is maybe already acquired.
if ($key->hasState(__CLASS__)) {
@@ -81,7 +82,7 @@ class FlockStore implements StoreInterface
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
if (!$handle = fopen($fileName, 'r+') ?: fopen($fileName, 'r')) {
if ($handle = fopen($fileName, 'x')) {
chmod($fileName, 0444);
chmod($fileName, 0666);
} elseif (!$handle = fopen($fileName, 'r+') ?: fopen($fileName, 'r')) {
usleep(100); // Give some time for chmod() to complete
$handle = fopen($fileName, 'r+') ?: fopen($fileName, 'r');
+26 -25
View File
@@ -12,18 +12,21 @@
namespace Symfony\Component\Lock\Store;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\InvalidTtlException;
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;
/**
* MemcachedStore is a StoreInterface implementation using Memcached as store engine.
* MemcachedStore is a PersistingStoreInterface implementation using Memcached as store engine.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class MemcachedStore implements StoreInterface
{
use ExpiringStoreTrait;
private $memcached;
private $initialTtl;
/** @var bool */
@@ -31,12 +34,11 @@ class MemcachedStore implements StoreInterface
public static function isSupported()
{
return extension_loaded('memcached');
return \extension_loaded('memcached');
}
/**
* @param \Memcached $memcached
* @param int $initialTtl the expiration delay of locks in seconds
* @param int $initialTtl the expiration delay of locks in seconds
*/
public function __construct(\Memcached $memcached, int $initialTtl = 300)
{
@@ -57,21 +59,25 @@ class MemcachedStore implements StoreInterface
*/
public function save(Key $key)
{
$token = $this->getToken($key);
$token = $this->getUniqueToken($key);
$key->reduceLifetime($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.
$this->putOffExpiration($key, $this->initialTtl);
}
if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
}
$this->checkNotExpired($key);
}
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.4.
*/
public function waitAndSave(Key $key)
{
throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
}
/**
@@ -80,13 +86,13 @@ class MemcachedStore implements StoreInterface
public function putOffExpiration(Key $key, $ttl)
{
if ($ttl < 1) {
throw new InvalidArgumentException(sprintf('%s() expects a TTL greater or equals to 1. Got %s.', __METHOD__, $ttl));
throw new InvalidTtlException(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.
$ttl = (int) ceil($ttl);
$token = $this->getToken($key);
$token = $this->getUniqueToken($key);
list($value, $cas) = $this->getValueAndCas($key);
@@ -110,9 +116,7 @@ class MemcachedStore implements StoreInterface
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));
}
$this->checkNotExpired($key);
}
/**
@@ -120,7 +124,7 @@ class MemcachedStore implements StoreInterface
*/
public function delete(Key $key)
{
$token = $this->getToken($key);
$token = $this->getUniqueToken($key);
list($value, $cas) = $this->getValueAndCas($key);
@@ -144,13 +148,10 @@ class MemcachedStore implements StoreInterface
*/
public function exists(Key $key)
{
return $this->memcached->get((string) $key) === $this->getToken($key);
return $this->memcached->get((string) $key) === $this->getUniqueToken($key);
}
/**
* Retrieve an unique token for the given key.
*/
private function getToken(Key $key): string
private function getUniqueToken(Key $key): string
{
if (!$key->hasState(__CLASS__)) {
$token = base64_encode(random_bytes(32));
@@ -160,7 +161,7 @@ class MemcachedStore implements StoreInterface
return $key->getState(__CLASS__);
}
private function getValueAndCas(Key $key)
private function getValueAndCas(Key $key): array
{
if (null === $this->useExtendedReturn) {
$this->useExtendedReturn = version_compare(phpversion('memcached'), '2.9.9', '>');
@@ -169,15 +170,15 @@ class MemcachedStore implements StoreInterface
if ($this->useExtendedReturn) {
$extendedReturn = $this->memcached->get((string) $key, null, \Memcached::GET_EXTENDED);
if (\Memcached::GET_ERROR_RETURN_VALUE === $extendedReturn) {
return array($extendedReturn, 0.0);
return [$extendedReturn, 0.0];
}
return array($extendedReturn['value'], $extendedReturn['cas']);
return [$extendedReturn['value'], $extendedReturn['cas']];
}
$cas = 0.0;
$value = $this->memcached->get((string) $key, null, $cas);
return array($value, $cas);
return [$value, $cas];
}
}
+363
View File
@@ -0,0 +1,363 @@
<?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\DriverManager;
use Doctrine\DBAL\Schema\Schema;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\InvalidTtlException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\NotSupportedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
/**
* PdoStore is a PersistingStoreInterface 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 TTLs 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
{
use ExpiringStoreTrait;
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 = [];
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: []]
*
* @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 = [], 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 InvalidTtlException(sprintf('%s() expects a strictly positive TTL, "%d" given.', __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();
} 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 ($this->gcProbability > 0 && (1.0 === $this->gcProbability || (random_int(0, PHP_INT_MAX) / PHP_INT_MAX) <= $this->gcProbability)) {
$this->prune();
}
$this->checkNotExpired($key);
}
/**
* {@inheritdoc}
*/
public function waitAndSave(Key $key)
{
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
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 InvalidTtlException(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 = :token1 WHERE $this->idCol = :id AND ($this->tokenCol = :token2 OR $this->expirationCol <= {$this->getCurrentTimestampStatement()})";
$stmt = $this->getConnection()->prepare($sql);
$uniqueToken = $this->getUniqueToken($key);
$stmt->bindValue(':id', $this->getHashedKey($key));
$stmt->bindValue(':token1', $uniqueToken);
$stmt->bindValue(':token2', $uniqueToken);
$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();
}
$this->checkNotExpired($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 a hashed version of the key.
*/
private function getHashedKey(Key $key): string
{
return hash('sha256', (string) $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) {
if (strpos($this->dsn, '://')) {
if (!class_exists(DriverManager::class)) {
throw new InvalidArgumentException(sprintf('Failed to parse the DSN "%s". Try running "composer require doctrine/dbal".', $this->dsn));
}
$this->conn = DriverManager::getConnection(['url' => $this->dsn]);
} else {
$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', ['length' => 64]);
$table->addColumn($this->tokenCol, 'string', ['length' => 44]);
$table->addColumn($this->expirationCol, 'integer', ['unsigned' => true]);
$table->setPrimaryKey([$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);
}
/**
* Cleans up the table by removing all expired locks.
*/
private function prune(): void
{
$sql = "DELETE FROM $this->table WHERE $this->expirationCol <= {$this->getCurrentTimestampStatement()}";
$this->getConnection()->exec($sql);
}
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();
}
}
}
+39 -29
View File
@@ -11,35 +11,39 @@
namespace Symfony\Component\Lock\Store;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\InvalidTtlException;
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;
/**
* RedisStore is a StoreInterface implementation using Redis as store engine.
* RedisStore is a PersistingStoreInterface implementation using Redis as store engine.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class RedisStore implements StoreInterface
{
use ExpiringStoreTrait;
private $redis;
private $initialTtl;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient
* @param float $initialTtl the expiration delay of locks in seconds
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface $redisClient
* @param float $initialTtl the expiration delay of locks in seconds
*/
public function __construct($redisClient, float $initialTtl = 300.0)
{
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client && !$redisClient instanceof RedisProxy) {
throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redisClient) ? get_class($redisClient) : gettype($redisClient)));
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\ClientInterface && !$redisClient instanceof RedisProxy) {
throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, %s given', __METHOD__, \is_object($redisClient) ? \get_class($redisClient) : \gettype($redisClient)));
}
if ($initialTtl <= 0) {
throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
throw new InvalidTtlException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
}
$this->redis = $redisClient;
@@ -54,24 +58,30 @@ class RedisStore implements StoreInterface
$script = '
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
elseif redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
return 1
else
return redis.call("set", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
return 0
end
';
$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, [$this->getUniqueToken($key), (int) ceil($this->initialTtl * 1000)])) {
throw new LockConflictedException();
}
if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
}
$this->checkNotExpired($key);
}
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.4.
*/
public function waitAndSave(Key $key)
{
throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
}
/**
@@ -88,13 +98,11 @@ class RedisStore implements StoreInterface
';
$key->reduceLifetime($ttl);
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($ttl * 1000)))) {
if (!$this->evaluate($script, (string) $key, [$this->getUniqueToken($key), (int) ceil($ttl * 1000)])) {
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));
}
$this->checkNotExpired($key);
}
/**
@@ -110,7 +118,7 @@ class RedisStore implements StoreInterface
end
';
$this->evaluate($script, (string) $key, array($this->getToken($key)));
$this->evaluate($script, (string) $key, [$this->getUniqueToken($key)]);
}
/**
@@ -118,7 +126,7 @@ class RedisStore implements StoreInterface
*/
public function exists(Key $key)
{
return $this->redis->get((string) $key) === $this->getToken($key);
return $this->redis->get((string) $key) === $this->getUniqueToken($key);
}
/**
@@ -128,25 +136,27 @@ class RedisStore implements StoreInterface
*/
private function evaluate(string $script, string $resource, array $args)
{
if ($this->redis instanceof \Redis || $this->redis instanceof \RedisCluster || $this->redis instanceof RedisProxy) {
return $this->redis->eval($script, array_merge(array($resource), $args), 1);
if (
$this->redis instanceof \Redis ||
$this->redis instanceof \RedisCluster ||
$this->redis instanceof RedisProxy ||
$this->redis instanceof RedisClusterProxy
) {
return $this->redis->eval($script, array_merge([$resource], $args), 1);
}
if ($this->redis instanceof \RedisArray) {
return $this->redis->_instance($this->redis->_target($resource))->eval($script, array_merge(array($resource), $args), 1);
return $this->redis->_instance($this->redis->_target($resource))->eval($script, array_merge([$resource], $args), 1);
}
if ($this->redis instanceof \Predis\Client) {
return call_user_func_array(array($this->redis, 'eval'), array_merge(array($script, 1, $resource), $args));
if ($this->redis instanceof \Predis\ClientInterface) {
return $this->redis->eval(...array_merge([$script, 1, $resource], $args));
}
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 being initialized with a Redis, RedisArray, RedisCluster or Predis\ClientInterface, %s given', __METHOD__, \is_object($this->redis) ? \get_class($this->redis) : \gettype($this->redis)));
}
/**
* Retrieves an unique token for the given key.
*/
private function getToken(Key $key): string
private function getUniqueToken(Key $key): string
{
if (!$key->hasState(__CLASS__)) {
$token = base64_encode(random_bytes(32));
+8 -7
View File
@@ -14,17 +14,19 @@ namespace Symfony\Component\Lock\Store;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Lock\BlockingStoreInterface;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\PersistingStoreInterface;
use Symfony\Component\Lock\StoreInterface;
/**
* RetryTillSaveStore is a StoreInterface implementation which decorate a non blocking StoreInterface to provide a
* RetryTillSaveStore is a PersistingStoreInterface implementation which decorate a non blocking PersistingStoreInterface to provide a
* blocking storage.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class RetryTillSaveStore implements StoreInterface, LoggerAwareInterface
class RetryTillSaveStore implements BlockingStoreInterface, StoreInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
@@ -33,11 +35,10 @@ class RetryTillSaveStore implements StoreInterface, LoggerAwareInterface
private $retryCount;
/**
* @param StoreInterface $decorated The decorated StoreInterface
* @param int $retrySleep Duration in ms between 2 retry
* @param int $retryCount Maximum amount of retry
* @param int $retrySleep Duration in ms between 2 retry
* @param int $retryCount Maximum amount of retry
*/
public function __construct(StoreInterface $decorated, int $retrySleep = 100, int $retryCount = PHP_INT_MAX)
public function __construct(PersistingStoreInterface $decorated, int $retrySleep = 100, int $retryCount = PHP_INT_MAX)
{
$this->decorated = $decorated;
$this->retrySleep = $retrySleep;
@@ -71,7 +72,7 @@ class RetryTillSaveStore implements StoreInterface, LoggerAwareInterface
}
} while (++$retry < $this->retryCount);
$this->logger->warning('Failed to store the "{resource}" lock. Abort after {retry} retry.', array('resource' => $key, 'retry' => $retry));
$this->logger->warning('Failed to store the "{resource}" lock. Abort after {retry} retry.', ['resource' => $key, 'retry' => $retry]);
throw new LockConflictedException();
}
+6 -7
View File
@@ -11,28 +11,27 @@
namespace Symfony\Component\Lock\Store;
use Symfony\Component\Lock\BlockingStoreInterface;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
/**
* SemaphoreStore is a StoreInterface implementation using Semaphore as store engine.
* SemaphoreStore is a PersistingStoreInterface implementation using Semaphore as store engine.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class SemaphoreStore implements StoreInterface
class SemaphoreStore implements StoreInterface, BlockingStoreInterface
{
/**
* Returns whether or not the store is supported.
*
* @return bool
*
* @internal
*/
public static function isSupported()
public static function isSupported(): bool
{
return extension_loaded('sysvsem');
return \extension_loaded('sysvsem');
}
public function __construct()
@@ -58,7 +57,7 @@ class SemaphoreStore implements StoreInterface
$this->lock($key, true);
}
private function lock(Key $key, $blocking)
private function lock(Key $key, bool $blocking)
{
if ($key->hasState(__CLASS__)) {
return;
+68 -8
View File
@@ -11,8 +11,12 @@
namespace Symfony\Component\Lock\Store;
use Doctrine\DBAL\Connection;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\PersistingStoreInterface;
/**
* StoreFactory create stores and connections.
@@ -22,19 +26,75 @@ use Symfony\Component\Lock\Exception\InvalidArgumentException;
class StoreFactory
{
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client|\Memcached $connection
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy|\Memcached|\PDO|Connection|\Zookeeper|string $connection Connection or DSN or Store short name
*
* @return RedisStore|MemcachedStore
* @return PersistingStoreInterface
*/
public static function createStore($connection)
{
if ($connection instanceof \Redis || $connection instanceof \RedisArray || $connection instanceof \RedisCluster || $connection instanceof \Predis\Client || $connection instanceof RedisProxy) {
return new RedisStore($connection);
}
if ($connection instanceof \Memcached) {
return new MemcachedStore($connection);
if (!\is_string($connection) && !\is_object($connection)) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be a string or a connection object, %s given.', __METHOD__, \gettype($connection)));
}
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', get_class($connection)));
switch (true) {
case $connection instanceof \Redis:
case $connection instanceof \RedisArray:
case $connection instanceof \RedisCluster:
case $connection instanceof \Predis\ClientInterface:
case $connection instanceof RedisProxy:
case $connection instanceof RedisClusterProxy:
return new RedisStore($connection);
case $connection instanceof \Memcached:
return new MemcachedStore($connection);
case $connection instanceof \PDO:
case $connection instanceof Connection:
return new PdoStore($connection);
case $connection instanceof \Zookeeper:
return new ZookeeperStore($connection);
case !\is_string($connection):
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', \get_class($connection)));
case 'flock' === $connection:
return new FlockStore();
case 0 === strpos($connection, 'flock://'):
return new FlockStore(substr($connection, 8));
case 'semaphore' === $connection:
return new SemaphoreStore();
case 0 === strpos($connection, 'redis://'):
case 0 === strpos($connection, 'rediss://'):
case 0 === strpos($connection, 'memcached://'):
if (!class_exists(AbstractAdapter::class)) {
throw new InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection));
}
$storeClass = 0 === strpos($connection, 'memcached://') ? MemcachedStore::class : RedisStore::class;
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
return new $storeClass($connection);
case 0 === strpos($connection, 'mssql://'):
case 0 === strpos($connection, 'mysql:'):
case 0 === strpos($connection, 'mysql2://'):
case 0 === strpos($connection, 'oci:'):
case 0 === strpos($connection, 'oci8://'):
case 0 === strpos($connection, 'pdo_oci://'):
case 0 === strpos($connection, 'pgsql:'):
case 0 === strpos($connection, 'postgres://'):
case 0 === strpos($connection, 'postgresql://'):
case 0 === strpos($connection, 'sqlsrv:'):
case 0 === strpos($connection, 'sqlite:'):
case 0 === strpos($connection, 'sqlite3://'):
return new PdoStore($connection);
case 0 === strpos($connection, 'zookeeper://'):
return new ZookeeperStore(ZookeeperStore::createConnection($connection));
}
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', $connection));
}
}
+176
View File
@@ -0,0 +1,176 @@
<?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 Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockReleasingException;
use Symfony\Component\Lock\Exception\NotSupportedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
/**
* ZookeeperStore is a PersistingStoreInterface implementation using Zookeeper as store engine.
*
* @author Ganesh Chandrasekaran <gchandrasekaran@wayfair.com>
*/
class ZookeeperStore implements StoreInterface
{
use ExpiringStoreTrait;
private $zookeeper;
public function __construct(\Zookeeper $zookeeper)
{
$this->zookeeper = $zookeeper;
}
public static function createConnection(string $dsn): \Zookeeper
{
if (0 !== strpos($dsn, 'zookeeper:')) {
throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
}
if (false === $params = parse_url($dsn)) {
throw new InvalidArgumentException(sprintf('Invalid Zookeeper DSN: %s.', $dsn));
}
$host = $params['host'] ?? '';
if (isset($params['port'])) {
$host .= ':'.$params['port'];
}
return new \Zookeeper($host);
}
/**
* {@inheritdoc}
*/
public function save(Key $key)
{
if ($this->exists($key)) {
return;
}
$resource = $this->getKeyResource($key);
$token = $this->getUniqueToken($key);
$this->createNewLock($resource, $token);
$this->checkNotExpired($key);
}
/**
* {@inheritdoc}
*/
public function delete(Key $key)
{
if (!$this->exists($key)) {
return;
}
$resource = $this->getKeyResource($key);
try {
$this->zookeeper->delete($resource);
} catch (\ZookeeperException $exception) {
// For Zookeeper Ephemeral Nodes, the node will be deleted upon session death. But, if we want to unlock
// the lock before proceeding further in the session, the client should be aware of this
throw new LockReleasingException($exception);
}
}
/**
* {@inheritdoc}
*/
public function exists(Key $key): bool
{
$resource = $this->getKeyResource($key);
try {
return $this->zookeeper->get($resource) === $this->getUniqueToken($key);
} catch (\ZookeeperException $ex) {
return false;
}
}
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.4.
*/
public function waitAndSave(Key $key)
{
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
throw new NotSupportedException();
}
/**
* {@inheritdoc}
*/
public function putOffExpiration(Key $key, $ttl)
{
// do nothing, zookeeper locks forever.
}
/**
* Creates a zookeeper node.
*
* @param string $node The node which needs to be created
* @param string $value The value to be assigned to a zookeeper node
*
* @throws LockConflictedException
* @throws LockAcquiringException
*/
private function createNewLock(string $node, string $value)
{
// Default Node Permissions
$acl = [['perms' => \Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone']];
// This ensures that the nodes are deleted when the client session to zookeeper server ends.
$type = \Zookeeper::EPHEMERAL;
try {
$this->zookeeper->create($node, $value, $acl, $type);
} catch (\ZookeeperException $ex) {
if (\Zookeeper::NODEEXISTS === $ex->getCode()) {
throw new LockConflictedException($ex);
}
throw new LockAcquiringException($ex);
}
}
private function getKeyResource(Key $key): string
{
// Since we do not support storing locks as multi-level nodes, we convert them to be stored at root level.
// For example: foo/bar will become /foo-bar and /foo/bar will become /-foo-bar
$resource = (string) $key;
if (false !== strpos($resource, '/')) {
$resource = strtr($resource, ['/' => '-']).'-'.sha1($resource);
}
if ('' === $resource) {
$resource = sha1($resource);
}
return '/'.$resource;
}
private function getUniqueToken(Key $key): string
{
if (!$key->hasState(self::class)) {
$token = base64_encode(random_bytes(32));
$key->setState(self::class, $token);
}
return $key->getState(self::class);
}
}