symfony/lock 추가
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
||||
<?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 Psr\Log\LoggerAwareInterface;
|
||||
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\StoreInterface;
|
||||
|
||||
/**
|
||||
* CombinedStore is a StoreInterface implementation able to manage and synchronize several StoreInterfaces.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/** @var StoreInterface[] */
|
||||
private $stores;
|
||||
/** @var StrategyInterface */
|
||||
private $strategy;
|
||||
|
||||
/**
|
||||
* @param StoreInterface[] $stores The list of synchronized stores
|
||||
* @param StrategyInterface $strategy
|
||||
*
|
||||
* @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)));
|
||||
}
|
||||
}
|
||||
|
||||
$this->stores = $stores;
|
||||
$this->strategy = $strategy;
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$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));
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
if (!$this->strategy->canBeMet($failureCount, $storesCount)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// clean up potential locks
|
||||
$this->delete($key);
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$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));
|
||||
$key->reduceLifetime(0);
|
||||
break;
|
||||
}
|
||||
|
||||
$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));
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
if (!$this->strategy->canBeMet($failureCount, $storesCount)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $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));
|
||||
|
||||
// clean up potential locks
|
||||
$this->delete($key);
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
foreach ($this->stores as $store) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$storesCount = count($this->stores);
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
if ($store->exists($key)) {
|
||||
++$successCount;
|
||||
} else {
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return true;
|
||||
}
|
||||
if (!$this->strategy->canBeMet($failureCount, $storesCount)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
<?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\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockStorageException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* FlockStore is a StoreInterface implementation using the FileSystem flock.
|
||||
*
|
||||
* Original implementation in \Symfony\Component\Filesystem\LockHandler.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
* @author Romain Neutron <imprec@gmail.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class FlockStore implements StoreInterface
|
||||
{
|
||||
private $lockPath;
|
||||
|
||||
/**
|
||||
* @param string|null $lockPath the directory to store the lock, defaults to the system's temporary directory
|
||||
*
|
||||
* @throws LockStorageException If the lock directory doesn’t exist or is not writable
|
||||
*/
|
||||
public function __construct($lockPath = null)
|
||||
{
|
||||
if (null === $lockPath) {
|
||||
$lockPath = sys_get_temp_dir();
|
||||
}
|
||||
if (!is_dir($lockPath) || !is_writable($lockPath)) {
|
||||
throw new InvalidArgumentException(sprintf('The directory "%s" is not writable.', $lockPath));
|
||||
}
|
||||
|
||||
$this->lockPath = $lockPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$this->lock($key, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
$this->lock($key, true);
|
||||
}
|
||||
|
||||
private function lock(Key $key, $blocking)
|
||||
{
|
||||
// The lock is maybe already acquired.
|
||||
if ($key->hasState(__CLASS__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fileName = sprintf('%s/sf.%s.%s.lock',
|
||||
$this->lockPath,
|
||||
preg_replace('/[^a-z0-9\._-]+/i', '-', $key),
|
||||
strtr(substr(base64_encode(hash('sha256', $key, true)), 0, 7), '/', '_')
|
||||
);
|
||||
|
||||
// Silence error reporting
|
||||
set_error_handler(function () {
|
||||
});
|
||||
if (!$handle = fopen($fileName, 'r')) {
|
||||
if ($handle = fopen($fileName, 'x')) {
|
||||
chmod($fileName, 0444);
|
||||
} elseif (!$handle = fopen($fileName, 'r')) {
|
||||
usleep(100); // Give some time for chmod() to complete
|
||||
$handle = fopen($fileName, 'r');
|
||||
}
|
||||
}
|
||||
restore_error_handler();
|
||||
|
||||
if (!$handle) {
|
||||
$error = error_get_last();
|
||||
throw new LockStorageException($error['message'], 0, null);
|
||||
}
|
||||
|
||||
// On Windows, even if PHP doc says the contrary, LOCK_NB works, see
|
||||
// https://bugs.php.net/54129
|
||||
if (!flock($handle, LOCK_EX | ($blocking ? 0 : LOCK_NB))) {
|
||||
fclose($handle);
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$key->setState(__CLASS__, $handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
// do nothing, the flock locks forever.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
// The lock is maybe not acquired.
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$handle = $key->getState(__CLASS__);
|
||||
|
||||
flock($handle, LOCK_UN | LOCK_NB);
|
||||
fclose($handle);
|
||||
|
||||
$key->removeState(__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $key->hasState(__CLASS__);
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
<?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\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* MemcachedStore is a StoreInterface implementation using Memcached as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class MemcachedStore implements StoreInterface
|
||||
{
|
||||
private $memcached;
|
||||
private $initialTtl;
|
||||
/** @var bool */
|
||||
private $useExtendedReturn;
|
||||
|
||||
public static function isSupported()
|
||||
{
|
||||
return extension_loaded('memcached');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Memcached $memcached
|
||||
* @param int $initialTtl the expiration delay of locks in seconds
|
||||
*/
|
||||
public function __construct(\Memcached $memcached, $initialTtl = 300)
|
||||
{
|
||||
if (!static::isSupported()) {
|
||||
throw new InvalidArgumentException('Memcached extension is required');
|
||||
}
|
||||
|
||||
if ($initialTtl < 1) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
}
|
||||
|
||||
$this->memcached = $memcached;
|
||||
$this->initialTtl = $initialTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$token = $this->getToken($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));
|
||||
}
|
||||
}
|
||||
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
// Interface defines a float value but Store required an integer.
|
||||
$ttl = (int) ceil($ttl);
|
||||
|
||||
$token = $this->getToken($key);
|
||||
|
||||
list($value, $cas) = $this->getValueAndCas($key);
|
||||
|
||||
$key->reduceLifetime($ttl);
|
||||
// Could happens when we ask a putOff after a timeout but in luck nobody steal the lock
|
||||
if (\Memcached::RES_NOTFOUND === $this->memcached->getResultCode()) {
|
||||
if ($this->memcached->add((string) $key, $token, $ttl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// no luck, with concurrency, someone else acquire the lock
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
// Someone else steal the lock
|
||||
if ($value !== $token) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
if (!$this->memcached->cas($cas, (string) $key, $token, $ttl)) {
|
||||
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)
|
||||
{
|
||||
$token = $this->getToken($key);
|
||||
|
||||
list($value, $cas) = $this->getValueAndCas($key);
|
||||
|
||||
if ($value !== $token) {
|
||||
// we are not the owner of the lock. Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// To avoid concurrency in deletion, the trick is to extends the TTL then deleting the key
|
||||
if (!$this->memcached->cas($cas, (string) $key, $token, 2)) {
|
||||
// Someone steal our lock. It does not belongs to us anymore. Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, we are the owner of the lock for 2 more seconds, we can delete it.
|
||||
$this->memcached->delete((string) $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $this->memcached->get((string) $key) === $this->getToken($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an unique token for the given key.
|
||||
*
|
||||
* @param Key $key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getToken(Key $key)
|
||||
{
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
$token = base64_encode(random_bytes(32));
|
||||
$key->setState(__CLASS__, $token);
|
||||
}
|
||||
|
||||
return $key->getState(__CLASS__);
|
||||
}
|
||||
|
||||
private function getValueAndCas(Key $key)
|
||||
{
|
||||
if (null === $this->useExtendedReturn) {
|
||||
$this->useExtendedReturn = version_compare(phpversion('memcached'), '2.9.9', '>');
|
||||
}
|
||||
|
||||
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 array($extendedReturn['value'], $extendedReturn['cas']);
|
||||
}
|
||||
|
||||
$cas = 0.0;
|
||||
$value = $this->memcached->get((string) $key, null, $cas);
|
||||
|
||||
return array($value, $cas);
|
||||
}
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
<?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\Cache\Traits\RedisProxy;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* RedisStore is a StoreInterface implementation using Redis as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class RedisStore implements StoreInterface
|
||||
{
|
||||
private $redis;
|
||||
private $initialTtl;
|
||||
|
||||
/**
|
||||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient
|
||||
* @param float $initialTtl the expiration delay of locks in seconds
|
||||
*/
|
||||
public function __construct($redisClient, $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 ($initialTtl <= 0) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
}
|
||||
|
||||
$this->redis = $redisClient;
|
||||
$this->initialTtl = $initialTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$script = '
|
||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
|
||||
else
|
||||
return redis.call("set", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
|
||||
end
|
||||
';
|
||||
|
||||
$key->reduceLifetime($this->initialTtl);
|
||||
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($this->initialTtl * 1000)))) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
|
||||
}
|
||||
}
|
||||
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
$script = '
|
||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
';
|
||||
|
||||
$key->reduceLifetime($ttl);
|
||||
if (!$this->evaluate($script, (string) $key, array($this->getToken($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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$script = '
|
||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("DEL", KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
';
|
||||
|
||||
$this->evaluate($script, (string) $key, array($this->getToken($key)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $this->redis->get((string) $key) === $this->getToken($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a script in the corresponding redis client.
|
||||
*
|
||||
* @param string $script
|
||||
* @param string $resource
|
||||
* @param array $args
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function evaluate($script, $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 \RedisArray) {
|
||||
return $this->redis->_instance($this->redis->_target($resource))->eval($script, array_merge(array($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));
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an unique token for the given key.
|
||||
*
|
||||
* @param Key $key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getToken(Key $key)
|
||||
{
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
$token = base64_encode(random_bytes(32));
|
||||
$key->setState(__CLASS__, $token);
|
||||
}
|
||||
|
||||
return $key->getState(__CLASS__);
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
<?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 Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* RetryTillSaveStore is a StoreInterface implementation which decorate a non blocking StoreInterface to provide a
|
||||
* blocking storage.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class RetryTillSaveStore implements StoreInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
private $decorated;
|
||||
private $retrySleep;
|
||||
private $retryCount;
|
||||
|
||||
/**
|
||||
* @param StoreInterface $decorated The decorated StoreInterface
|
||||
* @param int $retrySleep Duration in ms between 2 retry
|
||||
* @param int $retryCount Maximum amount of retry
|
||||
*/
|
||||
public function __construct(StoreInterface $decorated, $retrySleep = 100, $retryCount = PHP_INT_MAX)
|
||||
{
|
||||
$this->decorated = $decorated;
|
||||
$this->retrySleep = $retrySleep;
|
||||
$this->retryCount = $retryCount;
|
||||
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$this->decorated->save($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
$retry = 0;
|
||||
$sleepRandomness = (int) ($this->retrySleep / 10);
|
||||
do {
|
||||
try {
|
||||
$this->decorated->save($key);
|
||||
|
||||
return;
|
||||
} catch (LockConflictedException $e) {
|
||||
usleep(($this->retrySleep + random_int(-$sleepRandomness, $sleepRandomness)) * 1000);
|
||||
}
|
||||
} while (++$retry < $this->retryCount);
|
||||
|
||||
$this->logger->warning('Failed to store the "{resource}" lock. Abort after {retry} retry.', array('resource' => $key, 'retry' => $retry));
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
$this->decorated->putOffExpiration($key, $ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$this->decorated->delete($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $this->decorated->exists($key);
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
<?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\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* SemaphoreStore is a StoreInterface implementation using Semaphore as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class SemaphoreStore implements StoreInterface
|
||||
{
|
||||
/**
|
||||
* Returns whether or not the store is supported.
|
||||
*
|
||||
* @param bool|null $blocking when not null, checked again the blocking mode
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function isSupported($blocking = null)
|
||||
{
|
||||
if (!extension_loaded('sysvsem')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (false === $blocking && \PHP_VERSION_ID < 50601) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (!static::isSupported()) {
|
||||
throw new InvalidArgumentException('Semaphore extension (sysvsem) is required');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$this->lock($key, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
$this->lock($key, true);
|
||||
}
|
||||
|
||||
private function lock(Key $key, $blocking)
|
||||
{
|
||||
if ($key->hasState(__CLASS__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resource = sem_get(crc32($key));
|
||||
|
||||
if (\PHP_VERSION_ID < 50601) {
|
||||
if (!$blocking) {
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not supports non blocking locks.', get_class($this)));
|
||||
}
|
||||
|
||||
$acquired = sem_acquire($resource);
|
||||
} else {
|
||||
$acquired = sem_acquire($resource, !$blocking);
|
||||
}
|
||||
|
||||
if (!$acquired) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$key->setState(__CLASS__, $resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
// The lock is maybe not acquired.
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resource = $key->getState(__CLASS__);
|
||||
|
||||
sem_release($resource);
|
||||
|
||||
$key->removeState(__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
// do nothing, the semaphore locks forever.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $key->hasState(__CLASS__);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?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\Cache\Traits\RedisProxy;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* StoreFactory create stores and connections.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class StoreFactory
|
||||
{
|
||||
/**
|
||||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client|\Memcached $connection
|
||||
*
|
||||
* @return RedisStore|MemcachedStore
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', get_class($connection)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user