diff --git a/src/Symfony/Component/Semaphore/Store/LockStore.php b/src/Symfony/Component/Semaphore/Store/LockStore.php new file mode 100644 index 0000000000000..5cc77ff7ca1b2 --- /dev/null +++ b/src/Symfony/Component/Semaphore/Store/LockStore.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Semaphore\Store; + +use Symfony\Component\Lock\LockFactory; +use Symfony\Component\Lock\LockInterface; +use Symfony\Component\Semaphore\Exception\SemaphoreAcquiringException; +use Symfony\Component\Semaphore\Exception\SemaphoreExpiredException; +use Symfony\Component\Semaphore\Key; +use Symfony\Component\Semaphore\PersistingStoreInterface; + +/** + * @author Alexander Schranz + */ +final class LockStore implements PersistingStoreInterface +{ + public function __construct( + private readonly LockFactory $lockFactory, + ) { + } + + public function save(Key $key, float $ttlInSecond): void + { + $locks = $this->getExistingLocks($key); + + if ([] !== $locks) { + return; + } + + $locks = $this->createLocks($key, $ttlInSecond); + + $key->setState(__CLASS__, $locks); + } + + public function delete(Key $key): void + { + $this->releaseLocks($this->getExistingLocks($key), $key); + } + + public function exists(Key $key): bool + { + $locks = $this->getExistingLocks($key); + + if (\count($locks) === $key->getWeight()) { + return true; + } + + $this->releaseLocks($locks, $key); + + return false; + } + + public function putOffExpiration(Key $key, float $ttlInSecond): void + { + $locks = $this->getExistingLocks($key); + foreach ($locks as $lock) { + if ($lock->isExpired()) { + $this->releaseLocks($locks, $key); + + throw new SemaphoreExpiredException($key, 'One or multiple locks are already expired.'); + } + + $lock->refresh($ttlInSecond); + } + + if (\count($locks) !== $key->getWeight()) { + $this->releaseLocks($locks, $key); + + throw new SemaphoreExpiredException($key, 'One or multiple locks were not even acquired.'); + } + } + + /** + * @param array $locks + */ + private function releaseLocks(array $locks, Key $key): void + { + foreach ($locks as $lock) { + $lock->release(); + } + + $key->setState(__CLASS__, null); + } + + /** + * @return array + */ + private function getExistingLocks(Key $key): array + { + if ($key->hasState(__CLASS__)) { + return $key->getState(__CLASS__); + } + + return []; + } + + /** + * @return array + */ + private function createLocks(Key $key, float $ttlInSecond): array + { + $locks = []; + $lockName = base64_encode($key->__toString()); + $limit = $key->getLimit(); + + // use a random start point to have a higher chance to catch a free slot directly + $startPoint = rand(0, $limit - 1); + + for ($i = 0; $i < $limit; ++$i) { + $index = ($startPoint + $i) % $limit; + + $lock = $this->lockFactory->createLock($lockName.'_'.$index, $ttlInSecond, false); + if ($lock->acquire(false)) { // use lock if we can acquire it else try to catch next lock + $locks[] = $lock; + + if (\count($locks) === $key->getWeight()) { + break; + } + + continue; + } + + $acquired = \count($locks); + $required = $key->getWeight(); + $remaining = $key->getLimit() - $i; + if (($acquired + $remaining) < $required) { // no chance to get enough locks + break; + } + } + + if (\count($locks) !== $key->getWeight()) { + // release already acquired locks because not got the amount of locks which were required + $this->releaseLocks($locks, $key); + + throw new SemaphoreAcquiringException($key, 'There were no free locks found'); + } + + return $locks; + } +} diff --git a/src/Symfony/Component/Semaphore/Tests/Store/AbstractStoreTestCase.php b/src/Symfony/Component/Semaphore/Tests/Store/AbstractStoreTestCase.php index 4cd89458f5d92..aafd59adf93bd 100644 --- a/src/Symfony/Component/Semaphore/Tests/Store/AbstractStoreTestCase.php +++ b/src/Symfony/Component/Semaphore/Tests/Store/AbstractStoreTestCase.php @@ -204,7 +204,6 @@ public function testPutOffExpirationWhenSaveHasNotBeenCalled() $key1 = new Key(__METHOD__, 4, 2); $this->expectException(SemaphoreExpiredException::class); - $this->expectExceptionMessage('The semaphore "Symfony\Component\Semaphore\Tests\Store\AbstractStoreTestCase::testPutOffExpirationWhenSaveHasNotBeenCalled" has expired: the script returns a positive number.'); $store->putOffExpiration($key1, 20); } diff --git a/src/Symfony/Component/Semaphore/Tests/Store/LockStoreTest.php b/src/Symfony/Component/Semaphore/Tests/Store/LockStoreTest.php new file mode 100644 index 0000000000000..f84ecfafd6870 --- /dev/null +++ b/src/Symfony/Component/Semaphore/Tests/Store/LockStoreTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Semaphore\Tests\Store; + +use Symfony\Component\Lock\LockFactory; +use Symfony\Component\Lock\Store\FlockStore; +use Symfony\Component\Semaphore\PersistingStoreInterface; +use Symfony\Component\Semaphore\Store\LockStore; + +/** + * @author Alexander Schranz + */ +class LockStoreTest extends AbstractStoreTestCase +{ + public function getStore(): PersistingStoreInterface + { + $lock = new FlockStore(); + $factory = new LockFactory($lock); + + return new LockStore($factory); + } +} diff --git a/src/Symfony/Component/Semaphore/composer.json b/src/Symfony/Component/Semaphore/composer.json index a620c60cca25a..6c736a6e466c3 100644 --- a/src/Symfony/Component/Semaphore/composer.json +++ b/src/Symfony/Component/Semaphore/composer.json @@ -24,7 +24,8 @@ "psr/log": "^1|^2|^3" }, "require-dev": { - "predis/predis": "^1.1|^2.0" + "predis/predis": "^1.1|^2.0", + "symfony/lock": "^5.4 || ^6.0 || ^7.0" }, "conflict": { "symfony/cache": "<6.4" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy