재 업데이트
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
/Tests export-ignore
|
||||
/phpunit.xml.dist export-ignore
|
||||
/.gitignore export-ignore
|
||||
@@ -1,3 +0,0 @@
|
||||
composer.lock
|
||||
phpunit.xml
|
||||
vendor/
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @author Hamza Amrouche <hamza.simperfit@gmail.com>
|
||||
*/
|
||||
interface BlockingStoreInterface extends PersistingStoreInterface
|
||||
{
|
||||
/**
|
||||
* Waits until a key becomes free, then stores the resource.
|
||||
*
|
||||
* @throws LockConflictedException
|
||||
*/
|
||||
public function waitAndSave(Key $key);
|
||||
}
|
||||
Vendored
+16
@@ -1,6 +1,22 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
4.4.0
|
||||
-----
|
||||
|
||||
* added InvalidTtlException
|
||||
* deprecated `StoreInterface` in favor of `BlockingStoreInterface` and `PersistingStoreInterface`
|
||||
* `Factory` is deprecated, use `LockFactory` instead
|
||||
* `StoreFactory::createStore` allows PDO and Zookeeper DSN.
|
||||
* deprecated services `lock.store.flock`, `lock.store.semaphore`, `lock.store.memcached.abstract` and `lock.store.redis.abstract`,
|
||||
use `StoreFactory::createStore` instead.
|
||||
|
||||
4.2.0
|
||||
-----
|
||||
|
||||
* added the PDO Store
|
||||
* added a new Zookeeper Data Store for Lock Component
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
||||
|
||||
+1
-1
@@ -16,6 +16,6 @@ namespace Symfony\Component\Lock\Exception;
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface ExceptionInterface
|
||||
interface ExceptionInterface extends \Throwable
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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 Amrouche Hamza <hamza.simperfit@gmail.com>
|
||||
*/
|
||||
class InvalidTtlException extends InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
Vendored
+6
-4
@@ -19,6 +19,8 @@ use Psr\Log\NullLogger;
|
||||
* Factory provides method to create locks.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*
|
||||
* @deprecated "Symfony\Component\Lock\Factory" is deprecated since Symfony 4.4 and will be removed in 5.0 use "Symfony\Component\Lock\LockFactory" instead
|
||||
*/
|
||||
class Factory implements LoggerAwareInterface
|
||||
{
|
||||
@@ -26,7 +28,7 @@ class Factory implements LoggerAwareInterface
|
||||
|
||||
private $store;
|
||||
|
||||
public function __construct(StoreInterface $store)
|
||||
public function __construct(PersistingStoreInterface $store)
|
||||
{
|
||||
$this->store = $store;
|
||||
|
||||
@@ -36,9 +38,9 @@ class Factory implements LoggerAwareInterface
|
||||
/**
|
||||
* Creates a lock for the given resource.
|
||||
*
|
||||
* @param string $resource The resource to lock
|
||||
* @param float $ttl Maximum expected lock duration in seconds
|
||||
* @param bool $autoRelease Whether to automatically release the lock or not when the lock instance is destroyed
|
||||
* @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
|
||||
*/
|
||||
|
||||
Vendored
+5
-8
@@ -20,14 +20,14 @@ final class Key
|
||||
{
|
||||
private $resource;
|
||||
private $expiringTime;
|
||||
private $state = array();
|
||||
private $state = [];
|
||||
|
||||
public function __construct(string $resource)
|
||||
{
|
||||
$this->resource = $resource;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->resource;
|
||||
}
|
||||
@@ -60,7 +60,7 @@ final class Key
|
||||
/**
|
||||
* @param float $ttl the expiration delay of locks in seconds
|
||||
*/
|
||||
public function reduceLifetime($ttl)
|
||||
public function reduceLifetime(float $ttl)
|
||||
{
|
||||
$newTime = microtime(true) + $ttl;
|
||||
|
||||
@@ -74,15 +74,12 @@ final class Key
|
||||
*
|
||||
* @return float|null Remaining lifetime in seconds. Null when the key won't expire.
|
||||
*/
|
||||
public function getRemainingLifetime()
|
||||
public function getRemainingLifetime(): ?float
|
||||
{
|
||||
return null === $this->expiringTime ? null : $this->expiringTime - microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isExpired()
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return null !== $this->expiringTime && $this->expiringTime <= microtime(true);
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2016-2018 Fabien Potencier
|
||||
Copyright (c) 2016-2020 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
|
||||
|
||||
Vendored
+45
-23
@@ -19,6 +19,7 @@ use Symfony\Component\Lock\Exception\LockAcquiringException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Exception\LockReleasingException;
|
||||
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
|
||||
/**
|
||||
* Lock is the default implementation of the LockInterface.
|
||||
@@ -36,12 +37,10 @@ final class Lock implements LockInterface, LoggerAwareInterface
|
||||
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
|
||||
* @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, float $ttl = null, bool $autoRelease = true)
|
||||
public function __construct(Key $key, PersistingStoreInterface $store, float $ttl = null, bool $autoRelease = true)
|
||||
{
|
||||
$this->store = $store;
|
||||
$this->key = $key;
|
||||
@@ -66,30 +65,38 @@ final class Lock implements LockInterface, LoggerAwareInterface
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function acquire($blocking = false)
|
||||
public function acquire($blocking = false): bool
|
||||
{
|
||||
try {
|
||||
if (!$blocking) {
|
||||
$this->store->save($this->key);
|
||||
} else {
|
||||
if ($blocking) {
|
||||
if (!$this->store instanceof StoreInterface && !$this->store instanceof BlockingStoreInterface) {
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this->store)));
|
||||
}
|
||||
$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));
|
||||
$this->logger->info('Successfully acquired the "{resource}" lock.', ['resource' => $this->key]);
|
||||
|
||||
if ($this->ttl) {
|
||||
$this->refresh();
|
||||
}
|
||||
|
||||
if ($this->key->isExpired()) {
|
||||
try {
|
||||
$this->release();
|
||||
} catch (\Exception $e) {
|
||||
// swallow exception to not hide the original issue
|
||||
}
|
||||
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));
|
||||
$this->logger->notice('Failed to acquire the "{resource}" lock. Someone else already acquired the lock.', ['resource' => $this->key]);
|
||||
|
||||
if ($blocking) {
|
||||
throw $e;
|
||||
@@ -97,7 +104,7 @@ final class Lock implements LockInterface, LoggerAwareInterface
|
||||
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->notice('Failed to acquire the "{resource}" lock.', array('resource' => $this->key, 'exception' => $e));
|
||||
$this->logger->notice('Failed to acquire the "{resource}" lock.', ['resource' => $this->key, 'exception' => $e]);
|
||||
throw new LockAcquiringException(sprintf('Failed to acquire the "%s" lock.', $this->key), 0, $e);
|
||||
}
|
||||
}
|
||||
@@ -120,16 +127,21 @@ final class Lock implements LockInterface, LoggerAwareInterface
|
||||
$this->dirty = true;
|
||||
|
||||
if ($this->key->isExpired()) {
|
||||
try {
|
||||
$this->release();
|
||||
} catch (\Exception $e) {
|
||||
// swallow exception to not hide the original issue
|
||||
}
|
||||
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' => $ttl));
|
||||
$this->logger->info('Expiration defined for "{resource}" lock for "{ttl}" seconds.', ['resource' => $this->key, 'ttl' => $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));
|
||||
$this->logger->notice('Failed to define an expiration for the "{resource}" lock, someone else acquired the lock.', ['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));
|
||||
$this->logger->notice('Failed to define an expiration for the "{resource}" lock.', ['resource' => $this->key, 'exception' => $e]);
|
||||
throw new LockAcquiringException(sprintf('Failed to define an expiration for the "%s" lock.', $this->key), 0, $e);
|
||||
}
|
||||
}
|
||||
@@ -137,7 +149,7 @@ final class Lock implements LockInterface, LoggerAwareInterface
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isAcquired()
|
||||
public function isAcquired(): bool
|
||||
{
|
||||
return $this->dirty = $this->store->exists($this->key);
|
||||
}
|
||||
@@ -147,19 +159,29 @@ final class Lock implements LockInterface, LoggerAwareInterface
|
||||
*/
|
||||
public function release()
|
||||
{
|
||||
$this->store->delete($this->key);
|
||||
$this->dirty = false;
|
||||
try {
|
||||
try {
|
||||
$this->store->delete($this->key);
|
||||
$this->dirty = false;
|
||||
} catch (LockReleasingException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
throw new LockReleasingException(sprintf('Failed to release the "%s" lock.', $this->key), 0, $e);
|
||||
}
|
||||
|
||||
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));
|
||||
if ($this->store->exists($this->key)) {
|
||||
throw new LockReleasingException(sprintf('Failed to release the "%s" lock, the resource is still locked.', $this->key));
|
||||
}
|
||||
} catch (LockReleasingException $e) {
|
||||
$this->logger->notice('Failed to release the "{resource}" lock.', ['resource' => $this->key]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isExpired()
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->key->isExpired();
|
||||
}
|
||||
@@ -167,7 +189,7 @@ final class Lock implements LockInterface, LoggerAwareInterface
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRemainingLifetime()
|
||||
public function getRemainingLifetime(): ?float
|
||||
{
|
||||
return $this->key->getRemainingLifetime();
|
||||
}
|
||||
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Factory provides method to create locks.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
* @author Hamza Amrouche <hamza.simperfit@gmail.com>
|
||||
*/
|
||||
class LockFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function createLock($resource, $ttl = 300.0, $autoRelease = true): LockInterface
|
||||
{
|
||||
return parent::createLock($resource, $ttl, $autoRelease);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface PersistingStoreInterface
|
||||
{
|
||||
/**
|
||||
* Stores the resource if it's not locked by someone else.
|
||||
*
|
||||
* @throws LockAcquiringException
|
||||
* @throws LockConflictedException
|
||||
*/
|
||||
public function save(Key $key);
|
||||
|
||||
/**
|
||||
* Removes a resource from the storage.
|
||||
*
|
||||
* @throws LockReleasingException
|
||||
*/
|
||||
public function delete(Key $key);
|
||||
|
||||
/**
|
||||
* Returns whether or not the resource exists in the storage.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(Key $key);
|
||||
|
||||
/**
|
||||
* Extends the TTL of a resource.
|
||||
*
|
||||
* @param float $ttl amount of seconds to keep the lock in the store
|
||||
*
|
||||
* @throws LockConflictedException
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl);
|
||||
}
|
||||
+27
-23
@@ -16,37 +16,37 @@ use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\Strategy\StrategyInterface;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
use Symfony\Component\Lock\Strategy\StrategyInterface;
|
||||
|
||||
/**
|
||||
* CombinedStore is a StoreInterface implementation able to manage and synchronize several StoreInterfaces.
|
||||
* CombinedStore is a PersistingStoreInterface implementation able to manage and synchronize several StoreInterfaces.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
/** @var StoreInterface[] */
|
||||
/** @var PersistingStoreInterface[] */
|
||||
private $stores;
|
||||
/** @var StrategyInterface */
|
||||
private $strategy;
|
||||
|
||||
/**
|
||||
* @param StoreInterface[] $stores The list of synchronized stores
|
||||
* @param StrategyInterface $strategy
|
||||
* @param PersistingStoreInterface[] $stores The list of synchronized stores
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $stores, StrategyInterface $strategy)
|
||||
{
|
||||
foreach ($stores as $store) {
|
||||
if (!$store instanceof StoreInterface) {
|
||||
throw new InvalidArgumentException(sprintf('The store must implement "%s". Got "%s".', StoreInterface::class, get_class($store)));
|
||||
if (!$store instanceof PersistingStoreInterface) {
|
||||
throw new InvalidArgumentException(sprintf('The store must implement "%s". Got "%s".', PersistingStoreInterface::class, \get_class($store)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,14 +62,14 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$storesCount = count($this->stores);
|
||||
$storesCount = \count($this->stores);
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
try {
|
||||
$store->save($key);
|
||||
++$successCount;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('One store failed to save the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
|
||||
$this->logger->warning('One store failed to save the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
@@ -78,11 +78,13 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->warning('Failed to store the "{resource}" lock. Quorum has not been met.', array('resource' => $key, 'success' => $successCount, 'failure' => $failureCount));
|
||||
$this->logger->warning('Failed to store the "{resource}" lock. Quorum has not been met.', ['resource' => $key, 'success' => $successCount, 'failure' => $failureCount]);
|
||||
|
||||
// clean up potential locks
|
||||
$this->delete($key);
|
||||
@@ -90,9 +92,15 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @deprecated since Symfony 4.4.
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,13 +110,13 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$storesCount = count($this->stores);
|
||||
$storesCount = \count($this->stores);
|
||||
$expireAt = microtime(true) + $ttl;
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
try {
|
||||
if (0.0 >= $adjustedTtl = $expireAt - microtime(true)) {
|
||||
$this->logger->warning('Stores took to long to put off the expiration of the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'ttl' => $ttl));
|
||||
$this->logger->warning('Stores took to long to put off the expiration of the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'ttl' => $ttl]);
|
||||
$key->reduceLifetime(0);
|
||||
break;
|
||||
}
|
||||
@@ -116,7 +124,7 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
$store->putOffExpiration($key, $adjustedTtl);
|
||||
++$successCount;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('One store failed to put off the expiration of the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
|
||||
$this->logger->warning('One store failed to put off the expiration of the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
@@ -125,15 +133,13 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
}
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
|
||||
}
|
||||
$this->checkNotExpired($key);
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->warning('Failed to define the expiration for the "{resource}" lock. Quorum has not been met.', array('resource' => $key, 'success' => $successCount, 'failure' => $failureCount));
|
||||
$this->logger->warning('Failed to define the expiration for the "{resource}" lock. Quorum has not been met.', ['resource' => $key, 'success' => $successCount, 'failure' => $failureCount]);
|
||||
|
||||
// clean up potential locks
|
||||
$this->delete($key);
|
||||
@@ -150,9 +156,7 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
try {
|
||||
$store->delete($key);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->notice('One store failed to delete the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->notice('One store failed to delete the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
|
||||
$this->logger->notice('One store failed to delete the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +168,7 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$storesCount = count($this->stores);
|
||||
$storesCount = \count($this->stores);
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
if ($store->exists($key)) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
|
||||
trait ExpiringStoreTrait
|
||||
{
|
||||
private function checkNotExpired(Key $key)
|
||||
{
|
||||
if ($key->isExpired()) {
|
||||
try {
|
||||
$this->delete($key);
|
||||
} catch (\Exception $e) {
|
||||
// swallow exception to not hide the original issue
|
||||
}
|
||||
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Symfony\Component\Lock\BlockingStoreInterface;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockStorageException;
|
||||
@@ -18,7 +19,7 @@ use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* FlockStore is a StoreInterface implementation using the FileSystem flock.
|
||||
* FlockStore is a PersistingStoreInterface implementation using the FileSystem flock.
|
||||
*
|
||||
* Original implementation in \Symfony\Component\Filesystem\LockHandler.
|
||||
*
|
||||
@@ -27,7 +28,7 @@ use Symfony\Component\Lock\StoreInterface;
|
||||
* @author Romain Neutron <imprec@gmail.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class FlockStore implements StoreInterface
|
||||
class FlockStore implements StoreInterface, BlockingStoreInterface
|
||||
{
|
||||
private $lockPath;
|
||||
|
||||
@@ -64,7 +65,7 @@ class FlockStore implements StoreInterface
|
||||
$this->lock($key, true);
|
||||
}
|
||||
|
||||
private function lock(Key $key, $blocking)
|
||||
private function lock(Key $key, bool $blocking)
|
||||
{
|
||||
// The lock is maybe already acquired.
|
||||
if ($key->hasState(__CLASS__)) {
|
||||
@@ -81,7 +82,7 @@ class FlockStore implements StoreInterface
|
||||
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
|
||||
if (!$handle = fopen($fileName, 'r+') ?: fopen($fileName, 'r')) {
|
||||
if ($handle = fopen($fileName, 'x')) {
|
||||
chmod($fileName, 0444);
|
||||
chmod($fileName, 0666);
|
||||
} elseif (!$handle = fopen($fileName, 'r+') ?: fopen($fileName, 'r')) {
|
||||
usleep(100); // Give some time for chmod() to complete
|
||||
$handle = fopen($fileName, 'r+') ?: fopen($fileName, 'r');
|
||||
|
||||
+26
-25
@@ -12,18 +12,21 @@
|
||||
namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\InvalidTtlException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* MemcachedStore is a StoreInterface implementation using Memcached as store engine.
|
||||
* MemcachedStore is a PersistingStoreInterface implementation using Memcached as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class MemcachedStore implements StoreInterface
|
||||
{
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private $memcached;
|
||||
private $initialTtl;
|
||||
/** @var bool */
|
||||
@@ -31,12 +34,11 @@ class MemcachedStore implements StoreInterface
|
||||
|
||||
public static function isSupported()
|
||||
{
|
||||
return extension_loaded('memcached');
|
||||
return \extension_loaded('memcached');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Memcached $memcached
|
||||
* @param int $initialTtl the expiration delay of locks in seconds
|
||||
* @param int $initialTtl the expiration delay of locks in seconds
|
||||
*/
|
||||
public function __construct(\Memcached $memcached, int $initialTtl = 300)
|
||||
{
|
||||
@@ -57,21 +59,25 @@ class MemcachedStore implements StoreInterface
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$token = $this->getToken($key);
|
||||
$token = $this->getUniqueToken($key);
|
||||
$key->reduceLifetime($this->initialTtl);
|
||||
if (!$this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) {
|
||||
// the lock is already acquired. It could be us. Let's try to put off.
|
||||
$this->putOffExpiration($key, $this->initialTtl);
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
|
||||
}
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @deprecated since Symfony 4.4.
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,13 +86,13 @@ class MemcachedStore implements StoreInterface
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
if ($ttl < 1) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects a TTL greater or equals to 1. Got %s.', __METHOD__, $ttl));
|
||||
throw new InvalidTtlException(sprintf('%s() expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
|
||||
}
|
||||
|
||||
// Interface defines a float value but Store required an integer.
|
||||
$ttl = (int) ceil($ttl);
|
||||
|
||||
$token = $this->getToken($key);
|
||||
$token = $this->getUniqueToken($key);
|
||||
|
||||
list($value, $cas) = $this->getValueAndCas($key);
|
||||
|
||||
@@ -110,9 +116,7 @@ class MemcachedStore implements StoreInterface
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
|
||||
}
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,7 +124,7 @@ class MemcachedStore implements StoreInterface
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$token = $this->getToken($key);
|
||||
$token = $this->getUniqueToken($key);
|
||||
|
||||
list($value, $cas) = $this->getValueAndCas($key);
|
||||
|
||||
@@ -144,13 +148,10 @@ class MemcachedStore implements StoreInterface
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $this->memcached->get((string) $key) === $this->getToken($key);
|
||||
return $this->memcached->get((string) $key) === $this->getUniqueToken($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an unique token for the given key.
|
||||
*/
|
||||
private function getToken(Key $key): string
|
||||
private function getUniqueToken(Key $key): string
|
||||
{
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
$token = base64_encode(random_bytes(32));
|
||||
@@ -160,7 +161,7 @@ class MemcachedStore implements StoreInterface
|
||||
return $key->getState(__CLASS__);
|
||||
}
|
||||
|
||||
private function getValueAndCas(Key $key)
|
||||
private function getValueAndCas(Key $key): array
|
||||
{
|
||||
if (null === $this->useExtendedReturn) {
|
||||
$this->useExtendedReturn = version_compare(phpversion('memcached'), '2.9.9', '>');
|
||||
@@ -169,15 +170,15 @@ class MemcachedStore implements StoreInterface
|
||||
if ($this->useExtendedReturn) {
|
||||
$extendedReturn = $this->memcached->get((string) $key, null, \Memcached::GET_EXTENDED);
|
||||
if (\Memcached::GET_ERROR_RETURN_VALUE === $extendedReturn) {
|
||||
return array($extendedReturn, 0.0);
|
||||
return [$extendedReturn, 0.0];
|
||||
}
|
||||
|
||||
return array($extendedReturn['value'], $extendedReturn['cas']);
|
||||
return [$extendedReturn['value'], $extendedReturn['cas']];
|
||||
}
|
||||
|
||||
$cas = 0.0;
|
||||
$value = $this->memcached->get((string) $key, null, $cas);
|
||||
|
||||
return array($value, $cas);
|
||||
return [$value, $cas];
|
||||
}
|
||||
}
|
||||
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\InvalidTtlException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* PdoStore is a PersistingStoreInterface implementation using a PDO connection.
|
||||
*
|
||||
* Lock metadata are stored in a table. You can use createTable() to initialize
|
||||
* a correctly defined table.
|
||||
|
||||
* CAUTION: This store relies on all client and server nodes to have
|
||||
* synchronized clocks for lock expiry to occur at the correct time.
|
||||
* To ensure locks don't expire prematurely; the TTLs should be set with enough
|
||||
* extra time to account for any clock drift between nodes.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class PdoStore implements StoreInterface
|
||||
{
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private $conn;
|
||||
private $dsn;
|
||||
private $driver;
|
||||
private $table = 'lock_keys';
|
||||
private $idCol = 'key_id';
|
||||
private $tokenCol = 'key_token';
|
||||
private $expirationCol = 'key_expiration';
|
||||
private $username = '';
|
||||
private $password = '';
|
||||
private $connectionOptions = [];
|
||||
private $gcProbability;
|
||||
private $initialTtl;
|
||||
|
||||
/**
|
||||
* You can either pass an existing database connection as PDO instance or
|
||||
* a Doctrine DBAL Connection or a DSN string that will be used to
|
||||
* lazy-connect to the database when the lock is actually used.
|
||||
*
|
||||
* List of available options:
|
||||
* * db_table: The name of the table [default: lock_keys]
|
||||
* * db_id_col: The column where to store the lock key [default: key_id]
|
||||
* * db_token_col: The column where to store the lock token [default: key_token]
|
||||
* * db_expiration_col: The column where to store the expiration [default: key_expiration]
|
||||
* * db_username: The username when lazy-connect [default: '']
|
||||
* * db_password: The password when lazy-connect [default: '']
|
||||
* * db_connection_options: An array of driver-specific connection options [default: []]
|
||||
*
|
||||
* @param \PDO|Connection|string $connOrDsn A \PDO or Connection instance or DSN string or null
|
||||
* @param array $options An associative array of options
|
||||
* @param float $gcProbability Probability expressed as floating number between 0 and 1 to clean old locks
|
||||
* @param int $initialTtl The expiration delay of locks in seconds
|
||||
*
|
||||
* @throws InvalidArgumentException When first argument is not PDO nor Connection nor string
|
||||
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
|
||||
* @throws InvalidArgumentException When namespace contains invalid characters
|
||||
* @throws InvalidArgumentException When the initial ttl is not valid
|
||||
*/
|
||||
public function __construct($connOrDsn, array $options = [], float $gcProbability = 0.01, int $initialTtl = 300)
|
||||
{
|
||||
if ($gcProbability < 0 || $gcProbability > 1) {
|
||||
throw new InvalidArgumentException(sprintf('"%s" requires gcProbability between 0 and 1, "%f" given.', __METHOD__, $gcProbability));
|
||||
}
|
||||
if ($initialTtl < 1) {
|
||||
throw new InvalidTtlException(sprintf('%s() expects a strictly positive TTL, "%d" given.', __METHOD__, $initialTtl));
|
||||
}
|
||||
|
||||
if ($connOrDsn instanceof \PDO) {
|
||||
if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
|
||||
throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __METHOD__));
|
||||
}
|
||||
|
||||
$this->conn = $connOrDsn;
|
||||
} elseif ($connOrDsn instanceof Connection) {
|
||||
$this->conn = $connOrDsn;
|
||||
} elseif (\is_string($connOrDsn)) {
|
||||
$this->dsn = $connOrDsn;
|
||||
} else {
|
||||
throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, \is_object($connOrDsn) ? \get_class($connOrDsn) : \gettype($connOrDsn)));
|
||||
}
|
||||
|
||||
$this->table = $options['db_table'] ?? $this->table;
|
||||
$this->idCol = $options['db_id_col'] ?? $this->idCol;
|
||||
$this->tokenCol = $options['db_token_col'] ?? $this->tokenCol;
|
||||
$this->expirationCol = $options['db_expiration_col'] ?? $this->expirationCol;
|
||||
$this->username = $options['db_username'] ?? $this->username;
|
||||
$this->password = $options['db_password'] ?? $this->password;
|
||||
$this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
|
||||
|
||||
$this->gcProbability = $gcProbability;
|
||||
$this->initialTtl = $initialTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$key->reduceLifetime($this->initialTtl);
|
||||
|
||||
$sql = "INSERT INTO $this->table ($this->idCol, $this->tokenCol, $this->expirationCol) VALUES (:id, :token, {$this->getCurrentTimestampStatement()} + $this->initialTtl)";
|
||||
$stmt = $this->getConnection()->prepare($sql);
|
||||
|
||||
$stmt->bindValue(':id', $this->getHashedKey($key));
|
||||
$stmt->bindValue(':token', $this->getUniqueToken($key));
|
||||
|
||||
try {
|
||||
$stmt->execute();
|
||||
} catch (DBALException $e) {
|
||||
// the lock is already acquired. It could be us. Let's try to put off.
|
||||
$this->putOffExpiration($key, $this->initialTtl);
|
||||
} catch (\PDOException $e) {
|
||||
// the lock is already acquired. It could be us. Let's try to put off.
|
||||
$this->putOffExpiration($key, $this->initialTtl);
|
||||
}
|
||||
|
||||
if ($this->gcProbability > 0 && (1.0 === $this->gcProbability || (random_int(0, PHP_INT_MAX) / PHP_INT_MAX) <= $this->gcProbability)) {
|
||||
$this->prune();
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not supports blocking locks.', __METHOD__));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
if ($ttl < 1) {
|
||||
throw new InvalidTtlException(sprintf('%s() expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
|
||||
}
|
||||
|
||||
$key->reduceLifetime($ttl);
|
||||
|
||||
$sql = "UPDATE $this->table SET $this->expirationCol = {$this->getCurrentTimestampStatement()} + $ttl, $this->tokenCol = :token1 WHERE $this->idCol = :id AND ($this->tokenCol = :token2 OR $this->expirationCol <= {$this->getCurrentTimestampStatement()})";
|
||||
$stmt = $this->getConnection()->prepare($sql);
|
||||
|
||||
$uniqueToken = $this->getUniqueToken($key);
|
||||
$stmt->bindValue(':id', $this->getHashedKey($key));
|
||||
$stmt->bindValue(':token1', $uniqueToken);
|
||||
$stmt->bindValue(':token2', $uniqueToken);
|
||||
$stmt->execute();
|
||||
|
||||
// If this method is called twice in the same second, the row wouldn't be updated. We have to call exists to know if we are the owner
|
||||
if (!$stmt->rowCount() && !$this->exists($key)) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$sql = "DELETE FROM $this->table WHERE $this->idCol = :id AND $this->tokenCol = :token";
|
||||
$stmt = $this->getConnection()->prepare($sql);
|
||||
|
||||
$stmt->bindValue(':id', $this->getHashedKey($key));
|
||||
$stmt->bindValue(':token', $this->getUniqueToken($key));
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
$sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND $this->tokenCol = :token AND $this->expirationCol > {$this->getCurrentTimestampStatement()}";
|
||||
$stmt = $this->getConnection()->prepare($sql);
|
||||
|
||||
$stmt->bindValue(':id', $this->getHashedKey($key));
|
||||
$stmt->bindValue(':token', $this->getUniqueToken($key));
|
||||
$stmt->execute();
|
||||
|
||||
return (bool) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hashed version of the key.
|
||||
*/
|
||||
private function getHashedKey(Key $key): string
|
||||
{
|
||||
return hash('sha256', (string) $key);
|
||||
}
|
||||
|
||||
private function getUniqueToken(Key $key): string
|
||||
{
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
$token = base64_encode(random_bytes(32));
|
||||
$key->setState(__CLASS__, $token);
|
||||
}
|
||||
|
||||
return $key->getState(__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \PDO|Connection
|
||||
*/
|
||||
private function getConnection()
|
||||
{
|
||||
if (null === $this->conn) {
|
||||
if (strpos($this->dsn, '://')) {
|
||||
if (!class_exists(DriverManager::class)) {
|
||||
throw new InvalidArgumentException(sprintf('Failed to parse the DSN "%s". Try running "composer require doctrine/dbal".', $this->dsn));
|
||||
}
|
||||
$this->conn = DriverManager::getConnection(['url' => $this->dsn]);
|
||||
} else {
|
||||
$this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
|
||||
$this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the table to store lock keys which can be called once for setup.
|
||||
*
|
||||
* @throws \PDOException When the table already exists
|
||||
* @throws DBALException When the table already exists
|
||||
* @throws \DomainException When an unsupported PDO driver is used
|
||||
*/
|
||||
public function createTable(): void
|
||||
{
|
||||
// connect if we are not yet
|
||||
$conn = $this->getConnection();
|
||||
$driver = $this->getDriver();
|
||||
|
||||
if ($conn instanceof Connection) {
|
||||
$schema = new Schema();
|
||||
$table = $schema->createTable($this->table);
|
||||
$table->addColumn($this->idCol, 'string', ['length' => 64]);
|
||||
$table->addColumn($this->tokenCol, 'string', ['length' => 44]);
|
||||
$table->addColumn($this->expirationCol, 'integer', ['unsigned' => true]);
|
||||
$table->setPrimaryKey([$this->idCol]);
|
||||
|
||||
foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) {
|
||||
$conn->exec($sql);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($driver) {
|
||||
case 'mysql':
|
||||
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(44) NOT NULL, $this->expirationCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB";
|
||||
break;
|
||||
case 'sqlite':
|
||||
$sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->tokenCol TEXT NOT NULL, $this->expirationCol INTEGER)";
|
||||
break;
|
||||
case 'pgsql':
|
||||
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(64) NOT NULL, $this->expirationCol INTEGER)";
|
||||
break;
|
||||
case 'oci':
|
||||
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR2(64) NOT NULL, $this->expirationCol INTEGER)";
|
||||
break;
|
||||
case 'sqlsrv':
|
||||
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(64) NOT NULL, $this->expirationCol INTEGER)";
|
||||
break;
|
||||
default:
|
||||
throw new \DomainException(sprintf('Creating the lock table is currently not implemented for PDO driver "%s".', $driver));
|
||||
}
|
||||
|
||||
$conn->exec($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the table by removing all expired locks.
|
||||
*/
|
||||
private function prune(): void
|
||||
{
|
||||
$sql = "DELETE FROM $this->table WHERE $this->expirationCol <= {$this->getCurrentTimestampStatement()}";
|
||||
|
||||
$this->getConnection()->exec($sql);
|
||||
}
|
||||
|
||||
private function getDriver(): string
|
||||
{
|
||||
if (null !== $this->driver) {
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
$con = $this->getConnection();
|
||||
if ($con instanceof \PDO) {
|
||||
$this->driver = $con->getAttribute(\PDO::ATTR_DRIVER_NAME);
|
||||
} else {
|
||||
switch ($this->driver = $con->getDriver()->getName()) {
|
||||
case 'mysqli':
|
||||
case 'pdo_mysql':
|
||||
case 'drizzle_pdo_mysql':
|
||||
$this->driver = 'mysql';
|
||||
break;
|
||||
case 'pdo_sqlite':
|
||||
$this->driver = 'sqlite';
|
||||
break;
|
||||
case 'pdo_pgsql':
|
||||
$this->driver = 'pgsql';
|
||||
break;
|
||||
case 'oci8':
|
||||
case 'pdo_oracle':
|
||||
$this->driver = 'oci';
|
||||
break;
|
||||
case 'pdo_sqlsrv':
|
||||
$this->driver = 'sqlsrv';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a SQL function to get the current timestamp regarding the current connection's driver.
|
||||
*/
|
||||
private function getCurrentTimestampStatement(): string
|
||||
{
|
||||
switch ($this->getDriver()) {
|
||||
case 'mysql':
|
||||
return 'UNIX_TIMESTAMP()';
|
||||
case 'sqlite':
|
||||
return 'strftime(\'%s\',\'now\')';
|
||||
case 'pgsql':
|
||||
return 'CAST(EXTRACT(epoch FROM NOW()) AS INT)';
|
||||
case 'oci':
|
||||
return '(SYSDATE - TO_DATE(\'19700101\',\'yyyymmdd\'))*86400 - TO_NUMBER(SUBSTR(TZ_OFFSET(sessiontimezone), 1, 3))*3600';
|
||||
case 'sqlsrv':
|
||||
return 'DATEDIFF(s, \'1970-01-01\', GETUTCDATE())';
|
||||
default:
|
||||
return time();
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
-29
@@ -11,35 +11,39 @@
|
||||
|
||||
namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Symfony\Component\Cache\Traits\RedisClusterProxy;
|
||||
use Symfony\Component\Cache\Traits\RedisProxy;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\InvalidTtlException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* RedisStore is a StoreInterface implementation using Redis as store engine.
|
||||
* RedisStore is a PersistingStoreInterface implementation using Redis as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class RedisStore implements StoreInterface
|
||||
{
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private $redis;
|
||||
private $initialTtl;
|
||||
|
||||
/**
|
||||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient
|
||||
* @param float $initialTtl the expiration delay of locks in seconds
|
||||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface $redisClient
|
||||
* @param float $initialTtl the expiration delay of locks in seconds
|
||||
*/
|
||||
public function __construct($redisClient, float $initialTtl = 300.0)
|
||||
{
|
||||
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client && !$redisClient instanceof RedisProxy) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redisClient) ? get_class($redisClient) : gettype($redisClient)));
|
||||
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\ClientInterface && !$redisClient instanceof RedisProxy) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, %s given', __METHOD__, \is_object($redisClient) ? \get_class($redisClient) : \gettype($redisClient)));
|
||||
}
|
||||
|
||||
if ($initialTtl <= 0) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
throw new InvalidTtlException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
}
|
||||
|
||||
$this->redis = $redisClient;
|
||||
@@ -54,24 +58,30 @@ class RedisStore implements StoreInterface
|
||||
$script = '
|
||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
|
||||
elseif redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
|
||||
return 1
|
||||
else
|
||||
return redis.call("set", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
|
||||
return 0
|
||||
end
|
||||
';
|
||||
|
||||
$key->reduceLifetime($this->initialTtl);
|
||||
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($this->initialTtl * 1000)))) {
|
||||
if (!$this->evaluate($script, (string) $key, [$this->getUniqueToken($key), (int) ceil($this->initialTtl * 1000)])) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
|
||||
}
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @deprecated since Symfony 4.4.
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,13 +98,11 @@ class RedisStore implements StoreInterface
|
||||
';
|
||||
|
||||
$key->reduceLifetime($ttl);
|
||||
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($ttl * 1000)))) {
|
||||
if (!$this->evaluate($script, (string) $key, [$this->getUniqueToken($key), (int) ceil($ttl * 1000)])) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
if ($key->isExpired()) {
|
||||
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
|
||||
}
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,7 +118,7 @@ class RedisStore implements StoreInterface
|
||||
end
|
||||
';
|
||||
|
||||
$this->evaluate($script, (string) $key, array($this->getToken($key)));
|
||||
$this->evaluate($script, (string) $key, [$this->getUniqueToken($key)]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +126,7 @@ class RedisStore implements StoreInterface
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $this->redis->get((string) $key) === $this->getToken($key);
|
||||
return $this->redis->get((string) $key) === $this->getUniqueToken($key);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,25 +136,27 @@ class RedisStore implements StoreInterface
|
||||
*/
|
||||
private function evaluate(string $script, string $resource, array $args)
|
||||
{
|
||||
if ($this->redis instanceof \Redis || $this->redis instanceof \RedisCluster || $this->redis instanceof RedisProxy) {
|
||||
return $this->redis->eval($script, array_merge(array($resource), $args), 1);
|
||||
if (
|
||||
$this->redis instanceof \Redis ||
|
||||
$this->redis instanceof \RedisCluster ||
|
||||
$this->redis instanceof RedisProxy ||
|
||||
$this->redis instanceof RedisClusterProxy
|
||||
) {
|
||||
return $this->redis->eval($script, array_merge([$resource], $args), 1);
|
||||
}
|
||||
|
||||
if ($this->redis instanceof \RedisArray) {
|
||||
return $this->redis->_instance($this->redis->_target($resource))->eval($script, array_merge(array($resource), $args), 1);
|
||||
return $this->redis->_instance($this->redis->_target($resource))->eval($script, array_merge([$resource], $args), 1);
|
||||
}
|
||||
|
||||
if ($this->redis instanceof \Predis\Client) {
|
||||
return call_user_func_array(array($this->redis, 'eval'), array_merge(array($script, 1, $resource), $args));
|
||||
if ($this->redis instanceof \Predis\ClientInterface) {
|
||||
return $this->redis->eval(...array_merge([$script, 1, $resource], $args));
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('%s() expects been initialized with a Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($this->redis) ? get_class($this->redis) : gettype($this->redis)));
|
||||
throw new InvalidArgumentException(sprintf('%s() expects being initialized with a Redis, RedisArray, RedisCluster or Predis\ClientInterface, %s given', __METHOD__, \is_object($this->redis) ? \get_class($this->redis) : \gettype($this->redis)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an unique token for the given key.
|
||||
*/
|
||||
private function getToken(Key $key): string
|
||||
private function getUniqueToken(Key $key): string
|
||||
{
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
$token = base64_encode(random_bytes(32));
|
||||
|
||||
+8
-7
@@ -14,17 +14,19 @@ namespace Symfony\Component\Lock\Store;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Lock\BlockingStoreInterface;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* RetryTillSaveStore is a StoreInterface implementation which decorate a non blocking StoreInterface to provide a
|
||||
* RetryTillSaveStore is a PersistingStoreInterface implementation which decorate a non blocking PersistingStoreInterface to provide a
|
||||
* blocking storage.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class RetryTillSaveStore implements StoreInterface, LoggerAwareInterface
|
||||
class RetryTillSaveStore implements BlockingStoreInterface, StoreInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
@@ -33,11 +35,10 @@ class RetryTillSaveStore implements StoreInterface, LoggerAwareInterface
|
||||
private $retryCount;
|
||||
|
||||
/**
|
||||
* @param StoreInterface $decorated The decorated StoreInterface
|
||||
* @param int $retrySleep Duration in ms between 2 retry
|
||||
* @param int $retryCount Maximum amount of retry
|
||||
* @param int $retrySleep Duration in ms between 2 retry
|
||||
* @param int $retryCount Maximum amount of retry
|
||||
*/
|
||||
public function __construct(StoreInterface $decorated, int $retrySleep = 100, int $retryCount = PHP_INT_MAX)
|
||||
public function __construct(PersistingStoreInterface $decorated, int $retrySleep = 100, int $retryCount = PHP_INT_MAX)
|
||||
{
|
||||
$this->decorated = $decorated;
|
||||
$this->retrySleep = $retrySleep;
|
||||
@@ -71,7 +72,7 @@ class RetryTillSaveStore implements StoreInterface, LoggerAwareInterface
|
||||
}
|
||||
} while (++$retry < $this->retryCount);
|
||||
|
||||
$this->logger->warning('Failed to store the "{resource}" lock. Abort after {retry} retry.', array('resource' => $key, 'retry' => $retry));
|
||||
$this->logger->warning('Failed to store the "{resource}" lock. Abort after {retry} retry.', ['resource' => $key, 'retry' => $retry]);
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
+6
-7
@@ -11,28 +11,27 @@
|
||||
|
||||
namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Symfony\Component\Lock\BlockingStoreInterface;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* SemaphoreStore is a StoreInterface implementation using Semaphore as store engine.
|
||||
* SemaphoreStore is a PersistingStoreInterface implementation using Semaphore as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class SemaphoreStore implements StoreInterface
|
||||
class SemaphoreStore implements StoreInterface, BlockingStoreInterface
|
||||
{
|
||||
/**
|
||||
* Returns whether or not the store is supported.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function isSupported()
|
||||
public static function isSupported(): bool
|
||||
{
|
||||
return extension_loaded('sysvsem');
|
||||
return \extension_loaded('sysvsem');
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
@@ -58,7 +57,7 @@ class SemaphoreStore implements StoreInterface
|
||||
$this->lock($key, true);
|
||||
}
|
||||
|
||||
private function lock(Key $key, $blocking)
|
||||
private function lock(Key $key, bool $blocking)
|
||||
{
|
||||
if ($key->hasState(__CLASS__)) {
|
||||
return;
|
||||
|
||||
+68
-8
@@ -11,8 +11,12 @@
|
||||
|
||||
namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Component\Cache\Adapter\AbstractAdapter;
|
||||
use Symfony\Component\Cache\Traits\RedisClusterProxy;
|
||||
use Symfony\Component\Cache\Traits\RedisProxy;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
|
||||
/**
|
||||
* StoreFactory create stores and connections.
|
||||
@@ -22,19 +26,75 @@ use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
class StoreFactory
|
||||
{
|
||||
/**
|
||||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client|\Memcached $connection
|
||||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy|\Memcached|\PDO|Connection|\Zookeeper|string $connection Connection or DSN or Store short name
|
||||
*
|
||||
* @return RedisStore|MemcachedStore
|
||||
* @return PersistingStoreInterface
|
||||
*/
|
||||
public static function createStore($connection)
|
||||
{
|
||||
if ($connection instanceof \Redis || $connection instanceof \RedisArray || $connection instanceof \RedisCluster || $connection instanceof \Predis\Client || $connection instanceof RedisProxy) {
|
||||
return new RedisStore($connection);
|
||||
}
|
||||
if ($connection instanceof \Memcached) {
|
||||
return new MemcachedStore($connection);
|
||||
if (!\is_string($connection) && !\is_object($connection)) {
|
||||
throw new \TypeError(sprintf('Argument 1 passed to %s() must be a string or a connection object, %s given.', __METHOD__, \gettype($connection)));
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', get_class($connection)));
|
||||
switch (true) {
|
||||
case $connection instanceof \Redis:
|
||||
case $connection instanceof \RedisArray:
|
||||
case $connection instanceof \RedisCluster:
|
||||
case $connection instanceof \Predis\ClientInterface:
|
||||
case $connection instanceof RedisProxy:
|
||||
case $connection instanceof RedisClusterProxy:
|
||||
return new RedisStore($connection);
|
||||
|
||||
case $connection instanceof \Memcached:
|
||||
return new MemcachedStore($connection);
|
||||
|
||||
case $connection instanceof \PDO:
|
||||
case $connection instanceof Connection:
|
||||
return new PdoStore($connection);
|
||||
|
||||
case $connection instanceof \Zookeeper:
|
||||
return new ZookeeperStore($connection);
|
||||
|
||||
case !\is_string($connection):
|
||||
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', \get_class($connection)));
|
||||
case 'flock' === $connection:
|
||||
return new FlockStore();
|
||||
|
||||
case 0 === strpos($connection, 'flock://'):
|
||||
return new FlockStore(substr($connection, 8));
|
||||
|
||||
case 'semaphore' === $connection:
|
||||
return new SemaphoreStore();
|
||||
|
||||
case 0 === strpos($connection, 'redis://'):
|
||||
case 0 === strpos($connection, 'rediss://'):
|
||||
case 0 === strpos($connection, 'memcached://'):
|
||||
if (!class_exists(AbstractAdapter::class)) {
|
||||
throw new InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection));
|
||||
}
|
||||
$storeClass = 0 === strpos($connection, 'memcached://') ? MemcachedStore::class : RedisStore::class;
|
||||
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
|
||||
|
||||
return new $storeClass($connection);
|
||||
|
||||
case 0 === strpos($connection, 'mssql://'):
|
||||
case 0 === strpos($connection, 'mysql:'):
|
||||
case 0 === strpos($connection, 'mysql2://'):
|
||||
case 0 === strpos($connection, 'oci:'):
|
||||
case 0 === strpos($connection, 'oci8://'):
|
||||
case 0 === strpos($connection, 'pdo_oci://'):
|
||||
case 0 === strpos($connection, 'pgsql:'):
|
||||
case 0 === strpos($connection, 'postgres://'):
|
||||
case 0 === strpos($connection, 'postgresql://'):
|
||||
case 0 === strpos($connection, 'sqlsrv:'):
|
||||
case 0 === strpos($connection, 'sqlite:'):
|
||||
case 0 === strpos($connection, 'sqlite3://'):
|
||||
return new PdoStore($connection);
|
||||
|
||||
case 0 === strpos($connection, 'zookeeper://'):
|
||||
return new ZookeeperStore(ZookeeperStore::createConnection($connection));
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', $connection));
|
||||
}
|
||||
}
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockAcquiringException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockReleasingException;
|
||||
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* ZookeeperStore is a PersistingStoreInterface implementation using Zookeeper as store engine.
|
||||
*
|
||||
* @author Ganesh Chandrasekaran <gchandrasekaran@wayfair.com>
|
||||
*/
|
||||
class ZookeeperStore implements StoreInterface
|
||||
{
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private $zookeeper;
|
||||
|
||||
public function __construct(\Zookeeper $zookeeper)
|
||||
{
|
||||
$this->zookeeper = $zookeeper;
|
||||
}
|
||||
|
||||
public static function createConnection(string $dsn): \Zookeeper
|
||||
{
|
||||
if (0 !== strpos($dsn, 'zookeeper:')) {
|
||||
throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
|
||||
}
|
||||
|
||||
if (false === $params = parse_url($dsn)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid Zookeeper DSN: %s.', $dsn));
|
||||
}
|
||||
|
||||
$host = $params['host'] ?? '';
|
||||
if (isset($params['port'])) {
|
||||
$host .= ':'.$params['port'];
|
||||
}
|
||||
|
||||
return new \Zookeeper($host);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
if ($this->exists($key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resource = $this->getKeyResource($key);
|
||||
$token = $this->getUniqueToken($key);
|
||||
|
||||
$this->createNewLock($resource, $token);
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
if (!$this->exists($key)) {
|
||||
return;
|
||||
}
|
||||
$resource = $this->getKeyResource($key);
|
||||
try {
|
||||
$this->zookeeper->delete($resource);
|
||||
} catch (\ZookeeperException $exception) {
|
||||
// For Zookeeper Ephemeral Nodes, the node will be deleted upon session death. But, if we want to unlock
|
||||
// the lock before proceeding further in the session, the client should be aware of this
|
||||
throw new LockReleasingException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
$resource = $this->getKeyResource($key);
|
||||
try {
|
||||
return $this->zookeeper->get($resource) === $this->getUniqueToken($key);
|
||||
} catch (\ZookeeperException $ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @deprecated since Symfony 4.4.
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
// do nothing, zookeeper locks forever.
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a zookeeper node.
|
||||
*
|
||||
* @param string $node The node which needs to be created
|
||||
* @param string $value The value to be assigned to a zookeeper node
|
||||
*
|
||||
* @throws LockConflictedException
|
||||
* @throws LockAcquiringException
|
||||
*/
|
||||
private function createNewLock(string $node, string $value)
|
||||
{
|
||||
// Default Node Permissions
|
||||
$acl = [['perms' => \Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone']];
|
||||
// This ensures that the nodes are deleted when the client session to zookeeper server ends.
|
||||
$type = \Zookeeper::EPHEMERAL;
|
||||
|
||||
try {
|
||||
$this->zookeeper->create($node, $value, $acl, $type);
|
||||
} catch (\ZookeeperException $ex) {
|
||||
if (\Zookeeper::NODEEXISTS === $ex->getCode()) {
|
||||
throw new LockConflictedException($ex);
|
||||
}
|
||||
|
||||
throw new LockAcquiringException($ex);
|
||||
}
|
||||
}
|
||||
|
||||
private function getKeyResource(Key $key): string
|
||||
{
|
||||
// Since we do not support storing locks as multi-level nodes, we convert them to be stored at root level.
|
||||
// For example: foo/bar will become /foo-bar and /foo/bar will become /-foo-bar
|
||||
$resource = (string) $key;
|
||||
|
||||
if (false !== strpos($resource, '/')) {
|
||||
$resource = strtr($resource, ['/' => '-']).'-'.sha1($resource);
|
||||
}
|
||||
|
||||
if ('' === $resource) {
|
||||
$resource = sha1($resource);
|
||||
}
|
||||
|
||||
return '/'.$resource;
|
||||
}
|
||||
|
||||
private function getUniqueToken(Key $key): string
|
||||
{
|
||||
if (!$key->hasState(self::class)) {
|
||||
$token = base64_encode(random_bytes(32));
|
||||
$key->setState(self::class, $token);
|
||||
}
|
||||
|
||||
return $key->getState(self::class);
|
||||
}
|
||||
}
|
||||
+3
-32
@@ -18,16 +18,11 @@ use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
* StoreInterface defines an interface to manipulate a lock store.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*
|
||||
* @deprecated since Symfony 4.4, use PersistingStoreInterface and BlockingStoreInterface instead
|
||||
*/
|
||||
interface StoreInterface
|
||||
interface StoreInterface extends PersistingStoreInterface
|
||||
{
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -37,28 +32,4 @@ interface StoreInterface
|
||||
* @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 second 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);
|
||||
}
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
-268
@@ -1,268 +0,0 @@
|
||||
<?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 testRefreshCustom()
|
||||
{
|
||||
$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, 20);
|
||||
|
||||
$lock->refresh(20);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<?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 a microsecond used to order async actions
|
||||
$clockDelay = 50000;
|
||||
|
||||
/** @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
<?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\Strategy\UnanimousStrategy;
|
||||
use Symfony\Component\Lock\Strategy\StrategyInterface;
|
||||
use Symfony\Component\Lock\Store\CombinedStore;
|
||||
use Symfony\Component\Lock\Store\RedisStore;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
<?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 a microsecond used to order async actions.
|
||||
*
|
||||
* @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 sensible: the $clockDelay could be adjust.
|
||||
*/
|
||||
public function testExpiration()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$clockDelay = $this->getClockDelay();
|
||||
|
||||
/** @var StoreInterface $store */
|
||||
$store = $this->getStore();
|
||||
|
||||
$store->save($key);
|
||||
$store->putOffExpiration($key, $clockDelay / 1000000);
|
||||
$this->assertTrue($store->exists($key));
|
||||
|
||||
usleep(2 * $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 sensible: the $clockDelay could be adjust.
|
||||
*/
|
||||
public function testRefreshLock()
|
||||
{
|
||||
// Amount a microsecond used to order async actions
|
||||
$clockDelay = $this->getClockDelay();
|
||||
|
||||
// Amount a microsecond used to order async actions
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
|
||||
/** @var StoreInterface $store */
|
||||
$store = $this->getStore();
|
||||
|
||||
$store->save($key);
|
||||
$store->putOffExpiration($key, $clockDelay / 1000000);
|
||||
$this->assertTrue($store->exists($key));
|
||||
|
||||
usleep(2 * $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());
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
$lines = explode(PHP_EOL, trim(`ipcs -su`));
|
||||
if ('------ Semaphore Status --------' !== $lines[0]) {
|
||||
throw new \Exception('Failed to extract list of opend semaphores. Expect 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 opend semaphores. Expect a used arrays key, got '.implode(PHP_EOL, $lines));
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?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;
|
||||
|
||||
public 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));
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
<?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;
|
||||
|
||||
public 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));
|
||||
}
|
||||
}
|
||||
Vendored
+6
-1
@@ -20,8 +20,13 @@
|
||||
"psr/log": "~1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "~2.5",
|
||||
"mongodb/mongodb": "~1.1",
|
||||
"predis/predis": "~1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "<2.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Lock\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
@@ -31,7 +36,7 @@
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.1-dev"
|
||||
"dev-master": "4.4-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-32
@@ -1,32 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/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>
|
||||
Reference in New Issue
Block a user