Initial commit

This commit is contained in:
2021-01-19 18:19:37 +01:00
commit 6524a071df
14506 changed files with 1808535 additions and 0 deletions

3
core/vendor/symfony/lock/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
composer.lock
phpunit.xml
vendor/

7
core/vendor/symfony/lock/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,7 @@
CHANGELOG
=========
3.4.0
-----
* added the component

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* Base ExceptionInterface for the Lock Component.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface ExceptionInterface
{
}

View File

@@ -0,0 +1,19 @@
<?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\Exception;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* LockAcquiringException is thrown when an issue happens during the acquisition of a lock.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockAcquiringException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* LockConflictedException is thrown when a lock is acquired by someone else.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockConflictedException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* LockExpiredException is thrown when a lock may conflict due to a TTL expiration.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockExpiredException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* LockReleasingException is thrown when an issue happens during the release of a lock.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockReleasingException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* LockStorageException is thrown when an issue happens during the manipulation of a lock in a store.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockStorageException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* NotSupportedException is thrown when an unsupported method is called.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class NotSupportedException extends \LogicException implements ExceptionInterface
{
}

52
core/vendor/symfony/lock/Factory.php vendored Normal file
View File

@@ -0,0 +1,52 @@
<?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;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
/**
* Factory provides method to create locks.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class Factory implements LoggerAwareInterface
{
use LoggerAwareTrait;
private $store;
public function __construct(StoreInterface $store)
{
$this->store = $store;
$this->logger = new NullLogger();
}
/**
* Creates a lock for the given resource.
*
* @param string $resource The resource to lock
* @param float|null $ttl Maximum expected lock duration in seconds
* @param bool $autoRelease Whether to automatically release the lock or not when the lock instance is destroyed
*
* @return Lock
*/
public function createLock($resource, $ttl = 300.0, $autoRelease = true)
{
$lock = new Lock(new Key($resource), $this->store, $ttl, $autoRelease);
$lock->setLogger($this->logger);
return $lock;
}
}

109
core/vendor/symfony/lock/Key.php vendored Normal file
View File

@@ -0,0 +1,109 @@
<?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;
/**
* Key is a container for the state of the locks in stores.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
final class Key
{
private $resource;
private $expiringTime;
private $state = array();
/**
* @param string $resource
*/
public function __construct($resource)
{
$this->resource = (string) $resource;
}
public function __toString()
{
return $this->resource;
}
/**
* @param string $stateKey
*
* @return bool
*/
public function hasState($stateKey)
{
return isset($this->state[$stateKey]);
}
/**
* @param string $stateKey
* @param mixed $state
*/
public function setState($stateKey, $state)
{
$this->state[$stateKey] = $state;
}
/**
* @param string $stateKey
*/
public function removeState($stateKey)
{
unset($this->state[$stateKey]);
}
/**
* @param $stateKey
*
* @return mixed
*/
public function getState($stateKey)
{
return $this->state[$stateKey];
}
public function resetLifetime()
{
$this->expiringTime = null;
}
/**
* @param float $ttl the expiration delay of locks in seconds
*/
public function reduceLifetime($ttl)
{
$newTime = microtime(true) + $ttl;
if (null === $this->expiringTime || $this->expiringTime > $newTime) {
$this->expiringTime = $newTime;
}
}
/**
* Returns the remaining lifetime.
*
* @return float|null Remaining lifetime in seconds. Null when the key won't expire.
*/
public function getRemainingLifetime()
{
return null === $this->expiringTime ? null : $this->expiringTime - microtime(true);
}
/**
* @return bool
*/
public function isExpired()
{
return null !== $this->expiringTime && $this->expiringTime <= microtime(true);
}
}

19
core/vendor/symfony/lock/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2016-2019 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

173
core/vendor/symfony/lock/Lock.php vendored Normal file
View File

@@ -0,0 +1,173 @@
<?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;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Exception\LockReleasingException;
/**
* Lock is the default implementation of the LockInterface.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
final class Lock implements LockInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
private $store;
private $key;
private $ttl;
private $autoRelease;
private $dirty = false;
/**
* @param Key $key Resource to lock
* @param StoreInterface $store Store used to handle lock persistence
* @param float|null $ttl Maximum expected lock duration in seconds
* @param bool $autoRelease Whether to automatically release the lock or not when the lock instance is destroyed
*/
public function __construct(Key $key, StoreInterface $store, $ttl = null, $autoRelease = true)
{
$this->store = $store;
$this->key = $key;
$this->ttl = $ttl;
$this->autoRelease = (bool) $autoRelease;
$this->logger = new NullLogger();
}
/**
* Automatically releases the underlying lock when the object is destructed.
*/
public function __destruct()
{
if (!$this->autoRelease || !$this->dirty || !$this->isAcquired()) {
return;
}
$this->release();
}
/**
* {@inheritdoc}
*/
public function acquire($blocking = false)
{
try {
if ($blocking) {
$this->store->waitAndSave($this->key);
} else {
$this->store->save($this->key);
}
$this->dirty = true;
$this->logger->info('Successfully acquired the "{resource}" lock.', array('resource' => $this->key));
if ($this->ttl) {
$this->refresh();
}
if ($this->key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $this->key));
}
return true;
} catch (LockConflictedException $e) {
$this->dirty = false;
$this->logger->notice('Failed to acquire the "{resource}" lock. Someone else already acquired the lock.', array('resource' => $this->key));
if ($blocking) {
throw $e;
}
return false;
} catch (\Exception $e) {
$this->logger->notice('Failed to acquire the "{resource}" lock.', array('resource' => $this->key, 'exception' => $e));
throw new LockAcquiringException(sprintf('Failed to acquire the "%s" lock.', $this->key), 0, $e);
}
}
/**
* {@inheritdoc}
*/
public function refresh()
{
if (!$this->ttl) {
throw new InvalidArgumentException('You have to define an expiration duration.');
}
try {
$this->key->resetLifetime();
$this->store->putOffExpiration($this->key, $this->ttl);
$this->dirty = true;
if ($this->key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $this->key));
}
$this->logger->info('Expiration defined for "{resource}" lock for "{ttl}" seconds.', array('resource' => $this->key, 'ttl' => $this->ttl));
} catch (LockConflictedException $e) {
$this->dirty = false;
$this->logger->notice('Failed to define an expiration for the "{resource}" lock, someone else acquired the lock.', array('resource' => $this->key));
throw $e;
} catch (\Exception $e) {
$this->logger->notice('Failed to define an expiration for the "{resource}" lock.', array('resource' => $this->key, 'exception' => $e));
throw new LockAcquiringException(sprintf('Failed to define an expiration for the "%s" lock.', $this->key), 0, $e);
}
}
/**
* {@inheritdoc}
*/
public function isAcquired()
{
return $this->dirty = $this->store->exists($this->key);
}
/**
* {@inheritdoc}
*/
public function release()
{
$this->store->delete($this->key);
$this->dirty = false;
if ($this->store->exists($this->key)) {
$this->logger->notice('Failed to release the "{resource}" lock.', array('resource' => $this->key));
throw new LockReleasingException(sprintf('Failed to release the "%s" lock.', $this->key));
}
}
/**
* @return bool
*/
public function isExpired()
{
return $this->key->isExpired();
}
/**
* Returns the remaining lifetime.
*
* @return float|null Remaining lifetime in seconds. Null when the lock won't expire.
*/
public function getRemainingLifetime()
{
return $this->key->getRemainingLifetime();
}
}

View File

@@ -0,0 +1,59 @@
<?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;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockReleasingException;
/**
* LockInterface defines an interface to manipulate the status of a lock.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface LockInterface
{
/**
* Acquires the lock. If the lock is acquired by someone else, the parameter `blocking` determines whether or not
* the the call should block until the release of the lock.
*
* @param bool $blocking Whether or not the Lock should wait for the release of someone else
*
* @return bool whether or not the lock had been acquired
*
* @throws LockConflictedException If the lock is acquired by someone else in blocking mode
* @throws LockAcquiringException If the lock can not be acquired
*/
public function acquire($blocking = false);
/**
* Increase the duration of an acquired lock.
*
* @throws LockConflictedException If the lock is acquired by someone else
* @throws LockAcquiringException If the lock can not be refreshed
*/
public function refresh();
/**
* Returns whether or not the lock is acquired.
*
* @return bool
*/
public function isAcquired();
/**
* Release the lock.
*
* @throws LockReleasingException If the lock can not be released
*/
public function release();
}

11
core/vendor/symfony/lock/README.md vendored Normal file
View File

@@ -0,0 +1,11 @@
Lock Component
==============
Resources
---------
* [Documentation](https://symfony.com/doc/master/components/lock.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -0,0 +1,184 @@
<?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\StoreInterface;
use Symfony\Component\Lock\Strategy\StrategyInterface;
/**
* 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));
}
}
}
/**
* {@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;
}
}

View File

@@ -0,0 +1,139 @@
<?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 doesnt 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 ($type, $msg) use (&$error) { $error = $msg; });
if (!$handle = fopen($fileName, 'r+') ?: fopen($fileName, 'r')) {
if ($handle = fopen($fileName, 'x')) {
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');
}
}
restore_error_handler();
if (!$handle) {
throw new LockStorageException($error, 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__);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,168 @@
<?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])
elseif redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
return 1
else
return 0
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 being 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__);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,133 @@
<?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;
}
$keyId = crc32($key);
$resource = sem_get($keyId);
if (\PHP_VERSION_ID >= 50601) {
$acquired = @sem_acquire($resource, !$blocking);
} elseif (!$blocking) {
throw new NotSupportedException(sprintf('The store "%s" does not supports non blocking locks.', \get_class($this)));
} else {
$acquired = @sem_acquire($resource);
}
while ($blocking && !$acquired) {
$resource = sem_get($keyId);
$acquired = @sem_acquire($resource);
}
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_remove($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__);
}
}

View File

@@ -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)));
}
}

View File

@@ -0,0 +1,64 @@
<?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;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\NotSupportedException;
/**
* StoreInterface defines an interface to manipulate a lock store.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface StoreInterface
{
/**
* Stores the resource if it's not locked by someone else.
*
* @throws LockConflictedException
*/
public function save(Key $key);
/**
* Waits until a key becomes free, then stores the resource.
*
* If the store does not support this feature it should throw a NotSupportedException.
*
* @throws LockConflictedException
* @throws NotSupportedException
*/
public function waitAndSave(Key $key);
/**
* Extends the ttl of a resource.
*
* If the store does not support this feature it should throw a NotSupportedException.
*
* @param float $ttl amount of seconds to keep the lock in the store
*
* @throws LockConflictedException
* @throws NotSupportedException
*/
public function putOffExpiration(Key $key, $ttl);
/**
* Removes a resource from the storage.
*/
public function delete(Key $key);
/**
* Returns whether or not the resource exists in the storage.
*
* @return bool
*/
public function exists(Key $key);
}

View File

@@ -0,0 +1,36 @@
<?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\Strategy;
/**
* ConsensusStrategy is a StrategyInterface implementation where strictly more than 50% items should be successful.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class ConsensusStrategy implements StrategyInterface
{
/**
* {@inheritdoc}
*/
public function isMet($numberOfSuccess, $numberOfItems)
{
return $numberOfSuccess > ($numberOfItems / 2);
}
/**
* {@inheritdoc}
*/
public function canBeMet($numberOfFailure, $numberOfItems)
{
return $numberOfFailure < ($numberOfItems / 2);
}
}

View File

@@ -0,0 +1,43 @@
<?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\Strategy;
/**
* StrategyInterface defines an interface to indicate when a quorum is met and can be met.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface StrategyInterface
{
/**
* Returns whether or not the quorum is met.
*
* @param int $numberOfSuccess
* @param int $numberOfItems
*
* @return bool
*/
public function isMet($numberOfSuccess, $numberOfItems);
/**
* Returns whether or not the quorum *could* be met.
*
* This method does not mean the quorum *would* be met for sure, but can be useful to stop a process early when you
* known there is no chance to meet the quorum.
*
* @param int $numberOfFailure
* @param int $numberOfItems
*
* @return bool
*/
public function canBeMet($numberOfFailure, $numberOfItems);
}

View File

@@ -0,0 +1,36 @@
<?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\Strategy;
/**
* UnanimousStrategy is a StrategyInterface implementation where 100% of elements should be successful.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class UnanimousStrategy implements StrategyInterface
{
/**
* {@inheritdoc}
*/
public function isMet($numberOfSuccess, $numberOfItems)
{
return $numberOfSuccess === $numberOfItems;
}
/**
* {@inheritdoc}
*/
public function canBeMet($numberOfFailure, $numberOfItems)
{
return 0 === $numberOfFailure;
}
}

View File

@@ -0,0 +1,36 @@
<?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;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Lock\Factory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\StoreInterface;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class FactoryTest extends TestCase
{
public function testCreateLock()
{
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
$factory = new Factory($store);
$factory->setLogger($logger);
$lock = $factory->createLock('foo');
$this->assertInstanceOf(LockInterface::class, $lock);
}
}

View File

@@ -0,0 +1,254 @@
<?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;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\StoreInterface;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockTest extends TestCase
{
public function testAcquireNoBlocking()
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$lock = new Lock($key, $store);
$store
->expects($this->once())
->method('save');
$this->assertTrue($lock->acquire(false));
}
public function testAcquireReturnsFalse()
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$lock = new Lock($key, $store);
$store
->expects($this->once())
->method('save')
->willThrowException(new LockConflictedException());
$this->assertFalse($lock->acquire(false));
}
public function testAcquireBlocking()
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$lock = new Lock($key, $store);
$store
->expects($this->never())
->method('save');
$store
->expects($this->once())
->method('waitAndSave');
$this->assertTrue($lock->acquire(true));
}
public function testAcquireSetsTtl()
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
$store
->expects($this->once())
->method('save');
$store
->expects($this->once())
->method('putOffExpiration')
->with($key, 10);
$lock->acquire();
}
public function testRefresh()
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
$store
->expects($this->once())
->method('putOffExpiration')
->with($key, 10);
$lock->refresh();
}
public function testIsAquired()
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
$store
->expects($this->any())
->method('exists')
->with($key)
->will($this->onConsecutiveCalls(true, false));
$this->assertTrue($lock->isAcquired());
}
public function testRelease()
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
$store
->expects($this->once())
->method('delete')
->with($key);
$store
->expects($this->once())
->method('exists')
->with($key)
->willReturn(false);
$lock->release();
}
public function testReleaseOnDestruction()
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
$store
->method('exists')
->willReturnOnConsecutiveCalls(array(true, false))
;
$store
->expects($this->once())
->method('delete')
;
$lock->acquire(false);
unset($lock);
}
public function testNoAutoReleaseWhenNotConfigured()
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10, false);
$store
->method('exists')
->willReturnOnConsecutiveCalls(array(true, false))
;
$store
->expects($this->never())
->method('delete')
;
$lock->acquire(false);
unset($lock);
}
/**
* @expectedException \Symfony\Component\Lock\Exception\LockReleasingException
*/
public function testReleaseThrowsExceptionIfNotWellDeleted()
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
$store
->expects($this->once())
->method('delete')
->with($key);
$store
->expects($this->once())
->method('exists')
->with($key)
->willReturn(true);
$lock->release();
}
/**
* @expectedException \Symfony\Component\Lock\Exception\LockReleasingException
*/
public function testReleaseThrowsAndLog()
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
$lock = new Lock($key, $store, 10, true);
$lock->setLogger($logger);
$logger->expects($this->atLeastOnce())
->method('notice')
->with('Failed to release the "{resource}" lock.', array('resource' => $key));
$store
->expects($this->once())
->method('delete')
->with($key);
$store
->expects($this->once())
->method('exists')
->with($key)
->willReturn(true);
$lock->release();
}
/**
* @dataProvider provideExpiredDates
*/
public function testExpiration($ttls, $expected)
{
$key = new Key(uniqid(__METHOD__, true));
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
$lock = new Lock($key, $store, 10);
foreach ($ttls as $ttl) {
if (null === $ttl) {
$key->resetLifetime();
} else {
$key->reduceLifetime($ttl);
}
}
$this->assertSame($expected, $lock->isExpired());
}
public function provideExpiredDates()
{
yield array(array(-0.1), true);
yield array(array(0.1, -0.1), true);
yield array(array(-0.1, 0.1), true);
yield array(array(), false);
yield array(array(0.1), false);
yield array(array(-0.1, null), false);
}
}

View File

@@ -0,0 +1,45 @@
<?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\RedisStore;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
abstract class AbstractRedisStoreTest extends AbstractStoreTest
{
use ExpiringStoreTestTrait;
/**
* {@inheritdoc}
*/
protected function getClockDelay()
{
return 250000;
}
/**
* Return a RedisConnection.
*
* @return \Redis|\RedisArray|\RedisCluster|\Predis\Client
*/
abstract protected function getRedisConnection();
/**
* {@inheritdoc}
*/
public function getStore()
{
return new RedisStore($this->getRedisConnection());
}
}

View File

@@ -0,0 +1,115 @@
<?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 PHPUnit\Framework\TestCase;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
abstract class AbstractStoreTest extends TestCase
{
/**
* @return StoreInterface
*/
abstract protected function getStore();
public function testSave()
{
$store = $this->getStore();
$key = new Key(uniqid(__METHOD__, true));
$this->assertFalse($store->exists($key));
$store->save($key);
$this->assertTrue($store->exists($key));
$store->delete($key);
$this->assertFalse($store->exists($key));
}
public function testSaveWithDifferentResources()
{
$store = $this->getStore();
$key1 = new Key(uniqid(__METHOD__, true));
$key2 = new Key(uniqid(__METHOD__, true));
$store->save($key1);
$this->assertTrue($store->exists($key1));
$this->assertFalse($store->exists($key2));
$store->save($key2);
$this->assertTrue($store->exists($key1));
$this->assertTrue($store->exists($key2));
$store->delete($key1);
$this->assertFalse($store->exists($key1));
$this->assertTrue($store->exists($key2));
$store->delete($key2);
$this->assertFalse($store->exists($key1));
$this->assertFalse($store->exists($key2));
}
public function testSaveWithDifferentKeysOnSameResources()
{
$store = $this->getStore();
$resource = uniqid(__METHOD__, true);
$key1 = new Key($resource);
$key2 = new Key($resource);
$store->save($key1);
$this->assertTrue($store->exists($key1));
$this->assertFalse($store->exists($key2));
try {
$store->save($key2);
$this->fail('The store shouldn\'t save the second key');
} catch (LockConflictedException $e) {
}
// The failure of previous attempt should not impact the state of current locks
$this->assertTrue($store->exists($key1));
$this->assertFalse($store->exists($key2));
$store->delete($key1);
$this->assertFalse($store->exists($key1));
$this->assertFalse($store->exists($key2));
$store->save($key2);
$this->assertFalse($store->exists($key1));
$this->assertTrue($store->exists($key2));
$store->delete($key2);
$this->assertFalse($store->exists($key1));
$this->assertFalse($store->exists($key2));
}
public function testSaveTwice()
{
$store = $this->getStore();
$resource = uniqid(__METHOD__, true);
$key = new Key($resource);
$store->save($key);
$store->save($key);
// just asserts it don't throw an exception
$this->addToAssertionCount(1);
$store->delete($key);
}
}

View File

@@ -0,0 +1,98 @@
<?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\Exception\LockConflictedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
trait BlockingStoreTestTrait
{
/**
* @see AbstractStoreTest::getStore()
*/
abstract protected function getStore();
/**
* Tests blocking locks thanks to pcntl.
*
* This test is time sensible: the $clockDelay could be adjust.
*
* @requires extension pcntl
* @requires extension posix
* @requires function pcntl_sigwaitinfo
*/
public function testBlockingLocks()
{
// Amount of microseconds we should wait without slowing things down too much
$clockDelay = 50000;
if (\PHP_VERSION_ID < 50600 || \defined('HHVM_VERSION_ID')) {
$this->markTestSkipped('The PHP engine does not keep resource in child forks');
return;
}
/** @var StoreInterface $store */
$store = $this->getStore();
$key = new Key(uniqid(__METHOD__, true));
$parentPID = posix_getpid();
// Block SIGHUP signal
pcntl_sigprocmask(SIG_BLOCK, array(SIGHUP));
if ($childPID = pcntl_fork()) {
// Wait the start of the child
pcntl_sigwaitinfo(array(SIGHUP), $info);
try {
// This call should failed given the lock should already by acquired by the child
$store->save($key);
$this->fail('The store saves a locked key.');
} catch (LockConflictedException $e) {
}
// send the ready signal to the child
posix_kill($childPID, SIGHUP);
// This call should be blocked by the child #1
$store->waitAndSave($key);
$this->assertTrue($store->exists($key));
$store->delete($key);
// Now, assert the child process worked well
pcntl_waitpid($childPID, $status1);
$this->assertSame(0, pcntl_wexitstatus($status1), 'The child process couldn\'t lock the resource');
} else {
// Block SIGHUP signal
pcntl_sigprocmask(SIG_BLOCK, array(SIGHUP));
try {
$store->save($key);
// send the ready signal to the parent
posix_kill($parentPID, SIGHUP);
// Wait for the parent to be ready
pcntl_sigwaitinfo(array(SIGHUP), $info);
// Wait ClockDelay to let parent assert to finish
usleep($clockDelay);
$store->delete($key);
exit(0);
} catch (\Exception $e) {
exit(1);
}
}
}
}

View File

@@ -0,0 +1,356 @@
<?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\Exception\LockConflictedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\Store\CombinedStore;
use Symfony\Component\Lock\Store\RedisStore;
use Symfony\Component\Lock\StoreInterface;
use Symfony\Component\Lock\Strategy\StrategyInterface;
use Symfony\Component\Lock\Strategy\UnanimousStrategy;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class CombinedStoreTest extends AbstractStoreTest
{
use ExpiringStoreTestTrait;
/**
* {@inheritdoc}
*/
protected function getClockDelay()
{
return 250000;
}
/**
* {@inheritdoc}
*/
public function getStore()
{
$redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
try {
$redis->connect();
} catch (\Exception $e) {
self::markTestSkipped($e->getMessage());
}
return new CombinedStore(array(new RedisStore($redis)), new UnanimousStrategy());
}
/** @var \PHPUnit_Framework_MockObject_MockObject */
private $strategy;
/** @var \PHPUnit_Framework_MockObject_MockObject */
private $store1;
/** @var \PHPUnit_Framework_MockObject_MockObject */
private $store2;
/** @var CombinedStore */
private $store;
protected function setUp()
{
$this->strategy = $this->getMockBuilder(StrategyInterface::class)->getMock();
$this->store1 = $this->getMockBuilder(StoreInterface::class)->getMock();
$this->store2 = $this->getMockBuilder(StoreInterface::class)->getMock();
$this->store = new CombinedStore(array($this->store1, $this->store2), $this->strategy);
}
/**
* @expectedException \Symfony\Component\Lock\Exception\LockConflictedException
*/
public function testSaveThrowsExceptionOnFailure()
{
$key = new Key(uniqid(__METHOD__, true));
$this->store1
->expects($this->once())
->method('save')
->with($key)
->willThrowException(new LockConflictedException());
$this->store2
->expects($this->once())
->method('save')
->with($key)
->willThrowException(new LockConflictedException());
$this->strategy
->expects($this->any())
->method('canBeMet')
->willReturn(true);
$this->strategy
->expects($this->any())
->method('isMet')
->willReturn(false);
$this->store->save($key);
}
public function testSaveCleanupOnFailure()
{
$key = new Key(uniqid(__METHOD__, true));
$this->store1
->expects($this->once())
->method('save')
->with($key)
->willThrowException(new LockConflictedException());
$this->store2
->expects($this->once())
->method('save')
->with($key)
->willThrowException(new LockConflictedException());
$this->store1
->expects($this->once())
->method('delete');
$this->store2
->expects($this->once())
->method('delete');
$this->strategy
->expects($this->any())
->method('canBeMet')
->willReturn(true);
$this->strategy
->expects($this->any())
->method('isMet')
->willReturn(false);
try {
$this->store->save($key);
} catch (LockConflictedException $e) {
// Catch the exception given this is not what we want to assert in this tests
}
}
public function testSaveAbortWhenStrategyCantBeMet()
{
$key = new Key(uniqid(__METHOD__, true));
$this->store1
->expects($this->once())
->method('save')
->with($key)
->willThrowException(new LockConflictedException());
$this->store2
->expects($this->never())
->method('save');
$this->strategy
->expects($this->once())
->method('canBeMet')
->willReturn(false);
$this->strategy
->expects($this->any())
->method('isMet')
->willReturn(false);
try {
$this->store->save($key);
} catch (LockConflictedException $e) {
// Catch the exception given this is not what we want to assert in this tests
}
}
/**
* @expectedException \Symfony\Component\Lock\Exception\LockConflictedException
*/
public function testputOffExpirationThrowsExceptionOnFailure()
{
$key = new Key(uniqid(__METHOD__, true));
$ttl = random_int(1, 10);
$this->store1
->expects($this->once())
->method('putOffExpiration')
->with($key, $this->lessThanOrEqual($ttl))
->willThrowException(new LockConflictedException());
$this->store2
->expects($this->once())
->method('putOffExpiration')
->with($key, $this->lessThanOrEqual($ttl))
->willThrowException(new LockConflictedException());
$this->strategy
->expects($this->any())
->method('canBeMet')
->willReturn(true);
$this->strategy
->expects($this->any())
->method('isMet')
->willReturn(false);
$this->store->putOffExpiration($key, $ttl);
}
public function testputOffExpirationCleanupOnFailure()
{
$key = new Key(uniqid(__METHOD__, true));
$ttl = random_int(1, 10);
$this->store1
->expects($this->once())
->method('putOffExpiration')
->with($key, $this->lessThanOrEqual($ttl))
->willThrowException(new LockConflictedException());
$this->store2
->expects($this->once())
->method('putOffExpiration')
->with($key, $this->lessThanOrEqual($ttl))
->willThrowException(new LockConflictedException());
$this->store1
->expects($this->once())
->method('delete');
$this->store2
->expects($this->once())
->method('delete');
$this->strategy
->expects($this->any())
->method('canBeMet')
->willReturn(true);
$this->strategy
->expects($this->any())
->method('isMet')
->willReturn(false);
try {
$this->store->putOffExpiration($key, $ttl);
} catch (LockConflictedException $e) {
// Catch the exception given this is not what we want to assert in this tests
}
}
public function testputOffExpirationAbortWhenStrategyCantBeMet()
{
$key = new Key(uniqid(__METHOD__, true));
$ttl = random_int(1, 10);
$this->store1
->expects($this->once())
->method('putOffExpiration')
->with($key, $this->lessThanOrEqual($ttl))
->willThrowException(new LockConflictedException());
$this->store2
->expects($this->never())
->method('putOffExpiration');
$this->strategy
->expects($this->once())
->method('canBeMet')
->willReturn(false);
$this->strategy
->expects($this->any())
->method('isMet')
->willReturn(false);
try {
$this->store->putOffExpiration($key, $ttl);
} catch (LockConflictedException $e) {
// Catch the exception given this is not what we want to assert in this tests
}
}
public function testPutOffExpirationIgnoreNonExpiringStorage()
{
$store1 = $this->getMockBuilder(StoreInterface::class)->getMock();
$store2 = $this->getMockBuilder(StoreInterface::class)->getMock();
$store = new CombinedStore(array($store1, $store2), $this->strategy);
$key = new Key(uniqid(__METHOD__, true));
$ttl = random_int(1, 10);
$this->strategy
->expects($this->any())
->method('canBeMet')
->willReturn(true);
$this->strategy
->expects($this->once())
->method('isMet')
->with(2, 2)
->willReturn(true);
$store->putOffExpiration($key, $ttl);
}
public function testExistsDontAskToEveryBody()
{
$key = new Key(uniqid(__METHOD__, true));
$this->store1
->expects($this->any())
->method('exists')
->with($key)
->willReturn(false);
$this->store2
->expects($this->never())
->method('exists');
$this->strategy
->expects($this->any())
->method('canBeMet')
->willReturn(true);
$this->strategy
->expects($this->once())
->method('isMet')
->willReturn(true);
$this->assertTrue($this->store->exists($key));
}
public function testExistsAbortWhenStrategyCantBeMet()
{
$key = new Key(uniqid(__METHOD__, true));
$this->store1
->expects($this->any())
->method('exists')
->with($key)
->willReturn(false);
$this->store2
->expects($this->never())
->method('exists');
$this->strategy
->expects($this->once())
->method('canBeMet')
->willReturn(false);
$this->strategy
->expects($this->once())
->method('isMet')
->willReturn(false);
$this->assertFalse($this->store->exists($key));
}
public function testDeleteDontStopOnFailure()
{
$key = new Key(uniqid(__METHOD__, true));
$this->store1
->expects($this->once())
->method('delete')
->with($key)
->willThrowException(new \Exception());
$this->store2
->expects($this->once())
->method('delete')
->with($key);
$this->store->delete($key);
}
}

View File

@@ -0,0 +1,108 @@
<?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\Key;
use Symfony\Component\Lock\StoreInterface;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
trait ExpiringStoreTestTrait
{
/**
* Amount of microseconds used as a delay to test expiration. Should be
* small enough not to slow the test suite too much, and high enough not to
* fail because of race conditions.
*
* @return int
*/
abstract protected function getClockDelay();
/**
* @see AbstractStoreTest::getStore()
*/
abstract protected function getStore();
/**
* Tests the store automatically delete the key when it expire.
*
* This test is time-sensitive: the $clockDelay could be adjusted.
*/
public function testExpiration()
{
$key = new Key(uniqid(__METHOD__, true));
$clockDelay = $this->getClockDelay();
/** @var StoreInterface $store */
$store = $this->getStore();
$store->save($key);
$store->putOffExpiration($key, 2 * $clockDelay / 1000000);
$this->assertTrue($store->exists($key));
usleep(3 * $clockDelay);
$this->assertFalse($store->exists($key));
}
/**
* Tests the store thrown exception when TTL expires.
*
* @expectedException \Symfony\Component\Lock\Exception\LockExpiredException
*/
public function testAbortAfterExpiration()
{
$key = new Key(uniqid(__METHOD__, true));
/** @var StoreInterface $store */
$store = $this->getStore();
$store->save($key);
$store->putOffExpiration($key, 1 / 1000000);
}
/**
* Tests the refresh can push the limits to the expiration.
*
* This test is time-sensitive: the $clockDelay could be adjusted.
*/
public function testRefreshLock()
{
// Amount of microseconds we should wait without slowing things down too much
$clockDelay = $this->getClockDelay();
$key = new Key(uniqid(__METHOD__, true));
/** @var StoreInterface $store */
$store = $this->getStore();
$store->save($key);
$store->putOffExpiration($key, 2 * $clockDelay / 1000000);
$this->assertTrue($store->exists($key));
usleep(3 * $clockDelay);
$this->assertFalse($store->exists($key));
}
public function testSetExpiration()
{
$key = new Key(uniqid(__METHOD__, true));
/** @var StoreInterface $store */
$store = $this->getStore();
$store->save($key);
$store->putOffExpiration($key, 1);
$this->assertGreaterThanOrEqual(0, $key->getRemainingLifetime());
$this->assertLessThanOrEqual(1, $key->getRemainingLifetime());
}
}

View File

@@ -0,0 +1,78 @@
<?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\Key;
use Symfony\Component\Lock\Store\FlockStore;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class FlockStoreTest extends AbstractStoreTest
{
use BlockingStoreTestTrait;
/**
* {@inheritdoc}
*/
protected function getStore()
{
return new FlockStore();
}
/**
* @expectedException \Symfony\Component\Lock\Exception\InvalidArgumentException
* @expectedExceptionMessage The directory "/a/b/c/d/e" is not writable.
*/
public function testConstructWhenRepositoryDoesNotExist()
{
if (!getenv('USER') || 'root' === getenv('USER')) {
$this->markTestSkipped('This test will fail if run under superuser');
}
new FlockStore('/a/b/c/d/e');
}
/**
* @expectedException \Symfony\Component\Lock\Exception\InvalidArgumentException
* @expectedExceptionMessage The directory "/" is not writable.
*/
public function testConstructWhenRepositoryIsNotWriteable()
{
if (!getenv('USER') || 'root' === getenv('USER')) {
$this->markTestSkipped('This test will fail if run under superuser');
}
new FlockStore('/');
}
public function testSaveSanitizeName()
{
$store = $this->getStore();
$key = new Key('<?php echo "% hello word ! %" ?>');
$file = sprintf(
'%s/sf.-php-echo-hello-word-.%s.lock',
sys_get_temp_dir(),
strtr(substr(base64_encode(hash('sha256', $key, true)), 0, 7), '/', '_')
);
// ensure the file does not exist before the store
@unlink($file);
$store->save($key);
$this->assertFileExists($file);
$store->delete($key);
}
}

View 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\MemcachedStore;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*
* @requires extension memcached
*/
class MemcachedStoreTest extends AbstractStoreTest
{
use ExpiringStoreTestTrait;
public static function setupBeforeClass()
{
$memcached = new \Memcached();
$memcached->addServer(getenv('MEMCACHED_HOST'), 11211);
$memcached->get('foo');
$code = $memcached->getResultCode();
if (\Memcached::RES_SUCCESS !== $code && \Memcached::RES_NOTFOUND !== $code) {
self::markTestSkipped('Unable to connect to the memcache host');
}
}
/**
* {@inheritdoc}
*/
protected function getClockDelay()
{
return 1000000;
}
/**
* {@inheritdoc}
*/
public function getStore()
{
$memcached = new \Memcached();
$memcached->addServer(getenv('MEMCACHED_HOST'), 11211);
return new MemcachedStore($memcached);
}
public function testAbortAfterExpiration()
{
$this->markTestSkipped('Memcached expects a TTL greater than 1 sec. Simulating a slow network is too hard');
}
}

View File

@@ -0,0 +1,36 @@
<?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;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class PredisStoreTest extends AbstractRedisStoreTest
{
public static function setupBeforeClass()
{
$redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
try {
$redis->connect();
} catch (\Exception $e) {
self::markTestSkipped($e->getMessage());
}
}
protected function getRedisConnection()
{
$redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
$redis->connect();
return $redis;
}
}

View File

@@ -0,0 +1,38 @@
<?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;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*
* @requires extension redis
*/
class RedisArrayStoreTest extends AbstractRedisStoreTest
{
public static function setupBeforeClass()
{
if (!class_exists('RedisArray')) {
self::markTestSkipped('The RedisArray class is required.');
}
if (!@((new \Redis())->connect(getenv('REDIS_HOST')))) {
$e = error_get_last();
self::markTestSkipped($e['message']);
}
}
protected function getRedisConnection()
{
$redis = new \RedisArray(array(getenv('REDIS_HOST')));
return $redis;
}
}

View File

@@ -0,0 +1,35 @@
<?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;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*
* @requires extension redis
*/
class RedisClusterStoreTest extends AbstractRedisStoreTest
{
public static function setupBeforeClass()
{
if (!class_exists('RedisCluster')) {
self::markTestSkipped('The RedisCluster class is required.');
}
if (!getenv('REDIS_CLUSTER_HOSTS')) {
self::markTestSkipped('REDIS_CLUSTER_HOSTS env var is not defined.');
}
}
protected function getRedisConnection()
{
return new \RedisCluster(null, explode(' ', getenv('REDIS_CLUSTER_HOSTS')));
}
}

View File

@@ -0,0 +1,36 @@
<?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;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*
* @requires extension redis
*/
class RedisStoreTest extends AbstractRedisStoreTest
{
public static function setupBeforeClass()
{
if (!@((new \Redis())->connect(getenv('REDIS_HOST')))) {
$e = error_get_last();
self::markTestSkipped($e['message']);
}
}
protected function getRedisConnection()
{
$redis = new \Redis();
$redis->connect(getenv('REDIS_HOST'));
return $redis;
}
}

View File

@@ -0,0 +1,35 @@
<?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\RedisStore;
use Symfony\Component\Lock\Store\RetryTillSaveStore;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class RetryTillSaveStoreTest extends AbstractStoreTest
{
use BlockingStoreTestTrait;
public function getStore()
{
$redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
try {
$redis->connect();
} catch (\Exception $e) {
self::markTestSkipped($e->getMessage());
}
return new RetryTillSaveStore(new RedisStore($redis), 100, 100);
}
}

View File

@@ -0,0 +1,73 @@
<?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\Key;
use Symfony\Component\Lock\Store\SemaphoreStore;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*
* @requires extension sysvsem
*/
class SemaphoreStoreTest extends AbstractStoreTest
{
use BlockingStoreTestTrait;
/**
* {@inheritdoc}
*/
protected function getStore()
{
if (\PHP_VERSION_ID < 50601) {
$this->markTestSkipped('Non blocking semaphore are supported by PHP version greater or equals than 5.6.1');
}
return new SemaphoreStore();
}
public function testResourceRemoval()
{
$initialCount = $this->getOpenedSemaphores();
$store = new SemaphoreStore();
$key = new Key(uniqid(__METHOD__, true));
$store->waitAndSave($key);
$this->assertGreaterThan($initialCount, $this->getOpenedSemaphores(), 'Semaphores should have been created');
$store->delete($key);
$this->assertEquals($initialCount, $this->getOpenedSemaphores(), 'All semaphores should be removed');
}
private function getOpenedSemaphores()
{
if ('Darwin' === PHP_OS) {
$lines = explode(PHP_EOL, trim(`ipcs -s`));
if (-1 === $start = array_search('Semaphores:', $lines)) {
throw new \Exception('Failed to extract list of opened semaphores. Expected a Semaphore list, got '.implode(PHP_EOL, $lines));
}
return \count(\array_slice($lines, ++$start));
}
$lines = explode(PHP_EOL, trim(`LC_ALL=C ipcs -su`));
if ('------ Semaphore Status --------' !== $lines[0]) {
throw new \Exception('Failed to extract list of opened semaphores. Expected a Semaphore status, got '.implode(PHP_EOL, $lines));
}
list($key, $value) = explode(' = ', $lines[1]);
if ('used arrays' !== $key) {
throw new \Exception('Failed to extract list of opened semaphores. Expected a "used arrays" key, got '.implode(PHP_EOL, $lines));
}
return (int) $value;
}
}

View File

@@ -0,0 +1,89 @@
<?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\Strategy;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Lock\Strategy\ConsensusStrategy;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class ConsensusStrategyTest extends TestCase
{
/** @var ConsensusStrategy */
private $strategy;
protected function setUp()
{
$this->strategy = new ConsensusStrategy();
}
public function provideMetResults()
{
// success, failure, total, isMet
yield array(3, 0, 3, true);
yield array(2, 1, 3, true);
yield array(2, 0, 3, true);
yield array(1, 2, 3, false);
yield array(1, 1, 3, false);
yield array(1, 0, 3, false);
yield array(0, 3, 3, false);
yield array(0, 2, 3, false);
yield array(0, 1, 3, false);
yield array(0, 0, 3, false);
yield array(2, 0, 2, true);
yield array(1, 1, 2, false);
yield array(1, 0, 2, false);
yield array(0, 2, 2, false);
yield array(0, 1, 2, false);
yield array(0, 0, 2, false);
}
public function provideIndeterminate()
{
// success, failure, total, canBeMet
yield array(3, 0, 3, true);
yield array(2, 1, 3, true);
yield array(2, 0, 3, true);
yield array(1, 2, 3, false);
yield array(1, 1, 3, true);
yield array(1, 0, 3, true);
yield array(0, 3, 3, false);
yield array(0, 2, 3, false);
yield array(0, 1, 3, true);
yield array(0, 0, 3, true);
yield array(2, 0, 2, true);
yield array(1, 1, 2, false);
yield array(1, 0, 2, true);
yield array(0, 2, 2, false);
yield array(0, 1, 2, false);
yield array(0, 0, 2, true);
}
/**
* @dataProvider provideMetResults
*/
public function testMet($success, $failure, $total, $isMet)
{
$this->assertSame($isMet, $this->strategy->isMet($success, $total));
}
/**
* @dataProvider provideIndeterminate
*/
public function testCanBeMet($success, $failure, $total, $isMet)
{
$this->assertSame($isMet, $this->strategy->canBeMet($failure, $total));
}
}

View File

@@ -0,0 +1,89 @@
<?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\Strategy;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Lock\Strategy\UnanimousStrategy;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class UnanimousStrategyTest extends TestCase
{
/** @var UnanimousStrategy */
private $strategy;
protected function setUp()
{
$this->strategy = new UnanimousStrategy();
}
public function provideMetResults()
{
// success, failure, total, isMet
yield array(3, 0, 3, true);
yield array(2, 1, 3, false);
yield array(2, 0, 3, false);
yield array(1, 2, 3, false);
yield array(1, 1, 3, false);
yield array(1, 0, 3, false);
yield array(0, 3, 3, false);
yield array(0, 2, 3, false);
yield array(0, 1, 3, false);
yield array(0, 0, 3, false);
yield array(2, 0, 2, true);
yield array(1, 1, 2, false);
yield array(1, 0, 2, false);
yield array(0, 2, 2, false);
yield array(0, 1, 2, false);
yield array(0, 0, 2, false);
}
public function provideIndeterminate()
{
// success, failure, total, canBeMet
yield array(3, 0, 3, true);
yield array(2, 1, 3, false);
yield array(2, 0, 3, true);
yield array(1, 2, 3, false);
yield array(1, 1, 3, false);
yield array(1, 0, 3, true);
yield array(0, 3, 3, false);
yield array(0, 2, 3, false);
yield array(0, 1, 3, false);
yield array(0, 0, 3, true);
yield array(2, 0, 2, true);
yield array(1, 1, 2, false);
yield array(1, 0, 2, true);
yield array(0, 2, 2, false);
yield array(0, 1, 2, false);
yield array(0, 0, 2, true);
}
/**
* @dataProvider provideMetResults
*/
public function testMet($success, $failure, $total, $isMet)
{
$this->assertSame($isMet, $this->strategy->isMet($success, $total));
}
/**
* @dataProvider provideIndeterminate
*/
public function testCanBeMet($success, $failure, $total, $isMet)
{
$this->assertSame($isMet, $this->strategy->canBeMet($failure, $total));
}
}

38
core/vendor/symfony/lock/composer.json vendored Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "symfony/lock",
"type": "library",
"description": "Symfony Lock Component",
"keywords": ["locking", "redlock", "mutex", "semaphore", "flock", "cas"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Jérémy Derussé",
"email": "jeremy@derusse.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": "^5.5.9|>=7.0.8",
"symfony/polyfill-php70": "~1.0",
"psr/log": "~1.0"
},
"require-dev": {
"predis/predis": "~1.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Lock\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
}
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
<env name="REDIS_HOST" value="localhost" />
<env name="MEMCACHED_HOST" value="localhost" />
</php>
<testsuites>
<testsuite name="Symfony Lock Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>