Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock;
use Symfony\Component\Lock\Exception\LockConflictedException;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface BlockingSharedLockStoreInterface extends SharedLockStoreInterface
{
/**
* Waits until a key becomes free for reading, then stores the resource.
*
* @return void
*
* @throws LockConflictedException
*/
public function waitAndSaveRead(Key $key);
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock;
use Symfony\Component\Lock\Exception\LockConflictedException;
/**
* @author Hamza Amrouche <hamza.simperfit@gmail.com>
*/
interface BlockingStoreInterface extends PersistingStoreInterface
{
/**
* Waits until a key becomes free, then stores the resource.
*
* @return void
*
* @throws LockConflictedException
*/
public function waitAndSave(Key $key);
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Exception;
/**
* Base ExceptionInterface for the Lock Component.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Exception;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Exception;
/**
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
*/
class InvalidTtlException extends InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Exception;
/**
* LockAcquiringException is thrown when an issue happens during the acquisition of a lock.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockAcquiringException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Exception;
use Symfony\Component\Lock\Lock;
/**
* LockConflictedException is thrown when a lock is acquired by someone else.
*
* In non-blocking mode it is caught by {@see Lock::acquire()} and {@see Lock::acquireRead()}.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockConflictedException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Exception;
/**
* LockExpiredException is thrown when a lock may conflict due to a TTL expiration.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockExpiredException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Exception;
/**
* LockReleasingException is thrown when an issue happens during the release of a lock.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockReleasingException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Exception;
/**
* LockStorageException is thrown when an issue happens during the manipulation of a lock in a store.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class LockStorageException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Exception;
/**
* UnserializableKeyException is thrown when the key contains state that can no
* be serialized and the user try to serialize it.
* ie. Connection with a database, flock, semaphore, ...
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class UnserializableKeyException extends \RuntimeException implements ExceptionInterface
{
}

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

@@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock;
use Symfony\Component\Lock\Exception\UnserializableKeyException;
/**
* Key is a container for the state of the locks in stores.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
final class Key
{
private string $resource;
private ?float $expiringTime = null;
private array $state = [];
private bool $serializable = true;
public function __construct(string $resource)
{
$this->resource = $resource;
}
public function __toString(): string
{
return $this->resource;
}
public function hasState(string $stateKey): bool
{
return isset($this->state[$stateKey]);
}
public function setState(string $stateKey, mixed $state): void
{
$this->state[$stateKey] = $state;
}
public function removeState(string $stateKey): void
{
unset($this->state[$stateKey]);
}
public function getState(string $stateKey): mixed
{
return $this->state[$stateKey];
}
public function markUnserializable(): void
{
$this->serializable = false;
}
public function resetLifetime(): void
{
$this->expiringTime = null;
}
/**
* @param float $ttl the expiration delay of locks in seconds
*/
public function reduceLifetime(float $ttl): void
{
$newTime = microtime(true) + $ttl;
if (null === $this->expiringTime || $this->expiringTime > $newTime) {
$this->expiringTime = $newTime;
}
}
/**
* Returns the remaining lifetime in seconds.
*/
public function getRemainingLifetime(): ?float
{
return null === $this->expiringTime ? null : $this->expiringTime - microtime(true);
}
public function isExpired(): bool
{
return null !== $this->expiringTime && $this->expiringTime <= microtime(true);
}
public function __sleep(): array
{
if (!$this->serializable) {
throw new UnserializableKeyException('The key cannot be serialized.');
}
return ['resource', 'expiringTime', 'state'];
}
}

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

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

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

@@ -0,0 +1,246 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Exception\LockReleasingException;
/**
* Lock is the default implementation of the LockInterface.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
final class Lock implements SharedLockInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
private bool $dirty = false;
/**
* @param float|null $ttl Maximum expected lock duration in seconds
* @param bool $autoRelease Whether to automatically release the lock or not when the lock instance is destroyed
*/
public function __construct(
private Key $key,
private PersistingStoreInterface $store,
private ?float $ttl = null,
private bool $autoRelease = true,
) {
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
/**
* Automatically releases the underlying lock when the object is destructed.
*/
public function __destruct()
{
if (!$this->autoRelease || !$this->dirty || !$this->isAcquired()) {
return;
}
$this->release();
}
public function acquire(bool $blocking = false): bool
{
$this->key->resetLifetime();
try {
if ($blocking) {
if (!$this->store instanceof BlockingStoreInterface) {
while (true) {
try {
$this->store->save($this->key);
break;
} catch (LockConflictedException) {
usleep((100 + random_int(-10, 10)) * 1000);
}
}
} else {
$this->store->waitAndSave($this->key);
}
} else {
$this->store->save($this->key);
}
$this->dirty = true;
$this->logger?->debug('Successfully acquired the "{resource}" lock.', ['resource' => $this->key]);
if ($this->ttl) {
$this->refresh();
}
if ($this->key->isExpired()) {
try {
$this->release();
} catch (\Exception) {
// swallow exception to not hide the original issue
}
throw new LockExpiredException(\sprintf('Failed to store the "%s" lock.', $this->key));
}
return true;
} catch (LockConflictedException $e) {
$this->dirty = false;
$this->logger?->info('Failed to acquire the "{resource}" lock. Someone else already acquired the lock.', ['resource' => $this->key]);
if ($blocking) {
throw $e;
}
return false;
} catch (\Exception $e) {
$this->logger?->notice('Failed to acquire the "{resource}" lock.', ['resource' => $this->key, 'exception' => $e]);
throw new LockAcquiringException(\sprintf('Failed to acquire the "%s" lock.', $this->key), 0, $e);
}
}
public function acquireRead(bool $blocking = false): bool
{
$this->key->resetLifetime();
try {
if (!$this->store instanceof SharedLockStoreInterface) {
$this->logger?->debug('Store does not support ReadLocks, fallback to WriteLock.', ['resource' => $this->key]);
return $this->acquire($blocking);
}
if ($blocking) {
if (!$this->store instanceof BlockingSharedLockStoreInterface) {
while (true) {
try {
$this->store->saveRead($this->key);
break;
} catch (LockConflictedException) {
usleep((100 + random_int(-10, 10)) * 1000);
}
}
} else {
$this->store->waitAndSaveRead($this->key);
}
} else {
$this->store->saveRead($this->key);
}
$this->dirty = true;
$this->logger?->debug('Successfully acquired the "{resource}" lock for reading.', ['resource' => $this->key]);
if ($this->ttl) {
$this->refresh();
}
if ($this->key->isExpired()) {
try {
$this->release();
} catch (\Exception) {
// swallow exception to not hide the original issue
}
throw new LockExpiredException(\sprintf('Failed to store the "%s" lock.', $this->key));
}
return true;
} catch (LockConflictedException $e) {
$this->dirty = false;
$this->logger?->info('Failed to acquire the "{resource}" lock. Someone else already acquired the lock.', ['resource' => $this->key]);
if ($blocking) {
throw $e;
}
return false;
} catch (\Exception $e) {
$this->logger?->notice('Failed to acquire the "{resource}" lock.', ['resource' => $this->key, 'exception' => $e]);
throw new LockAcquiringException(\sprintf('Failed to acquire the "%s" lock.', $this->key), 0, $e);
}
}
public function refresh(?float $ttl = null): void
{
if (!$ttl ??= $this->ttl) {
throw new InvalidArgumentException('You have to define an expiration duration.');
}
try {
$this->key->resetLifetime();
$this->store->putOffExpiration($this->key, $ttl);
$this->dirty = true;
if ($this->key->isExpired()) {
try {
$this->release();
} catch (\Exception) {
// swallow exception to not hide the original issue
}
throw new LockExpiredException(\sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $this->key));
}
$this->logger?->debug('Expiration defined for "{resource}" lock for "{ttl}" seconds.', ['resource' => $this->key, 'ttl' => $ttl]);
} catch (LockConflictedException $e) {
$this->dirty = false;
$this->logger?->notice('Failed to define an expiration for the "{resource}" lock, someone else acquired the lock.', ['resource' => $this->key]);
throw $e;
} catch (\Exception $e) {
$this->logger?->notice('Failed to define an expiration for the "{resource}" lock.', ['resource' => $this->key, 'exception' => $e]);
throw new LockAcquiringException(\sprintf('Failed to define an expiration for the "%s" lock.', $this->key), 0, $e);
}
}
public function isAcquired(): bool
{
return $this->dirty = $this->store->exists($this->key);
}
public function release(): void
{
try {
try {
$this->store->delete($this->key);
$this->dirty = false;
} catch (LockReleasingException $e) {
throw $e;
} catch (\Exception $e) {
throw new LockReleasingException(\sprintf('Failed to release the "%s" lock.', $this->key), 0, $e);
}
if ($this->store->exists($this->key)) {
throw new LockReleasingException(\sprintf('Failed to release the "%s" lock, the resource is still locked.', $this->key));
}
$this->logger?->debug('Successfully released the "{resource}" lock.', ['resource' => $this->key]);
} catch (LockReleasingException $e) {
$this->logger?->notice('Failed to release the "{resource}" lock.', ['resource' => $this->key]);
throw $e;
}
}
public function isExpired(): bool
{
return $this->key->isExpired();
}
public function getRemainingLifetime(): ?float
{
return $this->key->getRemainingLifetime();
}
}

64
vendor/symfony/lock/LockFactory.php vendored Normal file
View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
/**
* Factory provides method to create locks.
*
* @author Jérémy Derussé <jeremy@derusse.com>
* @author Hamza Amrouche <hamza.simperfit@gmail.com>
*/
class LockFactory implements LoggerAwareInterface
{
use LoggerAwareTrait;
public function __construct(
private PersistingStoreInterface $store,
) {
}
/**
* Creates a lock for the given resource.
*
* @param string $resource The resource to lock
* @param float|null $ttl Maximum expected lock duration in seconds
* @param bool $autoRelease Whether to automatically release the lock or not when the lock instance is destroyed
*
* @return SharedLockInterface
*/
public function createLock(string $resource, ?float $ttl = 300.0, bool $autoRelease = true): LockInterface
{
return $this->createLockFromKey(new Key($resource), $ttl, $autoRelease);
}
/**
* Creates a lock from the given key.
*
* @param Key $key The key containing the lock's state
* @param float|null $ttl Maximum expected lock duration in seconds
* @param bool $autoRelease Whether to automatically release the lock or not when the lock instance is destroyed
*
* @return SharedLockInterface
*/
public function createLockFromKey(Key $key, ?float $ttl = 300.0, bool $autoRelease = true): LockInterface
{
$lock = new Lock($key, $this->store, $ttl, $autoRelease);
if ($this->logger) {
$lock->setLogger($this->logger);
}
return $lock;
}
}

66
vendor/symfony/lock/LockInterface.php vendored Normal file
View File

@@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockReleasingException;
/**
* LockInterface defines an interface to manipulate the status of a lock.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface LockInterface
{
/**
* Acquires the lock. If the lock is acquired by someone else, the parameter `blocking` determines whether or not
* the call should block until the release of the lock.
*
* @throws LockConflictedException If the lock is acquired by someone else in blocking mode
* @throws LockAcquiringException If the lock cannot be acquired
*/
public function acquire(bool $blocking = false): bool;
/**
* Increase the duration of an acquired lock.
*
* @param float|null $ttl Maximum expected lock duration in seconds
*
* @return void
*
* @throws LockConflictedException If the lock is acquired by someone else
* @throws LockAcquiringException If the lock cannot be refreshed
*/
public function refresh(?float $ttl = null);
/**
* Returns whether or not the lock is acquired.
*/
public function isAcquired(): bool;
/**
* Release the lock.
*
* @return void
*
* @throws LockReleasingException If the lock cannot be released
*/
public function release();
public function isExpired(): bool;
/**
* Returns the remaining lifetime in seconds.
*/
public function getRemainingLifetime(): ?float;
}

51
vendor/symfony/lock/NoLock.php vendored Normal file
View File

@@ -0,0 +1,51 @@
<?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;
/**
* A non locking lock.
*
* This can be used to disable locking in classes
* requiring a lock.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class NoLock implements LockInterface
{
public function acquire(bool $blocking = false): bool
{
return true;
}
public function refresh(?float $ttl = null): void
{
}
public function isAcquired(): bool
{
return true;
}
public function release(): void
{
}
public function isExpired(): bool
{
return false;
}
public function getRemainingLifetime(): ?float
{
return null;
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockReleasingException;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface PersistingStoreInterface
{
/**
* Stores the resource if it's not locked by someone else.
*
* @return void
*
* @throws LockAcquiringException
* @throws LockConflictedException
*/
public function save(Key $key);
/**
* Removes a resource from the storage.
*
* @return void
*
* @throws LockReleasingException
*/
public function delete(Key $key);
/**
* Returns whether or not the resource exists in the storage.
*/
public function exists(Key $key): bool;
/**
* Extends the TTL of a resource.
*
* @param float $ttl amount of seconds to keep the lock in the store
*
* @return void
*
* @throws LockConflictedException
*/
public function putOffExpiration(Key $key, float $ttl);
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Exception\LockConflictedException;
/**
* SharedLockInterface defines an interface to manipulate the status of a shared lock.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface SharedLockInterface extends LockInterface
{
/**
* Acquires the lock for reading. If the lock is acquired by someone else in write mode, the parameter `blocking`
* determines whether or not the call should block until the release of the lock.
*
* @throws LockConflictedException If the lock is acquired by someone else in blocking mode
* @throws LockAcquiringException If the lock cannot be acquired
*/
public function acquireRead(bool $blocking = false): bool;
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock;
use Symfony\Component\Lock\Exception\LockConflictedException;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface SharedLockStoreInterface extends PersistingStoreInterface
{
/**
* Stores the resource if it's not locked for reading by someone else.
*
* @return void
*
* @throws LockConflictedException
*/
public function saveRead(Key $key);
}

View 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;
}
}

View 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();
}
}
}

View 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();
}
}

View 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,
};
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Store;
use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Key;
trait ExpiringStoreTrait
{
private function checkNotExpired(Key $key): 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
vendor/symfony/lock/Store/FlockStore.php vendored Normal file
View 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 doesnt 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__);
}
}

View 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__);
}
}

View 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];
}
}

View 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
vendor/symfony/lock/Store/PdoStore.php vendored Normal file
View 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,
};
}
}

View 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
vendor/symfony/lock/Store/RedisStore.php vendored Normal file
View 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)
';
}
}

View 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__);
}
}

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

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

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Strategy;
/**
* ConsensusStrategy is a StrategyInterface implementation where strictly more than 50% items should be successful.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class ConsensusStrategy implements StrategyInterface
{
public function isMet(int $numberOfSuccess, int $numberOfItems): bool
{
return $numberOfSuccess > ($numberOfItems / 2);
}
public function canBeMet(int $numberOfFailure, int $numberOfItems): bool
{
return $numberOfFailure < ($numberOfItems / 2);
}
}

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Strategy;
/**
* StrategyInterface defines an interface to indicate when a quorum is met and can be met.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
interface StrategyInterface
{
/**
* Returns whether or not the quorum is met.
*/
public function isMet(int $numberOfSuccess, int $numberOfItems): bool;
/**
* Returns whether or not the quorum *could* be met.
*
* This method does not mean the quorum *would* be met for sure, but can be useful to stop a process early when you
* known there is no chance to meet the quorum.
*/
public function canBeMet(int $numberOfFailure, int $numberOfItems): bool;
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Lock\Strategy;
/**
* UnanimousStrategy is a StrategyInterface implementation where 100% of elements should be successful.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class UnanimousStrategy implements StrategyInterface
{
public function isMet(int $numberOfSuccess, int $numberOfItems): bool
{
return $numberOfSuccess === $numberOfItems;
}
public function canBeMet(int $numberOfFailure, int $numberOfItems): bool
{
return 0 === $numberOfFailure;
}
}