update
This commit is contained in:
218
projects/priceservice/vendor/symfony/lock/Store/CombinedStore.php
vendored
Normal file
218
projects/priceservice/vendor/symfony/lock/Store/CombinedStore.php
vendored
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
use Symfony\Component\Lock\SharedLockStoreInterface;
|
||||
use Symfony\Component\Lock\Strategy\StrategyInterface;
|
||||
|
||||
/**
|
||||
* CombinedStore is a PersistingStoreInterface implementation able to manage and synchronize several StoreInterfaces.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CombinedStore implements SharedLockStoreInterface, LoggerAwareInterface
|
||||
{
|
||||
use ExpiringStoreTrait;
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/** @var PersistingStoreInterface[] */
|
||||
private array $stores;
|
||||
private 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 PersistingStoreInterface) {
|
||||
throw new InvalidArgumentException(\sprintf('The store must implement "%s". Got "%s".', PersistingStoreInterface::class, get_debug_type($store)));
|
||||
}
|
||||
}
|
||||
|
||||
$this->stores = $stores;
|
||||
$this->strategy = $strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$storesCount = \count($this->stores);
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
try {
|
||||
$store->save($key);
|
||||
++$successCount;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger?->debug('One store failed to save the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
if (!$this->strategy->canBeMet($failureCount, $storesCount)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger?->info('Failed to store the "{resource}" lock. Quorum has not been met.', ['resource' => $key, 'success' => $successCount, 'failure' => $failureCount]);
|
||||
|
||||
// clean up potential locks
|
||||
$this->delete($key);
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function saveRead(Key $key)
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$storesCount = \count($this->stores);
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
try {
|
||||
if ($store instanceof SharedLockStoreInterface) {
|
||||
$store->saveRead($key);
|
||||
} else {
|
||||
$store->save($key);
|
||||
}
|
||||
++$successCount;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger?->debug('One store failed to save the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
if (!$this->strategy->canBeMet($failureCount, $storesCount)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger?->info('Failed to store the "{resource}" lock. Quorum has not been met.', ['resource' => $key, 'success' => $successCount, 'failure' => $failureCount]);
|
||||
|
||||
// clean up potential locks
|
||||
$this->delete($key);
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function putOffExpiration(Key $key, float $ttl)
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$storesCount = \count($this->stores);
|
||||
$expireAt = microtime(true) + $ttl;
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
try {
|
||||
if (0.0 >= $adjustedTtl = $expireAt - microtime(true)) {
|
||||
$this->logger?->debug('Stores took to long to put off the expiration of the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'ttl' => $ttl]);
|
||||
$key->reduceLifetime(0);
|
||||
break;
|
||||
}
|
||||
|
||||
$store->putOffExpiration($key, $adjustedTtl);
|
||||
++$successCount;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger?->debug('One store failed to put off the expiration of the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
if (!$this->strategy->canBeMet($failureCount, $storesCount)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger?->notice('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);
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
foreach ($this->stores as $store) {
|
||||
try {
|
||||
$store->delete($key);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger?->notice('One store failed to delete the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$storesCount = \count($this->stores);
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
try {
|
||||
if ($store->exists($key)) {
|
||||
++$successCount;
|
||||
} else {
|
||||
++$failureCount;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger?->debug('One store failed to check the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return true;
|
||||
}
|
||||
if (!$this->strategy->canBeMet($failureCount, $storesCount)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
75
projects/priceservice/vendor/symfony/lock/Store/DatabaseTableTrait.php
vendored
Normal file
75
projects/priceservice/vendor/symfony/lock/Store/DatabaseTableTrait.php
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
<?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\InvalidTtlException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait DatabaseTableTrait
|
||||
{
|
||||
private string $table = 'lock_keys';
|
||||
private string $idCol = 'key_id';
|
||||
private string $tokenCol = 'key_token';
|
||||
private string $expirationCol = 'key_expiration';
|
||||
private float $gcProbability;
|
||||
private int $initialTtl;
|
||||
|
||||
private function init(array $options, float $gcProbability, int $initialTtl): void
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
$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->gcProbability = $gcProbability;
|
||||
$this->initialTtl = $initialTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune the table randomly, based on GC probability.
|
||||
*/
|
||||
private function randomlyPrune(): void
|
||||
{
|
||||
if ($this->gcProbability > 0 && (1.0 === $this->gcProbability || (random_int(0, \PHP_INT_MAX) / \PHP_INT_MAX) <= $this->gcProbability)) {
|
||||
$this->prune();
|
||||
}
|
||||
}
|
||||
}
|
||||
309
projects/priceservice/vendor/symfony/lock/Store/DoctrineDbalPostgreSqlStore.php
vendored
Normal file
309
projects/priceservice/vendor/symfony/lock/Store/DoctrineDbalPostgreSqlStore.php
vendored
Normal file
@@ -0,0 +1,309 @@
|
||||
<?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\Configuration;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
|
||||
use Doctrine\DBAL\Tools\DsnParser;
|
||||
use Symfony\Component\Lock\BlockingSharedLockStoreInterface;
|
||||
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\SharedLockStoreInterface;
|
||||
|
||||
/**
|
||||
* DoctrineDbalPostgreSqlStore is a PersistingStoreInterface implementation using
|
||||
* PostgreSql advisory locks with a Doctrine DBAL Connection.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class DoctrineDbalPostgreSqlStore implements BlockingSharedLockStoreInterface, BlockingStoreInterface
|
||||
{
|
||||
private Connection $conn;
|
||||
private static array $storeRegistry = [];
|
||||
|
||||
/**
|
||||
* You can either pass an existing database connection a Doctrine DBAL Connection
|
||||
* or a URL that will be used to connect to the database.
|
||||
*
|
||||
* @throws InvalidArgumentException When first argument is not Connection nor string
|
||||
*/
|
||||
public function __construct(#[\SensitiveParameter] Connection|string $connOrUrl)
|
||||
{
|
||||
if ($connOrUrl instanceof Connection) {
|
||||
if (!$connOrUrl->getDatabasePlatform() instanceof PostgreSQLPlatform) {
|
||||
throw new InvalidArgumentException(\sprintf('The adapter "%s" does not support the "%s" platform.', __CLASS__, $connOrUrl->getDatabasePlatform()::class));
|
||||
}
|
||||
$this->conn = $connOrUrl;
|
||||
} else {
|
||||
if (!class_exists(DriverManager::class)) {
|
||||
throw new InvalidArgumentException('Failed to parse DSN. Try running "composer require doctrine/dbal".');
|
||||
}
|
||||
if (class_exists(DsnParser::class)) {
|
||||
$params = (new DsnParser([
|
||||
'db2' => 'ibm_db2',
|
||||
'mssql' => 'pdo_sqlsrv',
|
||||
'mysql' => 'pdo_mysql',
|
||||
'mysql2' => 'pdo_mysql',
|
||||
'postgres' => 'pdo_pgsql',
|
||||
'postgresql' => 'pdo_pgsql',
|
||||
'pgsql' => 'pdo_pgsql',
|
||||
'sqlite' => 'pdo_sqlite',
|
||||
'sqlite3' => 'pdo_sqlite',
|
||||
]))->parse($this->filterDsn($connOrUrl));
|
||||
} else {
|
||||
$params = ['url' => $this->filterDsn($connOrUrl)];
|
||||
}
|
||||
|
||||
$config = new Configuration();
|
||||
if (class_exists(DefaultSchemaManagerFactory::class)) {
|
||||
$config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
|
||||
}
|
||||
|
||||
$this->conn = DriverManager::getConnection($params, $config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
// prevent concurrency within the same connection
|
||||
$this->getInternalStore()->save($key);
|
||||
|
||||
$lockAcquired = false;
|
||||
|
||||
try {
|
||||
$sql = 'SELECT pg_try_advisory_lock(:key)';
|
||||
$result = $this->conn->executeQuery($sql, [
|
||||
'key' => $this->getHashedKey($key),
|
||||
]);
|
||||
|
||||
// Check if lock is acquired
|
||||
if (true === $result->fetchOne()) {
|
||||
$key->markUnserializable();
|
||||
// release sharedLock in case of promotion
|
||||
$this->unlockShared($key);
|
||||
|
||||
$lockAcquired = true;
|
||||
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
if (!$lockAcquired) {
|
||||
$this->getInternalStore()->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function saveRead(Key $key)
|
||||
{
|
||||
// prevent concurrency within the same connection
|
||||
$this->getInternalStore()->saveRead($key);
|
||||
|
||||
$lockAcquired = false;
|
||||
|
||||
try {
|
||||
$sql = 'SELECT pg_try_advisory_lock_shared(:key)';
|
||||
$result = $this->conn->executeQuery($sql, [
|
||||
'key' => $this->getHashedKey($key),
|
||||
]);
|
||||
|
||||
// Check if lock is acquired
|
||||
if (true === $result->fetchOne()) {
|
||||
$key->markUnserializable();
|
||||
// release lock in case of demotion
|
||||
$this->unlock($key);
|
||||
|
||||
$lockAcquired = true;
|
||||
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
if (!$lockAcquired) {
|
||||
$this->getInternalStore()->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function putOffExpiration(Key $key, float $ttl)
|
||||
{
|
||||
// postgresql locks forever.
|
||||
// check if lock still exists
|
||||
if (!$this->exists($key)) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
// Prevent deleting locks own by an other key in the same connection
|
||||
if (!$this->exists($key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->unlock($key);
|
||||
|
||||
// Prevent deleting Readlocks own by current key AND an other key in the same connection
|
||||
$store = $this->getInternalStore();
|
||||
try {
|
||||
// If lock acquired = there is no other ReadLock
|
||||
$store->save($key);
|
||||
$this->unlockShared($key);
|
||||
} catch (LockConflictedException) {
|
||||
// an other key exists in this ReadLock
|
||||
}
|
||||
|
||||
$store->delete($key);
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
$sql = "SELECT count(*) FROM pg_locks WHERE locktype='advisory' AND objid=:key AND pid=pg_backend_pid()";
|
||||
$result = $this->conn->executeQuery($sql, [
|
||||
'key' => $this->getHashedKey($key),
|
||||
]);
|
||||
|
||||
if ($result->fetchOne() > 0) {
|
||||
// connection is locked, check for lock in internal store
|
||||
return $this->getInternalStore()->exists($key);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
// prevent concurrency within the same connection
|
||||
// Internal store does not allow blocking mode, because there is no way to acquire one in a single process
|
||||
$this->getInternalStore()->save($key);
|
||||
|
||||
$lockAcquired = false;
|
||||
$sql = 'SELECT pg_advisory_lock(:key)';
|
||||
try {
|
||||
$this->conn->executeStatement($sql, [
|
||||
'key' => $this->getHashedKey($key),
|
||||
]);
|
||||
$lockAcquired = true;
|
||||
} finally {
|
||||
if (!$lockAcquired) {
|
||||
$this->getInternalStore()->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
// release lock in case of promotion
|
||||
$this->unlockShared($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function waitAndSaveRead(Key $key)
|
||||
{
|
||||
// prevent concurrency within the same connection
|
||||
// Internal store does not allow blocking mode, because there is no way to acquire one in a single process
|
||||
$this->getInternalStore()->saveRead($key);
|
||||
|
||||
$lockAcquired = false;
|
||||
$sql = 'SELECT pg_advisory_lock_shared(:key)';
|
||||
try {
|
||||
$this->conn->executeStatement($sql, [
|
||||
'key' => $this->getHashedKey($key),
|
||||
]);
|
||||
$lockAcquired = true;
|
||||
} finally {
|
||||
if (!$lockAcquired) {
|
||||
$this->getInternalStore()->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
// release lock in case of demotion
|
||||
$this->unlock($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hashed version of the key.
|
||||
*/
|
||||
private function getHashedKey(Key $key): int
|
||||
{
|
||||
return crc32((string) $key);
|
||||
}
|
||||
|
||||
private function unlock(Key $key): void
|
||||
{
|
||||
do {
|
||||
$sql = "SELECT pg_advisory_unlock(objid::bigint) FROM pg_locks WHERE locktype='advisory' AND mode='ExclusiveLock' AND objid=:key AND pid=pg_backend_pid()";
|
||||
$result = $this->conn->executeQuery($sql, [
|
||||
'key' => $this->getHashedKey($key),
|
||||
]);
|
||||
} while (0 !== $result->rowCount());
|
||||
}
|
||||
|
||||
private function unlockShared(Key $key): void
|
||||
{
|
||||
do {
|
||||
$sql = "SELECT pg_advisory_unlock_shared(objid::bigint) FROM pg_locks WHERE locktype='advisory' AND mode='ShareLock' AND objid=:key AND pid=pg_backend_pid()";
|
||||
$result = $this->conn->executeQuery($sql, [
|
||||
'key' => $this->getHashedKey($key),
|
||||
]);
|
||||
} while (0 !== $result->rowCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check driver and remove scheme extension from DSN.
|
||||
* From pgsql+advisory://server/ to pgsql://server/.
|
||||
*
|
||||
* @throws InvalidArgumentException when driver is not supported
|
||||
*/
|
||||
private function filterDsn(#[\SensitiveParameter] string $dsn): string
|
||||
{
|
||||
if (!str_contains($dsn, '://')) {
|
||||
throw new InvalidArgumentException('DSN is invalid for Doctrine DBAL.');
|
||||
}
|
||||
|
||||
[$scheme, $rest] = explode(':', $dsn, 2);
|
||||
$driver = strtok($scheme, '+');
|
||||
if (!\in_array($driver, ['pgsql', 'postgres', 'postgresql'])) {
|
||||
throw new InvalidArgumentException(\sprintf('The adapter "%s" does not support the "%s" driver.', __CLASS__, $driver));
|
||||
}
|
||||
|
||||
return \sprintf('%s:%s', $driver, $rest);
|
||||
}
|
||||
|
||||
private function getInternalStore(): SharedLockStoreInterface
|
||||
{
|
||||
$namespace = spl_object_hash($this->conn);
|
||||
|
||||
return self::$storeRegistry[$namespace] ??= new InMemoryStore();
|
||||
}
|
||||
}
|
||||
285
projects/priceservice/vendor/symfony/lock/Store/DoctrineDbalStore.php
vendored
Normal file
285
projects/priceservice/vendor/symfony/lock/Store/DoctrineDbalStore.php
vendored
Normal file
@@ -0,0 +1,285 @@
|
||||
<?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\Configuration;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\DBAL\Exception\TableNotFoundException;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Tools\DsnParser;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\InvalidTtlException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
|
||||
/**
|
||||
* DbalStore is a PersistingStoreInterface implementation using a Doctrine DBAL 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 DoctrineDbalStore implements PersistingStoreInterface
|
||||
{
|
||||
use DatabaseTableTrait;
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private Connection $conn;
|
||||
|
||||
/**
|
||||
* 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].
|
||||
*
|
||||
* @param Connection|string $connOrUrl A DBAL Connection instance or Doctrine URL
|
||||
* @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 namespace contains invalid characters
|
||||
* @throws InvalidArgumentException When the initial ttl is not valid
|
||||
*/
|
||||
public function __construct(Connection|string $connOrUrl, array $options = [], float $gcProbability = 0.01, int $initialTtl = 300)
|
||||
{
|
||||
$this->init($options, $gcProbability, $initialTtl);
|
||||
|
||||
if ($connOrUrl instanceof Connection) {
|
||||
$this->conn = $connOrUrl;
|
||||
} else {
|
||||
if (!class_exists(DriverManager::class)) {
|
||||
throw new InvalidArgumentException('Failed to parse the DSN. Try running "composer require doctrine/dbal".');
|
||||
}
|
||||
if (class_exists(DsnParser::class)) {
|
||||
$params = (new DsnParser([
|
||||
'db2' => 'ibm_db2',
|
||||
'mssql' => 'pdo_sqlsrv',
|
||||
'mysql' => 'pdo_mysql',
|
||||
'mysql2' => 'pdo_mysql',
|
||||
'postgres' => 'pdo_pgsql',
|
||||
'postgresql' => 'pdo_pgsql',
|
||||
'pgsql' => 'pdo_pgsql',
|
||||
'sqlite' => 'pdo_sqlite',
|
||||
'sqlite3' => 'pdo_sqlite',
|
||||
]))->parse($connOrUrl);
|
||||
} else {
|
||||
$params = ['url' => $connOrUrl];
|
||||
}
|
||||
|
||||
$config = new Configuration();
|
||||
if (class_exists(DefaultSchemaManagerFactory::class)) {
|
||||
$config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
|
||||
}
|
||||
|
||||
$this->conn = DriverManager::getConnection($params, $config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$key->reduceLifetime($this->initialTtl);
|
||||
|
||||
$sql = "INSERT INTO $this->table ($this->idCol, $this->tokenCol, $this->expirationCol) VALUES (?, ?, {$this->getCurrentTimestampStatement()} + $this->initialTtl)";
|
||||
|
||||
try {
|
||||
$this->conn->executeStatement($sql, [
|
||||
$this->getHashedKey($key),
|
||||
$this->getUniqueToken($key),
|
||||
], [
|
||||
ParameterType::STRING,
|
||||
ParameterType::STRING,
|
||||
]);
|
||||
} catch (TableNotFoundException) {
|
||||
if (!$this->conn->isTransactionActive() || $this->platformSupportsTableCreationInTransaction()) {
|
||||
$this->createTable();
|
||||
}
|
||||
|
||||
try {
|
||||
$this->conn->executeStatement($sql, [
|
||||
$this->getHashedKey($key),
|
||||
$this->getUniqueToken($key),
|
||||
], [
|
||||
ParameterType::STRING,
|
||||
ParameterType::STRING,
|
||||
]);
|
||||
} catch (DBALException) {
|
||||
$this->putOffExpiration($key, $this->initialTtl);
|
||||
}
|
||||
} catch (DBALException) {
|
||||
// the lock is already acquired. It could be us. Let's try to put off.
|
||||
$this->putOffExpiration($key, $this->initialTtl);
|
||||
}
|
||||
|
||||
$this->randomlyPrune();
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
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()} + ?, $this->tokenCol = ? WHERE $this->idCol = ? AND ($this->tokenCol = ? OR $this->expirationCol <= {$this->getCurrentTimestampStatement()})";
|
||||
$uniqueToken = $this->getUniqueToken($key);
|
||||
|
||||
$result = $this->conn->executeQuery($sql, [
|
||||
$ttl,
|
||||
$uniqueToken,
|
||||
$this->getHashedKey($key),
|
||||
$uniqueToken,
|
||||
], [
|
||||
ParameterType::INTEGER,
|
||||
ParameterType::STRING,
|
||||
ParameterType::STRING,
|
||||
ParameterType::STRING,
|
||||
]);
|
||||
|
||||
// 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 (!$result->rowCount() && !$this->exists($key)) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$this->conn->delete($this->table, [
|
||||
$this->idCol => $this->getHashedKey($key),
|
||||
$this->tokenCol => $this->getUniqueToken($key),
|
||||
]);
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
$sql = "SELECT 1 FROM $this->table WHERE $this->idCol = ? AND $this->tokenCol = ? AND $this->expirationCol > {$this->getCurrentTimestampStatement()}";
|
||||
$result = $this->conn->fetchOne($sql, [
|
||||
$this->getHashedKey($key),
|
||||
$this->getUniqueToken($key),
|
||||
], [
|
||||
ParameterType::STRING,
|
||||
ParameterType::STRING,
|
||||
]);
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the table to store lock keys which can be called once for setup.
|
||||
*
|
||||
* @throws DBALException When the table already exists
|
||||
*/
|
||||
public function createTable(): void
|
||||
{
|
||||
$schema = new Schema();
|
||||
$this->configureSchema($schema);
|
||||
|
||||
foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) {
|
||||
$this->conn->executeStatement($sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the Table to the Schema if it doesn't exist.
|
||||
*
|
||||
* @param \Closure $isSameDatabase
|
||||
*/
|
||||
public function configureSchema(Schema $schema/* , \Closure $isSameDatabase */): void
|
||||
{
|
||||
if ($schema->hasTable($this->table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isSameDatabase = 1 < \func_num_args() ? func_get_arg(1) : static fn () => true;
|
||||
|
||||
if (!$isSameDatabase($this->conn->executeStatement(...))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the table by removing all expired locks.
|
||||
*/
|
||||
private function prune(): void
|
||||
{
|
||||
$sql = "DELETE FROM $this->table WHERE $this->expirationCol <= {$this->getCurrentTimestampStatement()}";
|
||||
|
||||
$this->conn->executeStatement($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an SQL function to get the current timestamp regarding the current connection's driver.
|
||||
*/
|
||||
private function getCurrentTimestampStatement(): string
|
||||
{
|
||||
$platform = $this->conn->getDatabasePlatform();
|
||||
|
||||
return match (true) {
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\MySQL57Platform,
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\MySQLPlatform => 'UNIX_TIMESTAMP(NOW(6))',
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform => "(julianday('now') - 2440587.5) * 86400.0",
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform,
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform => 'CAST(EXTRACT(epoch FROM NOW()) AS DOUBLE PRECISION)',
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform => "(CAST(systimestamp AT TIME ZONE 'UTC' AS DATE) - DATE '1970-01-01') * 86400 + TO_NUMBER(TO_CHAR(systimestamp AT TIME ZONE 'UTC', 'SSSSS.FF'))",
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform,
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform => "CAST(DATEDIFF_BIG(ms, '1970-01-01', SYSUTCDATETIME()) AS FLOAT) / 1000.0",
|
||||
default => (new \DateTimeImmutable())->format('U.u'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether current platform supports table creation within transaction.
|
||||
*/
|
||||
private function platformSupportsTableCreationInTransaction(): bool
|
||||
{
|
||||
$platform = $this->conn->getDatabasePlatform();
|
||||
|
||||
return match (true) {
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform,
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform,
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform,
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform,
|
||||
$platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
30
projects/priceservice/vendor/symfony/lock/Store/ExpiringStoreTrait.php
vendored
Normal file
30
projects/priceservice/vendor/symfony/lock/Store/ExpiringStoreTrait.php
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
|
||||
trait ExpiringStoreTrait
|
||||
{
|
||||
private function checkNotExpired(Key $key): void
|
||||
{
|
||||
if ($key->isExpired()) {
|
||||
try {
|
||||
$this->delete($key);
|
||||
} catch (\Exception) {
|
||||
// swallow exception to not hide the original issue
|
||||
}
|
||||
throw new LockExpiredException(\sprintf('Failed to store the "%s" lock.', $key));
|
||||
}
|
||||
}
|
||||
}
|
||||
165
projects/priceservice/vendor/symfony/lock/Store/FlockStore.php
vendored
Normal file
165
projects/priceservice/vendor/symfony/lock/Store/FlockStore.php
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
<?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\BlockingStoreInterface;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockStorageException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\SharedLockStoreInterface;
|
||||
|
||||
/**
|
||||
* FlockStore is a PersistingStoreInterface implementation using the FileSystem flock.
|
||||
*
|
||||
* Original implementation in \Symfony\Component\Filesystem\LockHandler.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
* @author Romain Neutron <imprec@gmail.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class FlockStore implements BlockingStoreInterface, SharedLockStoreInterface
|
||||
{
|
||||
private ?string $lockPath;
|
||||
|
||||
/**
|
||||
* @param string|null $lockPath the directory to store the lock, defaults to the system's temporary directory
|
||||
*
|
||||
* @throws LockStorageException If the lock directory doesn’t exist or is not writable
|
||||
*/
|
||||
public function __construct(?string $lockPath = null)
|
||||
{
|
||||
if (!is_dir($lockPath ??= sys_get_temp_dir())) {
|
||||
if (false === @mkdir($lockPath, 0777, true) && !is_dir($lockPath)) {
|
||||
throw new InvalidArgumentException(\sprintf('The FlockStore directory "%s" does not exists and cannot be created.', $lockPath));
|
||||
}
|
||||
} elseif (!is_writable($lockPath)) {
|
||||
throw new InvalidArgumentException(\sprintf('The FlockStore directory "%s" is not writable.', $lockPath));
|
||||
}
|
||||
|
||||
$this->lockPath = $lockPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$this->lock($key, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function saveRead(Key $key)
|
||||
{
|
||||
$this->lock($key, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
$this->lock($key, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function waitAndSaveRead(Key $key)
|
||||
{
|
||||
$this->lock($key, true, true);
|
||||
}
|
||||
|
||||
private function lock(Key $key, bool $read, bool $blocking): void
|
||||
{
|
||||
$handle = null;
|
||||
// The lock is maybe already acquired.
|
||||
if ($key->hasState(__CLASS__)) {
|
||||
[$stateRead, $handle] = $key->getState(__CLASS__);
|
||||
// Check for promotion or demotion
|
||||
if ($stateRead === $read) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$handle) {
|
||||
$fileName = \sprintf('%s/sf.%s.%s.lock',
|
||||
$this->lockPath,
|
||||
substr(preg_replace('/[^a-z0-9\._-]+/i', '-', $key), 0, 50),
|
||||
strtr(substr(base64_encode(hash('sha256', $key, true)), 0, 7), '/', '_')
|
||||
);
|
||||
|
||||
// Silence error reporting
|
||||
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
|
||||
try {
|
||||
if (!$handle = fopen($fileName, 'r+') ?: fopen($fileName, 'r')) {
|
||||
if ($handle = fopen($fileName, 'x')) {
|
||||
chmod($fileName, 0666);
|
||||
} elseif (!$handle = fopen($fileName, 'r+') ?: fopen($fileName, 'r')) {
|
||||
usleep(100); // Give some time for chmod() to complete
|
||||
$handle = fopen($fileName, 'r+') ?: fopen($fileName, 'r');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$handle) {
|
||||
throw new LockStorageException($error, 0, null);
|
||||
}
|
||||
|
||||
// On Windows, even if PHP doc says the contrary, LOCK_NB works, see
|
||||
// https://bugs.php.net/54129
|
||||
if (!flock($handle, ($read ? \LOCK_SH : \LOCK_EX) | ($blocking ? 0 : \LOCK_NB))) {
|
||||
fclose($handle);
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$key->setState(__CLASS__, [$read, $handle]);
|
||||
$key->markUnserializable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function putOffExpiration(Key $key, float $ttl)
|
||||
{
|
||||
// do nothing, the flock locks forever.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
// The lock is maybe not acquired.
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$handle = $key->getState(__CLASS__)[1];
|
||||
|
||||
flock($handle, \LOCK_UN | \LOCK_NB);
|
||||
fclose($handle);
|
||||
|
||||
$key->removeState(__CLASS__);
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
return $key->hasState(__CLASS__);
|
||||
}
|
||||
}
|
||||
126
projects/priceservice/vendor/symfony/lock/Store/InMemoryStore.php
vendored
Normal file
126
projects/priceservice/vendor/symfony/lock/Store/InMemoryStore.php
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
<?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\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\SharedLockStoreInterface;
|
||||
|
||||
/**
|
||||
* InMemoryStore is a PersistingStoreInterface implementation using
|
||||
* php-array to manage locks.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class InMemoryStore implements SharedLockStoreInterface
|
||||
{
|
||||
private array $locks = [];
|
||||
private array $readLocks = [];
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$hashKey = (string) $key;
|
||||
$token = $this->getUniqueToken($key);
|
||||
if (isset($this->locks[$hashKey])) {
|
||||
// already acquired
|
||||
if ($this->locks[$hashKey] === $token) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
// check for promotion
|
||||
if (isset($this->readLocks[$hashKey][$token]) && 1 === \count($this->readLocks[$hashKey])) {
|
||||
unset($this->readLocks[$hashKey]);
|
||||
$this->locks[$hashKey] = $token;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (\count($this->readLocks[$hashKey] ?? []) > 0) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$this->locks[$hashKey] = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function saveRead(Key $key)
|
||||
{
|
||||
$hashKey = (string) $key;
|
||||
$token = $this->getUniqueToken($key);
|
||||
|
||||
// check if lock is already acquired in read mode
|
||||
if (isset($this->readLocks[$hashKey])) {
|
||||
$this->readLocks[$hashKey][$token] = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// check for demotion
|
||||
if (isset($this->locks[$hashKey])) {
|
||||
if ($this->locks[$hashKey] !== $token) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
unset($this->locks[$hashKey]);
|
||||
}
|
||||
|
||||
$this->readLocks[$hashKey][$token] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function putOffExpiration(Key $key, float $ttl)
|
||||
{
|
||||
// do nothing, memory locks forever.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$hashKey = (string) $key;
|
||||
$token = $this->getUniqueToken($key);
|
||||
|
||||
unset($this->readLocks[$hashKey][$token]);
|
||||
if (($this->locks[$hashKey] ?? null) === $token) {
|
||||
unset($this->locks[$hashKey]);
|
||||
}
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
$hashKey = (string) $key;
|
||||
$token = $this->getUniqueToken($key);
|
||||
|
||||
return isset($this->readLocks[$hashKey][$token]) || ($this->locks[$hashKey] ?? null) === $token;
|
||||
}
|
||||
|
||||
private function getUniqueToken(Key $key): string
|
||||
{
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
$token = base64_encode(random_bytes(32));
|
||||
$key->setState(__CLASS__, $token);
|
||||
}
|
||||
|
||||
return $key->getState(__CLASS__);
|
||||
}
|
||||
}
|
||||
167
projects/priceservice/vendor/symfony/lock/Store/MemcachedStore.php
vendored
Normal file
167
projects/priceservice/vendor/symfony/lock/Store/MemcachedStore.php
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
<?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\InvalidTtlException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
|
||||
/**
|
||||
* MemcachedStore is a PersistingStoreInterface implementation using Memcached as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class MemcachedStore implements PersistingStoreInterface
|
||||
{
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private \Memcached $memcached;
|
||||
private int $initialTtl;
|
||||
private bool $useExtendedReturn;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public static function isSupported()
|
||||
{
|
||||
return \extension_loaded('memcached');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $initialTtl the expiration delay of locks in seconds
|
||||
*/
|
||||
public function __construct(\Memcached $memcached, int $initialTtl = 300)
|
||||
{
|
||||
if (!static::isSupported()) {
|
||||
throw new InvalidArgumentException('Memcached extension is required.');
|
||||
}
|
||||
|
||||
if ($initialTtl < 1) {
|
||||
throw new InvalidArgumentException(\sprintf('"%s()" expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
}
|
||||
|
||||
$this->memcached = $memcached;
|
||||
$this->initialTtl = $initialTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function save(Key $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);
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function putOffExpiration(Key $key, float $ttl)
|
||||
{
|
||||
if ($ttl < 1) {
|
||||
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->getUniqueToken($key);
|
||||
|
||||
[$value, $cas] = $this->getValueAndCas($key);
|
||||
|
||||
$key->reduceLifetime($ttl);
|
||||
// Could happens when we ask a putOff after a timeout but in luck nobody steal the lock
|
||||
if (\Memcached::RES_NOTFOUND === $this->memcached->getResultCode()) {
|
||||
if ($this->memcached->add((string) $key, $token, $ttl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// no luck, with concurrency, someone else acquire the lock
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
// Someone else steal the lock
|
||||
if ($value !== $token) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
if (!$this->memcached->cas($cas, (string) $key, $token, $ttl)) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$token = $this->getUniqueToken($key);
|
||||
|
||||
[$value, $cas] = $this->getValueAndCas($key);
|
||||
|
||||
if ($value !== $token) {
|
||||
// we are not the owner of the lock. Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// To avoid concurrency in deletion, the trick is to extends the TTL then deleting the key
|
||||
if (!$this->memcached->cas($cas, (string) $key, $token, 2)) {
|
||||
// Someone steal our lock. It does not belongs to us anymore. Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, we are the owner of the lock for 2 more seconds, we can delete it.
|
||||
$this->memcached->delete((string) $key);
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
return $this->memcached->get((string) $key) === $this->getUniqueToken($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__);
|
||||
}
|
||||
|
||||
private function getValueAndCas(Key $key): array
|
||||
{
|
||||
if ($this->useExtendedReturn ??= version_compare(phpversion('memcached'), '2.9.9', '>')) {
|
||||
$extendedReturn = $this->memcached->get((string) $key, null, \Memcached::GET_EXTENDED);
|
||||
if (\Memcached::GET_ERROR_RETURN_VALUE === $extendedReturn) {
|
||||
return [$extendedReturn, 0.0];
|
||||
}
|
||||
|
||||
return [$extendedReturn['value'], $extendedReturn['cas']];
|
||||
}
|
||||
|
||||
$cas = 0.0;
|
||||
$value = $this->memcached->get((string) $key, null, $cas);
|
||||
|
||||
return [$value, $cas];
|
||||
}
|
||||
}
|
||||
405
projects/priceservice/vendor/symfony/lock/Store/MongoDbStore.php
vendored
Normal file
405
projects/priceservice/vendor/symfony/lock/Store/MongoDbStore.php
vendored
Normal file
@@ -0,0 +1,405 @@
|
||||
<?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 MongoDB\BSON\UTCDateTime;
|
||||
use MongoDB\Client;
|
||||
use MongoDB\Collection;
|
||||
use MongoDB\Database;
|
||||
use MongoDB\Driver\BulkWrite;
|
||||
use MongoDB\Driver\Command;
|
||||
use MongoDB\Driver\Exception\BulkWriteException;
|
||||
use MongoDB\Driver\Manager;
|
||||
use MongoDB\Driver\Query;
|
||||
use MongoDB\Driver\ReadPreference;
|
||||
use MongoDB\Driver\WriteConcern;
|
||||
use MongoDB\Exception\DriverRuntimeException;
|
||||
use MongoDB\Exception\InvalidArgumentException as MongoInvalidArgumentException;
|
||||
use MongoDB\Exception\UnsupportedException;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\InvalidTtlException;
|
||||
use Symfony\Component\Lock\Exception\LockAcquiringException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockExpiredException;
|
||||
use Symfony\Component\Lock\Exception\LockStorageException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
|
||||
/**
|
||||
* MongoDbStore is a StoreInterface implementation using MongoDB as a storage
|
||||
* engine. Support for MongoDB server >=2.2 due to use of TTL indexes.
|
||||
*
|
||||
* CAUTION: TTL Indexes are used so 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.
|
||||
*
|
||||
* CAUTION: The locked resource name is indexed in the _id field of the lock
|
||||
* collection. An indexed field's value in MongoDB can be a maximum of 1024
|
||||
* bytes in length inclusive of structural overhead.
|
||||
*
|
||||
* @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit
|
||||
*
|
||||
* @author Joe Bennett <joe@assimtech.com>
|
||||
* @author Jérôme Tamarelle <jerome@tamarelle.net>
|
||||
*/
|
||||
class MongoDbStore implements PersistingStoreInterface
|
||||
{
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private Manager $manager;
|
||||
private string $namespace;
|
||||
private string $uri;
|
||||
private array $options;
|
||||
private float $initialTtl;
|
||||
|
||||
/**
|
||||
* @param Collection|Database|Client|Manager|string $mongo An instance of a Collection or Client or URI @see https://docs.mongodb.com/manual/reference/connection-string/
|
||||
* @param array $options See below
|
||||
* @param float $initialTtl The expiration delay of locks in seconds
|
||||
*
|
||||
* @throws InvalidArgumentException If required options are not provided
|
||||
* @throws InvalidTtlException When the initial ttl is not valid
|
||||
*
|
||||
* Options:
|
||||
* gcProbability: Should a TTL Index be created expressed as a probability from 0.0 to 1.0 [default: 0.001]
|
||||
* database: The name of the database [required when $mongo is a Client]
|
||||
* collection: The name of the collection [required when $mongo is a Client]
|
||||
* uriOptions: Array of uri options. [used when $mongo is a URI]
|
||||
* driverOptions: Array of driver options. [used when $mongo is a URI]
|
||||
*
|
||||
* When using a URI string:
|
||||
* The database is determined from the uri's path, otherwise the "database" option is used. To specify an alternate authentication database; "authSource" uriOption or querystring parameter must be used.
|
||||
* The collection is determined from the uri's "collection" querystring parameter, otherwise the "collection" option is used.
|
||||
*
|
||||
* For example: mongodb://myuser:mypass@myhost/mydatabase?collection=mycollection
|
||||
*
|
||||
* @see https://docs.mongodb.com/php-library/current/reference/method/MongoDBClient__construct/
|
||||
*
|
||||
* If gcProbability is set to a value greater than 0.0 there is a chance
|
||||
* this store will attempt to create a TTL index on self::save().
|
||||
* If you prefer to create your TTL Index manually you can set gcProbability
|
||||
* to 0.0 and optionally leverage
|
||||
* self::createTtlIndex(int $expireAfterSeconds = 0).
|
||||
*
|
||||
* readConcern is not specified by MongoDbStore meaning the connection's settings will take effect.
|
||||
* writeConcern is majority for all update queries.
|
||||
* readPreference is primary for all read queries.
|
||||
*
|
||||
* @see https://docs.mongodb.com/manual/applications/replication/
|
||||
*/
|
||||
public function __construct(Collection|Database|Client|Manager|string $mongo, array $options = [], float $initialTtl = 300.0)
|
||||
{
|
||||
if (isset($options['gcProbablity'])) {
|
||||
trigger_deprecation('symfony/lock', '6.3', 'The "gcProbablity" option (notice the typo in its name) is deprecated in "%s"; use the "gcProbability" option instead.', __CLASS__);
|
||||
|
||||
$options['gcProbability'] = $options['gcProbablity'];
|
||||
unset($options['gcProbablity']);
|
||||
}
|
||||
|
||||
$this->options = array_merge([
|
||||
'gcProbability' => 0.001,
|
||||
'database' => null,
|
||||
'collection' => null,
|
||||
'uriOptions' => [],
|
||||
'driverOptions' => [],
|
||||
], $options);
|
||||
|
||||
$this->initialTtl = $initialTtl;
|
||||
|
||||
if ($mongo instanceof Collection) {
|
||||
$this->options['database'] ??= $mongo->getDatabaseName();
|
||||
$this->options['collection'] ??= $mongo->getCollectionName();
|
||||
$this->manager = $mongo->getManager();
|
||||
} elseif ($mongo instanceof Database) {
|
||||
$this->options['database'] ??= $mongo->getDatabaseName();
|
||||
$this->manager = $mongo->getManager();
|
||||
} elseif ($mongo instanceof Client) {
|
||||
$this->manager = $mongo->getManager();
|
||||
} elseif ($mongo instanceof Manager) {
|
||||
$this->manager = $mongo;
|
||||
} else {
|
||||
$this->uri = $this->skimUri($mongo);
|
||||
}
|
||||
|
||||
if (null === $this->options['database']) {
|
||||
throw new InvalidArgumentException(\sprintf('"%s()" requires the "database" in the URI path or option.', __METHOD__));
|
||||
}
|
||||
if (null === $this->options['collection']) {
|
||||
throw new InvalidArgumentException(\sprintf('"%s()" requires the "collection" in the URI querystring or option.', __METHOD__));
|
||||
}
|
||||
$this->namespace = $this->options['database'].'.'.$this->options['collection'];
|
||||
|
||||
if ($this->options['gcProbability'] < 0.0 || $this->options['gcProbability'] > 1.0) {
|
||||
throw new InvalidArgumentException(\sprintf('"%s()" gcProbability must be a float from 0.0 to 1.0, "%f" given.', __METHOD__, $this->options['gcProbability']));
|
||||
}
|
||||
|
||||
if ($this->initialTtl <= 0) {
|
||||
throw new InvalidTtlException(\sprintf('"%s()" expects a strictly positive TTL, got "%d".', __METHOD__, $this->initialTtl));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract default database and collection from given connection URI and remove collection querystring.
|
||||
*
|
||||
* Non-standard parameters are removed from the URI to improve libmongoc's re-use of connections.
|
||||
*
|
||||
* @see https://php.net/mongodb.connection-handling
|
||||
*/
|
||||
private function skimUri(string $uri): string
|
||||
{
|
||||
if (!str_starts_with($uri, 'mongodb://') && !str_starts_with($uri, 'mongodb+srv://')) {
|
||||
throw new InvalidArgumentException(\sprintf('The given MongoDB Connection URI "%s" is invalid. Expecting "mongodb://" or "mongodb+srv://".', $uri));
|
||||
}
|
||||
|
||||
if (false === $params = parse_url($uri)) {
|
||||
throw new InvalidArgumentException(\sprintf('The given MongoDB Connection URI "%s" is invalid.', $uri));
|
||||
}
|
||||
$pathDb = ltrim($params['path'] ?? '', '/') ?: null;
|
||||
if (null !== $pathDb) {
|
||||
$this->options['database'] = $pathDb;
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
if (preg_match('/^(.*[\?&])collection=([^&#]*)&?(([^#]*).*)$/', $uri, $matches)) {
|
||||
$prefix = $matches[1];
|
||||
$this->options['collection'] = $matches[2];
|
||||
if (empty($matches[4])) {
|
||||
$prefix = substr($prefix, 0, -1);
|
||||
}
|
||||
$uri = $prefix.$matches[3];
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TTL index to automatically remove expired locks.
|
||||
*
|
||||
* If the gcProbability option is set higher than 0.0 (defaults to 0.001);
|
||||
* there is a chance this will be called on self::save().
|
||||
*
|
||||
* Otherwise; this should be called once manually during database setup.
|
||||
*
|
||||
* Alternatively the TTL index can be created manually on the database:
|
||||
*
|
||||
* db.lock.createIndex(
|
||||
* { "expires_at": 1 },
|
||||
* { "expireAfterSeconds": 0 }
|
||||
* )
|
||||
*
|
||||
* Please note, expires_at is based on the application server. If the
|
||||
* database time differs; a lock could be cleaned up before it has expired.
|
||||
* To ensure locks don't expire prematurely; the lock TTL should be set
|
||||
* with enough extra time to account for any clock drift between nodes.
|
||||
*
|
||||
* A TTL index MUST BE used to automatically clean up expired locks.
|
||||
*
|
||||
* @see http://docs.mongodb.org/manual/tutorial/expire-data/
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws UnsupportedException if options are not supported by the selected server
|
||||
* @throws MongoInvalidArgumentException for parameter/option parsing errors
|
||||
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
|
||||
*/
|
||||
public function createTtlIndex(int $expireAfterSeconds = 0)
|
||||
{
|
||||
$server = $this->getManager()->selectServer();
|
||||
$server->executeCommand($this->options['database'], new Command([
|
||||
'createIndexes' => $this->options['collection'],
|
||||
'indexes' => [
|
||||
[
|
||||
'key' => [
|
||||
'expires_at' => 1,
|
||||
],
|
||||
'name' => 'expires_at_1',
|
||||
'expireAfterSeconds' => $expireAfterSeconds,
|
||||
],
|
||||
],
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*
|
||||
* @throws LockExpiredException when save is called on an expired lock
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$key->reduceLifetime($this->initialTtl);
|
||||
|
||||
try {
|
||||
$this->upsert($key, $this->initialTtl);
|
||||
} catch (BulkWriteException $e) {
|
||||
if ($this->isDuplicateKeyException($e)) {
|
||||
throw new LockConflictedException('Lock was acquired by someone else.', 0, $e);
|
||||
}
|
||||
throw new LockAcquiringException('Failed to acquire lock.', 0, $e);
|
||||
}
|
||||
|
||||
if ($this->options['gcProbability'] > 0.0 && (1.0 === $this->options['gcProbability'] || (random_int(0, \PHP_INT_MAX) / \PHP_INT_MAX) <= $this->options['gcProbability'])) {
|
||||
$this->createTtlIndex();
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*
|
||||
* @throws LockStorageException
|
||||
* @throws LockExpiredException
|
||||
*/
|
||||
public function putOffExpiration(Key $key, float $ttl)
|
||||
{
|
||||
$key->reduceLifetime($ttl);
|
||||
|
||||
try {
|
||||
$this->upsert($key, $ttl);
|
||||
} catch (BulkWriteException $e) {
|
||||
if ($this->isDuplicateKeyException($e)) {
|
||||
throw new LockConflictedException('Failed to put off the expiration of the lock.', 0, $e);
|
||||
}
|
||||
throw new LockStorageException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$write = new BulkWrite();
|
||||
$write->delete(
|
||||
[
|
||||
'_id' => (string) $key,
|
||||
'token' => $this->getUniqueToken($key),
|
||||
],
|
||||
['limit' => 1]
|
||||
);
|
||||
|
||||
$this->getManager()->executeBulkWrite(
|
||||
$this->namespace,
|
||||
$write,
|
||||
['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)]
|
||||
);
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
$cursor = $this->manager->executeQuery($this->namespace, new Query(
|
||||
[
|
||||
'_id' => (string) $key,
|
||||
'token' => $this->getUniqueToken($key),
|
||||
'expires_at' => [
|
||||
'$gt' => $this->createMongoDateTime(microtime(true)),
|
||||
],
|
||||
],
|
||||
[
|
||||
'limit' => 1,
|
||||
'projection' => ['_id' => 1],
|
||||
]
|
||||
), [
|
||||
'readPreference' => new ReadPreference(ReadPreference::PRIMARY)
|
||||
]);
|
||||
|
||||
return [] !== $cursor->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or Insert a Key.
|
||||
*
|
||||
* @param float $ttl Expiry in seconds from now
|
||||
*/
|
||||
private function upsert(Key $key, float $ttl): void
|
||||
{
|
||||
$now = microtime(true);
|
||||
$token = $this->getUniqueToken($key);
|
||||
|
||||
$write = new BulkWrite();
|
||||
$write->update(
|
||||
[
|
||||
'_id' => (string) $key,
|
||||
'$or' => [
|
||||
[
|
||||
'token' => $token,
|
||||
],
|
||||
[
|
||||
'expires_at' => [
|
||||
'$lte' => $this->createMongoDateTime($now),
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'$set' => [
|
||||
'_id' => (string) $key,
|
||||
'token' => $token,
|
||||
'expires_at' => $this->createMongoDateTime($now + $ttl),
|
||||
],
|
||||
],
|
||||
[
|
||||
'upsert' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$this->getManager()->executeBulkWrite(
|
||||
$this->namespace,
|
||||
$write,
|
||||
['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)]
|
||||
);
|
||||
}
|
||||
|
||||
private function isDuplicateKeyException(BulkWriteException $e): bool
|
||||
{
|
||||
$code = $e->getCode();
|
||||
|
||||
$writeErrors = $e->getWriteResult()->getWriteErrors();
|
||||
if (1 === \count($writeErrors)) {
|
||||
$code = $writeErrors[0]->getCode();
|
||||
}
|
||||
|
||||
// Mongo error E11000 - DuplicateKey
|
||||
return 11000 === $code;
|
||||
}
|
||||
|
||||
private function getManager(): Manager
|
||||
{
|
||||
return $this->manager ??= new Manager($this->uri, $this->options['uriOptions'], $this->options['driverOptions']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $seconds Seconds since 1970-01-01T00:00:00.000Z supporting millisecond precision. Defaults to now.
|
||||
*/
|
||||
private function createMongoDateTime(float $seconds): UTCDateTime
|
||||
{
|
||||
return new UTCDateTime((int) ($seconds * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a unique token for the given key namespaced to this store.
|
||||
*
|
||||
* @param Key $key lock state container
|
||||
*/
|
||||
private function getUniqueToken(Key $key): string
|
||||
{
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
$token = base64_encode(random_bytes(32));
|
||||
$key->setState(__CLASS__, $token);
|
||||
}
|
||||
|
||||
return $key->getState(__CLASS__);
|
||||
}
|
||||
}
|
||||
255
projects/priceservice/vendor/symfony/lock/Store/PdoStore.php
vendored
Normal file
255
projects/priceservice/vendor/symfony/lock/Store/PdoStore.php
vendored
Normal file
@@ -0,0 +1,255 @@
|
||||
<?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\InvalidTtlException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
|
||||
/**
|
||||
* 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 PersistingStoreInterface
|
||||
{
|
||||
use DatabaseTableTrait;
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private \PDO $conn;
|
||||
private string $dsn;
|
||||
private string $driver;
|
||||
private ?string $username = null;
|
||||
private ?string $password = null;
|
||||
private array $connectionOptions = [];
|
||||
|
||||
/**
|
||||
* You can either pass an existing database connection as PDO instance
|
||||
* 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 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 the initial ttl is not valid
|
||||
*/
|
||||
public function __construct(#[\SensitiveParameter] \PDO|string $connOrDsn, #[\SensitiveParameter] array $options = [], float $gcProbability = 0.01, int $initialTtl = 300)
|
||||
{
|
||||
$this->init($options, $gcProbability, $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;
|
||||
} else {
|
||||
$this->dsn = $connOrDsn;
|
||||
}
|
||||
|
||||
$this->username = $options['db_username'] ?? $this->username;
|
||||
$this->password = $options['db_password'] ?? $this->password;
|
||||
$this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
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)";
|
||||
$conn = $this->getConnection();
|
||||
try {
|
||||
$stmt = $conn->prepare($sql);
|
||||
} catch (\PDOException $e) {
|
||||
if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->getDriver(), ['pgsql', 'sqlite', 'sqlsrv'], true))) {
|
||||
$this->createTable();
|
||||
}
|
||||
$stmt = $conn->prepare($sql);
|
||||
}
|
||||
|
||||
$stmt->bindValue(':id', $this->getHashedKey($key));
|
||||
$stmt->bindValue(':token', $this->getUniqueToken($key));
|
||||
|
||||
try {
|
||||
$stmt->execute();
|
||||
} catch (\PDOException $e) {
|
||||
if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->getDriver(), ['pgsql', 'sqlite', 'sqlsrv'], true))) {
|
||||
$this->createTable();
|
||||
|
||||
try {
|
||||
$stmt->execute();
|
||||
} catch (\PDOException) {
|
||||
$this->putOffExpiration($key, $this->initialTtl);
|
||||
}
|
||||
} else {
|
||||
// the lock is already acquired. It could be us. Let's try to put off.
|
||||
$this->putOffExpiration($key, $this->initialTtl);
|
||||
}
|
||||
}
|
||||
|
||||
$this->randomlyPrune();
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function putOffExpiration(Key $key, float $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);
|
||||
$result = $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 (!(\is_object($result) ? $result : $stmt)->rowCount() && !$this->exists($key)) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
$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));
|
||||
$result = $stmt->execute();
|
||||
|
||||
return (bool) (\is_object($result) ? $result->fetchOne() : $stmt->fetchColumn());
|
||||
}
|
||||
|
||||
private function getConnection(): \PDO
|
||||
{
|
||||
if (!isset($this->conn)) {
|
||||
$this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
|
||||
$this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
}
|
||||
|
||||
return $this->conn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the table to store lock keys which can be called once for setup.
|
||||
*
|
||||
* @throws \PDOException When the table already exists
|
||||
* @throws \DomainException When an unsupported PDO driver is used
|
||||
*/
|
||||
public function createTable(): void
|
||||
{
|
||||
$sql = match ($driver = $this->getDriver()) {
|
||||
'mysql' => "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 utf8mb4_bin, ENGINE = InnoDB",
|
||||
'sqlite' => "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->tokenCol TEXT NOT NULL, $this->expirationCol INTEGER)",
|
||||
'pgsql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(64) NOT NULL, $this->expirationCol INTEGER)",
|
||||
'oci' => "CREATE TABLE $this->table ($this->idCol VARCHAR2(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR2(64) NOT NULL, $this->expirationCol INTEGER)",
|
||||
'sqlsrv' => "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(64) NOT NULL, $this->expirationCol INTEGER)",
|
||||
default => throw new \DomainException(\sprintf('Creating the lock table is currently not implemented for platform "%s".', $driver)),
|
||||
};
|
||||
|
||||
$this->getConnection()->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
|
||||
{
|
||||
return $this->driver ??= $this->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an SQL function to get the current timestamp regarding the current connection's driver.
|
||||
*/
|
||||
private function getCurrentTimestampStatement(): string
|
||||
{
|
||||
return match ($this->getDriver()) {
|
||||
'mysql' => 'UNIX_TIMESTAMP(NOW(6))',
|
||||
'sqlite' => "(julianday('now') - 2440587.5) * 86400.0",
|
||||
'pgsql' => 'CAST(EXTRACT(epoch FROM NOW()) AS DOUBLE PRECISION)',
|
||||
'oci' => "(CAST(systimestamp AT TIME ZONE 'UTC' AS DATE) - DATE '1970-01-01') * 86400 + TO_NUMBER(TO_CHAR(systimestamp AT TIME ZONE 'UTC', 'SSSSS.FF'))",
|
||||
'sqlsrv' => "CAST(DATEDIFF_BIG(ms, '1970-01-01', SYSUTCDATETIME()) AS FLOAT) / 1000.0",
|
||||
default => (new \DateTimeImmutable())->format('U.u'),
|
||||
};
|
||||
}
|
||||
|
||||
private function isTableMissing(\PDOException $exception): bool
|
||||
{
|
||||
$driver = $this->getDriver();
|
||||
[$sqlState, $code] = $exception->errorInfo ?? [null, $exception->getCode()];
|
||||
|
||||
return match ($driver) {
|
||||
'pgsql' => '42P01' === $sqlState,
|
||||
'sqlite' => str_contains($exception->getMessage(), 'no such table:'),
|
||||
'oci' => 942 === $code,
|
||||
'sqlsrv' => 208 === $code,
|
||||
'mysql' => 1146 === $code,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
308
projects/priceservice/vendor/symfony/lock/Store/PostgreSqlStore.php
vendored
Normal file
308
projects/priceservice/vendor/symfony/lock/Store/PostgreSqlStore.php
vendored
Normal file
@@ -0,0 +1,308 @@
|
||||
<?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\BlockingSharedLockStoreInterface;
|
||||
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\SharedLockStoreInterface;
|
||||
|
||||
/**
|
||||
* PostgreSqlStore is a PersistingStoreInterface implementation using
|
||||
* PostgreSql advisory locks.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class PostgreSqlStore implements BlockingSharedLockStoreInterface, BlockingStoreInterface
|
||||
{
|
||||
private \PDO $conn;
|
||||
private string $dsn;
|
||||
private ?string $username = null;
|
||||
private ?string $password = null;
|
||||
private array $connectionOptions = [];
|
||||
private static array $storeRegistry = [];
|
||||
|
||||
/**
|
||||
* You can either pass an existing database connection as PDO instance 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_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 array $options An associative array of options
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public function __construct(#[\SensitiveParameter] \PDO|string $connOrDsn, #[\SensitiveParameter] array $options = [])
|
||||
{
|
||||
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;
|
||||
$this->checkDriver();
|
||||
} else {
|
||||
$this->dsn = $connOrDsn;
|
||||
}
|
||||
|
||||
$this->username = $options['db_username'] ?? $this->username;
|
||||
$this->password = $options['db_password'] ?? $this->password;
|
||||
$this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
// prevent concurrency within the same connection
|
||||
$this->getInternalStore()->save($key);
|
||||
|
||||
$lockAcquired = false;
|
||||
|
||||
try {
|
||||
$sql = 'SELECT pg_try_advisory_lock(:key)';
|
||||
$stmt = $this->getConnection()->prepare($sql);
|
||||
$stmt->bindValue(':key', $this->getHashedKey($key));
|
||||
$result = $stmt->execute();
|
||||
|
||||
// Check if lock is acquired
|
||||
if (true === $stmt->fetchColumn()) {
|
||||
$key->markUnserializable();
|
||||
// release sharedLock in case of promotion
|
||||
$this->unlockShared($key);
|
||||
|
||||
$lockAcquired = true;
|
||||
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
if (!$lockAcquired) {
|
||||
$this->getInternalStore()->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function saveRead(Key $key)
|
||||
{
|
||||
// prevent concurrency within the same connection
|
||||
$this->getInternalStore()->saveRead($key);
|
||||
|
||||
$lockAcquired = false;
|
||||
|
||||
try {
|
||||
$sql = 'SELECT pg_try_advisory_lock_shared(:key)';
|
||||
$stmt = $this->getConnection()->prepare($sql);
|
||||
|
||||
$stmt->bindValue(':key', $this->getHashedKey($key));
|
||||
$result = $stmt->execute();
|
||||
|
||||
// Check if lock is acquired
|
||||
if (true === $stmt->fetchColumn()) {
|
||||
$key->markUnserializable();
|
||||
// release lock in case of demotion
|
||||
$this->unlock($key);
|
||||
|
||||
$lockAcquired = true;
|
||||
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
if (!$lockAcquired) {
|
||||
$this->getInternalStore()->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function putOffExpiration(Key $key, float $ttl)
|
||||
{
|
||||
// postgresql locks forever.
|
||||
// check if lock still exists
|
||||
if (!$this->exists($key)) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
// Prevent deleting locks own by an other key in the same connection
|
||||
if (!$this->exists($key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->unlock($key);
|
||||
|
||||
// Prevent deleting Readlocks own by current key AND an other key in the same connection
|
||||
$store = $this->getInternalStore();
|
||||
try {
|
||||
// If lock acquired = there is no other ReadLock
|
||||
$store->save($key);
|
||||
$this->unlockShared($key);
|
||||
} catch (LockConflictedException) {
|
||||
// an other key exists in this ReadLock
|
||||
}
|
||||
|
||||
$store->delete($key);
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
$sql = "SELECT count(*) FROM pg_locks WHERE locktype='advisory' AND objid=:key AND pid=pg_backend_pid()";
|
||||
$stmt = $this->getConnection()->prepare($sql);
|
||||
|
||||
$stmt->bindValue(':key', $this->getHashedKey($key));
|
||||
$result = $stmt->execute();
|
||||
|
||||
if ($stmt->fetchColumn() > 0) {
|
||||
// connection is locked, check for lock in internal store
|
||||
return $this->getInternalStore()->exists($key);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
// prevent concurrency within the same connection
|
||||
// Internal store does not allow blocking mode, because there is no way to acquire one in a single process
|
||||
$this->getInternalStore()->save($key);
|
||||
|
||||
$lockAcquired = false;
|
||||
$sql = 'SELECT pg_advisory_lock(:key)';
|
||||
try {
|
||||
$stmt = $this->getConnection()->prepare($sql);
|
||||
$stmt->bindValue(':key', $this->getHashedKey($key));
|
||||
$stmt->execute();
|
||||
$lockAcquired = true;
|
||||
} finally {
|
||||
if (!$lockAcquired) {
|
||||
$this->getInternalStore()->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
// release lock in case of promotion
|
||||
$this->unlockShared($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function waitAndSaveRead(Key $key)
|
||||
{
|
||||
// prevent concurrency within the same connection
|
||||
// Internal store does not allow blocking mode, because there is no way to acquire one in a single process
|
||||
$this->getInternalStore()->saveRead($key);
|
||||
|
||||
$lockAcquired = false;
|
||||
$sql = 'SELECT pg_advisory_lock_shared(:key)';
|
||||
|
||||
try {
|
||||
$stmt = $this->getConnection()->prepare($sql);
|
||||
$stmt->bindValue(':key', $this->getHashedKey($key));
|
||||
$stmt->execute();
|
||||
$lockAcquired = true;
|
||||
} finally {
|
||||
if (!$lockAcquired) {
|
||||
$this->getInternalStore()->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
// release lock in case of demotion
|
||||
$this->unlock($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hashed version of the key.
|
||||
*/
|
||||
private function getHashedKey(Key $key): int
|
||||
{
|
||||
return crc32((string) $key);
|
||||
}
|
||||
|
||||
private function unlock(Key $key): void
|
||||
{
|
||||
while (true) {
|
||||
$sql = "SELECT pg_advisory_unlock(objid::bigint) FROM pg_locks WHERE locktype='advisory' AND mode='ExclusiveLock' AND objid=:key AND pid=pg_backend_pid()";
|
||||
$stmt = $this->getConnection()->prepare($sql);
|
||||
$stmt->bindValue(':key', $this->getHashedKey($key));
|
||||
$result = $stmt->execute();
|
||||
|
||||
if (0 === $stmt->rowCount()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function unlockShared(Key $key): void
|
||||
{
|
||||
while (true) {
|
||||
$sql = "SELECT pg_advisory_unlock_shared(objid::bigint) FROM pg_locks WHERE locktype='advisory' AND mode='ShareLock' AND objid=:key AND pid=pg_backend_pid()";
|
||||
$stmt = $this->getConnection()->prepare($sql);
|
||||
$stmt->bindValue(':key', $this->getHashedKey($key));
|
||||
$result = $stmt->execute();
|
||||
|
||||
if (0 === $stmt->rowCount()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getConnection(): \PDO
|
||||
{
|
||||
if (!isset($this->conn)) {
|
||||
$this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
|
||||
$this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$this->checkDriver();
|
||||
}
|
||||
|
||||
return $this->conn;
|
||||
}
|
||||
|
||||
private function checkDriver(): void
|
||||
{
|
||||
if ('pgsql' !== $driver = $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
|
||||
throw new InvalidArgumentException(\sprintf('The adapter "%s" does not support the "%s" driver.', __CLASS__, $driver));
|
||||
}
|
||||
}
|
||||
|
||||
private function getInternalStore(): SharedLockStoreInterface
|
||||
{
|
||||
$namespace = spl_object_hash($this->getConnection());
|
||||
|
||||
return self::$storeRegistry[$namespace] ??= new InMemoryStore();
|
||||
}
|
||||
}
|
||||
318
projects/priceservice/vendor/symfony/lock/Store/RedisStore.php
vendored
Normal file
318
projects/priceservice/vendor/symfony/lock/Store/RedisStore.php
vendored
Normal file
@@ -0,0 +1,318 @@
|
||||
<?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 Predis\Response\ServerException;
|
||||
use Relay\Relay;
|
||||
use Symfony\Component\Lock\Exception\InvalidTtlException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockStorageException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
use Symfony\Component\Lock\SharedLockStoreInterface;
|
||||
|
||||
/**
|
||||
* RedisStore is a PersistingStoreInterface implementation using Redis as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class RedisStore implements SharedLockStoreInterface
|
||||
{
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private bool $supportTime;
|
||||
|
||||
/**
|
||||
* @param float $initialTtl The expiration delay of locks in seconds
|
||||
*/
|
||||
public function __construct(
|
||||
private \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis,
|
||||
private float $initialTtl = 300.0,
|
||||
) {
|
||||
if ($initialTtl <= 0) {
|
||||
throw new InvalidTtlException(\sprintf('"%s()" expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$script = '
|
||||
local key = KEYS[1]
|
||||
local uniqueToken = ARGV[2]
|
||||
local ttl = tonumber(ARGV[3])
|
||||
|
||||
-- asserts the KEY is compatible with current version (old Symfony <5.2 BC)
|
||||
if redis.call("TYPE", key).ok == "string" then
|
||||
return false
|
||||
end
|
||||
|
||||
'.$this->getNowCode().'
|
||||
|
||||
-- Remove expired values
|
||||
redis.call("ZREMRANGEBYSCORE", key, "-inf", now)
|
||||
|
||||
-- is already acquired
|
||||
if redis.call("ZSCORE", key, uniqueToken) then
|
||||
-- is not WRITE lock and cannot be promoted
|
||||
if not redis.call("ZSCORE", key, "__write__") and redis.call("ZCOUNT", key, "-inf", "+inf") > 1 then
|
||||
return false
|
||||
end
|
||||
elseif redis.call("ZCOUNT", key, "-inf", "+inf") > 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
redis.call("ZADD", key, now + ttl, uniqueToken)
|
||||
redis.call("ZADD", key, now + ttl, "__write__")
|
||||
|
||||
-- Extend the TTL of the key
|
||||
local maxExpiration = redis.call("ZREVRANGE", key, 0, 0, "WITHSCORES")[2]
|
||||
redis.call("PEXPIREAT", key, maxExpiration)
|
||||
|
||||
return true
|
||||
';
|
||||
|
||||
$key->reduceLifetime($this->initialTtl);
|
||||
if (!$this->evaluate($script, (string) $key, [microtime(true), $this->getUniqueToken($key), (int) ceil($this->initialTtl * 1000)])) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function saveRead(Key $key)
|
||||
{
|
||||
$script = '
|
||||
local key = KEYS[1]
|
||||
local uniqueToken = ARGV[2]
|
||||
local ttl = tonumber(ARGV[3])
|
||||
|
||||
-- asserts the KEY is compatible with current version (old Symfony <5.2 BC)
|
||||
if redis.call("TYPE", key).ok == "string" then
|
||||
return false
|
||||
end
|
||||
|
||||
'.$this->getNowCode().'
|
||||
|
||||
-- Remove expired values
|
||||
redis.call("ZREMRANGEBYSCORE", key, "-inf", now)
|
||||
|
||||
-- lock not already acquired and a WRITE lock exists?
|
||||
if not redis.call("ZSCORE", key, uniqueToken) and redis.call("ZSCORE", key, "__write__") then
|
||||
return false
|
||||
end
|
||||
|
||||
redis.call("ZADD", key, now + ttl, uniqueToken)
|
||||
redis.call("ZREM", key, "__write__")
|
||||
|
||||
-- Extend the TTL of the key
|
||||
local maxExpiration = redis.call("ZREVRANGE", key, 0, 0, "WITHSCORES")[2]
|
||||
redis.call("PEXPIREAT", key, maxExpiration)
|
||||
|
||||
return true
|
||||
';
|
||||
|
||||
$key->reduceLifetime($this->initialTtl);
|
||||
if (!$this->evaluate($script, (string) $key, [microtime(true), $this->getUniqueToken($key), (int) ceil($this->initialTtl * 1000)])) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function putOffExpiration(Key $key, float $ttl)
|
||||
{
|
||||
$script = '
|
||||
local key = KEYS[1]
|
||||
local uniqueToken = ARGV[2]
|
||||
local ttl = tonumber(ARGV[3])
|
||||
|
||||
-- asserts the KEY is compatible with current version (old Symfony <5.2 BC)
|
||||
if redis.call("TYPE", key).ok == "string" then
|
||||
return false
|
||||
end
|
||||
|
||||
'.$this->getNowCode().'
|
||||
|
||||
-- lock already acquired acquired?
|
||||
if not redis.call("ZSCORE", key, uniqueToken) then
|
||||
return false
|
||||
end
|
||||
|
||||
redis.call("ZADD", key, now + ttl, uniqueToken)
|
||||
-- if the lock is also a WRITE lock, increase the TTL
|
||||
if redis.call("ZSCORE", key, "__write__") then
|
||||
redis.call("ZADD", key, now + ttl, "__write__")
|
||||
end
|
||||
|
||||
-- Extend the TTL of the key
|
||||
local maxExpiration = redis.call("ZREVRANGE", key, 0, 0, "WITHSCORES")[2]
|
||||
redis.call("PEXPIREAT", key, maxExpiration)
|
||||
|
||||
return true
|
||||
';
|
||||
|
||||
$key->reduceLifetime($ttl);
|
||||
if (!$this->evaluate($script, (string) $key, [microtime(true), $this->getUniqueToken($key), (int) ceil($ttl * 1000)])) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$script = '
|
||||
local key = KEYS[1]
|
||||
local uniqueToken = ARGV[1]
|
||||
|
||||
-- asserts the KEY is compatible with current version (old Symfony <5.2 BC)
|
||||
if redis.call("TYPE", key).ok == "string" then
|
||||
return false
|
||||
end
|
||||
|
||||
-- lock not already acquired
|
||||
if not redis.call("ZSCORE", key, uniqueToken) then
|
||||
return false
|
||||
end
|
||||
|
||||
redis.call("ZREM", key, uniqueToken)
|
||||
redis.call("ZREM", key, "__write__")
|
||||
|
||||
local maxExpiration = redis.call("ZREVRANGE", key, 0, 0, "WITHSCORES")[2]
|
||||
if nil ~= maxExpiration then
|
||||
redis.call("PEXPIREAT", key, maxExpiration)
|
||||
end
|
||||
|
||||
return true
|
||||
';
|
||||
|
||||
$this->evaluate($script, (string) $key, [$this->getUniqueToken($key)]);
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
$script = '
|
||||
local key = KEYS[1]
|
||||
local uniqueToken = ARGV[2]
|
||||
|
||||
-- asserts the KEY is compatible with current version (old Symfony <5.2 BC)
|
||||
if redis.call("TYPE", key).ok == "string" then
|
||||
return false
|
||||
end
|
||||
|
||||
'.$this->getNowCode().'
|
||||
|
||||
-- Remove expired values
|
||||
redis.call("ZREMRANGEBYSCORE", key, "-inf", now)
|
||||
|
||||
if redis.call("ZSCORE", key, uniqueToken) then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
';
|
||||
|
||||
return (bool) $this->evaluate($script, (string) $key, [microtime(true), $this->getUniqueToken($key)]);
|
||||
}
|
||||
|
||||
private function evaluate(string $script, string $resource, array $args): mixed
|
||||
{
|
||||
if ($this->redis instanceof \Redis || $this->redis instanceof Relay || $this->redis instanceof \RedisCluster) {
|
||||
$this->redis->clearLastError();
|
||||
$result = $this->redis->eval($script, array_merge([$resource], $args), 1);
|
||||
if (null !== $err = $this->redis->getLastError()) {
|
||||
throw new LockStorageException($err);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($this->redis instanceof \RedisArray) {
|
||||
$client = $this->redis->_instance($this->redis->_target($resource));
|
||||
$client->clearLastError();
|
||||
$result = $client->eval($script, array_merge([$resource], $args), 1);
|
||||
if (null !== $err = $client->getLastError()) {
|
||||
throw new LockStorageException($err);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
\assert($this->redis instanceof \Predis\ClientInterface);
|
||||
|
||||
try {
|
||||
return $this->redis->eval(...array_merge([$script, 1, $resource], $args));
|
||||
} catch (ServerException $e) {
|
||||
throw new LockStorageException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function getUniqueToken(Key $key): string
|
||||
{
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
$token = base64_encode(random_bytes(32));
|
||||
$key->setState(__CLASS__, $token);
|
||||
}
|
||||
|
||||
return $key->getState(__CLASS__);
|
||||
}
|
||||
|
||||
private function getNowCode(): string
|
||||
{
|
||||
if (!isset($this->supportTime)) {
|
||||
// Redis < 5.0 does not support TIME (not deterministic) in script.
|
||||
// https://redis.io/commands/eval#replicating-commands-instead-of-scripts
|
||||
// This code asserts TIME can be use, otherwise will fallback to a timestamp generated by the PHP process.
|
||||
$script = '
|
||||
local now = redis.call("TIME")
|
||||
redis.call("SET", KEYS[1], "1", "PX", 1)
|
||||
|
||||
return 1
|
||||
';
|
||||
try {
|
||||
$this->supportTime = 1 === $this->evaluate($script, 'symfony_check_support_time', []);
|
||||
} catch (LockStorageException $e) {
|
||||
if (!str_contains($e->getMessage(), 'commands not allowed after non deterministic')
|
||||
&& !str_contains($e->getMessage(), 'is not allowed from script script')
|
||||
) {
|
||||
throw $e;
|
||||
}
|
||||
$this->supportTime = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->supportTime) {
|
||||
return '
|
||||
local now = redis.call("TIME")
|
||||
now = now[1] * 1000 + math.floor(now[2] / 1000)
|
||||
';
|
||||
}
|
||||
|
||||
return '
|
||||
local now = tonumber(ARGV[1])
|
||||
now = math.floor(now * 1000)
|
||||
';
|
||||
}
|
||||
}
|
||||
111
projects/priceservice/vendor/symfony/lock/Store/SemaphoreStore.php
vendored
Normal file
111
projects/priceservice/vendor/symfony/lock/Store/SemaphoreStore.php
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
<?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\BlockingStoreInterface;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
|
||||
/**
|
||||
* SemaphoreStore is a PersistingStoreInterface implementation using Semaphore as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class SemaphoreStore implements BlockingStoreInterface
|
||||
{
|
||||
/**
|
||||
* Returns whether or not the store is supported.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function isSupported(): bool
|
||||
{
|
||||
return \extension_loaded('sysvsem');
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (!static::isSupported()) {
|
||||
throw new InvalidArgumentException('Semaphore extension (sysvsem) is required.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$this->lock($key, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
$this->lock($key, true);
|
||||
}
|
||||
|
||||
private function lock(Key $key, bool $blocking): void
|
||||
{
|
||||
if ($key->hasState(__CLASS__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$keyId = unpack('i', hash('xxh128', $key, true))[1];
|
||||
$resource = @sem_get($keyId);
|
||||
$acquired = $resource && @sem_acquire($resource, !$blocking);
|
||||
|
||||
while ($blocking && !$acquired) {
|
||||
$resource = @sem_get($keyId);
|
||||
$acquired = $resource && @sem_acquire($resource);
|
||||
}
|
||||
|
||||
if (!$acquired) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$key->setState(__CLASS__, $resource);
|
||||
$key->markUnserializable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
// The lock is maybe not acquired.
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resource = $key->getState(__CLASS__);
|
||||
|
||||
sem_remove($resource);
|
||||
|
||||
$key->removeState(__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function putOffExpiration(Key $key, float $ttl)
|
||||
{
|
||||
// do nothing, the semaphore locks forever.
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
return $key->hasState(__CLASS__);
|
||||
}
|
||||
}
|
||||
113
projects/priceservice/vendor/symfony/lock/Store/StoreFactory.php
vendored
Normal file
113
projects/priceservice/vendor/symfony/lock/Store/StoreFactory.php
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
<?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 Relay\Relay;
|
||||
use Symfony\Component\Cache\Adapter\AbstractAdapter;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
|
||||
/**
|
||||
* StoreFactory create stores and connections.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class StoreFactory
|
||||
{
|
||||
public static function createStore(#[\SensitiveParameter] object|string $connection): PersistingStoreInterface
|
||||
{
|
||||
switch (true) {
|
||||
case $connection instanceof \Redis:
|
||||
case $connection instanceof Relay:
|
||||
case $connection instanceof \RedisArray:
|
||||
case $connection instanceof \RedisCluster:
|
||||
case $connection instanceof \Predis\ClientInterface:
|
||||
return new RedisStore($connection);
|
||||
|
||||
case $connection instanceof \Memcached:
|
||||
return new MemcachedStore($connection);
|
||||
|
||||
case $connection instanceof \MongoDB\Collection:
|
||||
return new MongoDbStore($connection);
|
||||
|
||||
case $connection instanceof \PDO:
|
||||
return new PdoStore($connection);
|
||||
|
||||
case $connection instanceof Connection:
|
||||
return new DoctrineDbalStore($connection);
|
||||
|
||||
case $connection instanceof \Zookeeper:
|
||||
return new ZookeeperStore($connection);
|
||||
|
||||
case !\is_string($connection):
|
||||
throw new InvalidArgumentException(\sprintf('Unsupported Connection: "%s".', get_debug_type($connection)));
|
||||
case 'flock' === $connection:
|
||||
return new FlockStore();
|
||||
|
||||
case str_starts_with($connection, 'flock://'):
|
||||
return new FlockStore(substr($connection, 8));
|
||||
|
||||
case 'semaphore' === $connection:
|
||||
return new SemaphoreStore();
|
||||
|
||||
case str_starts_with($connection, 'redis:'):
|
||||
case str_starts_with($connection, 'rediss:'):
|
||||
case str_starts_with($connection, 'memcached:'):
|
||||
if (!class_exists(AbstractAdapter::class)) {
|
||||
throw new InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".');
|
||||
}
|
||||
$storeClass = str_starts_with($connection, 'memcached:') ? MemcachedStore::class : RedisStore::class;
|
||||
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
|
||||
|
||||
return new $storeClass($connection);
|
||||
|
||||
case str_starts_with($connection, 'mongodb'):
|
||||
return new MongoDbStore($connection);
|
||||
|
||||
case str_starts_with($connection, 'mssql://'):
|
||||
case str_starts_with($connection, 'mysql://'):
|
||||
case str_starts_with($connection, 'mysql2://'):
|
||||
case str_starts_with($connection, 'oci8://'):
|
||||
case str_starts_with($connection, 'pdo_oci://'):
|
||||
case str_starts_with($connection, 'pgsql://'):
|
||||
case str_starts_with($connection, 'postgres://'):
|
||||
case str_starts_with($connection, 'postgresql://'):
|
||||
case str_starts_with($connection, 'sqlite://'):
|
||||
case str_starts_with($connection, 'sqlite3://'):
|
||||
return new DoctrineDbalStore($connection);
|
||||
|
||||
case str_starts_with($connection, 'mysql:'):
|
||||
case str_starts_with($connection, 'oci:'):
|
||||
case str_starts_with($connection, 'pgsql:'):
|
||||
case str_starts_with($connection, 'sqlsrv:'):
|
||||
case str_starts_with($connection, 'sqlite:'):
|
||||
return new PdoStore($connection);
|
||||
|
||||
case str_starts_with($connection, 'pgsql+advisory://'):
|
||||
case str_starts_with($connection, 'postgres+advisory://'):
|
||||
case str_starts_with($connection, 'postgresql+advisory://'):
|
||||
return new DoctrineDbalPostgreSqlStore($connection);
|
||||
|
||||
case str_starts_with($connection, 'pgsql+advisory:'):
|
||||
return new PostgreSqlStore(preg_replace('/^([^:+]+)\+advisory/', '$1', $connection));
|
||||
|
||||
case str_starts_with($connection, 'zookeeper://'):
|
||||
return new ZookeeperStore(ZookeeperStore::createConnection($connection));
|
||||
|
||||
case 'in-memory' === $connection:
|
||||
return new InMemoryStore();
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(\sprintf('Unsupported Connection: "%s".', $connection));
|
||||
}
|
||||
}
|
||||
166
projects/priceservice/vendor/symfony/lock/Store/ZookeeperStore.php
vendored
Normal file
166
projects/priceservice/vendor/symfony/lock/Store/ZookeeperStore.php
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Lock\Store;
|
||||
|
||||
use Symfony\Component\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\Key;
|
||||
use Symfony\Component\Lock\PersistingStoreInterface;
|
||||
|
||||
/**
|
||||
* ZookeeperStore is a PersistingStoreInterface implementation using Zookeeper as store engine.
|
||||
*
|
||||
* @author Ganesh Chandrasekaran <gchandrasekaran@wayfair.com>
|
||||
*/
|
||||
class ZookeeperStore implements PersistingStoreInterface
|
||||
{
|
||||
use ExpiringStoreTrait;
|
||||
|
||||
private \Zookeeper $zookeeper;
|
||||
|
||||
public function __construct(\Zookeeper $zookeeper)
|
||||
{
|
||||
$this->zookeeper = $zookeeper;
|
||||
}
|
||||
|
||||
public static function createConnection(#[\SensitiveParameter] string $dsn): \Zookeeper
|
||||
{
|
||||
if (!str_starts_with($dsn, 'zookeeper:')) {
|
||||
throw new InvalidArgumentException('Unsupported DSN for Zookeeper.');
|
||||
}
|
||||
|
||||
if (false === $params = parse_url($dsn)) {
|
||||
throw new InvalidArgumentException('Invalid Zookeeper DSN.');
|
||||
}
|
||||
|
||||
$host = $params['host'] ?? '';
|
||||
$hosts = explode(',', $host);
|
||||
|
||||
foreach ($hosts as $index => $host) {
|
||||
if (isset($params['port'])) {
|
||||
$hosts[$index] = $host.':'.$params['port'];
|
||||
}
|
||||
}
|
||||
|
||||
return new \Zookeeper(implode(',', $hosts));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
if ($this->exists($key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resource = $this->getKeyResource($key);
|
||||
$token = $this->getUniqueToken($key);
|
||||
|
||||
$this->createNewLock($resource, $token);
|
||||
$key->markUnserializable();
|
||||
|
||||
$this->checkNotExpired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public function exists(Key $key): bool
|
||||
{
|
||||
$resource = $this->getKeyResource($key);
|
||||
try {
|
||||
return $this->zookeeper->get($resource) === $this->getUniqueToken($key);
|
||||
} catch (\ZookeeperException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function putOffExpiration(Key $key, float $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): void
|
||||
{
|
||||
// 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 (str_contains($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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user