-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[Semaphore] Add a semaphore store based on locks #59202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 7.4
Are you sure you want to change the base?
Changes from all commits
d1345a4
bc4480d
858dee3
975e96f
9201e55
6d42b79
80e652c
ed794b6
f3201bd
dfd6449
9f74f27
a4bfd5c
1660b64
9e2fc0e
05729d1
a54590e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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\Semaphore\Exception; | ||
|
||
use Symfony\Component\Lock\Exception\ExceptionInterface; | ||
|
||
/** | ||
* UnserializableKeyException is thrown when the key contains state that can no | ||
* be serialized and the user try to serialize it. | ||
* ie. Semaphore with lock states, ... | ||
* | ||
* @author Alexander Schranz <alexander@sulu.io> | ||
*/ | ||
class UnserializableKeyException extends \RuntimeException implements ExceptionInterface | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
<?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\Semaphore\Store; | ||
|
||
use Symfony\Component\Lock\Exception\LockReleasingException; | ||
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 <alexander@sulu.io> | ||
*/ | ||
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); | ||
$key->markUnserializable(); | ||
} | ||
|
||
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.'); | ||
jderusse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
$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<LockInterface> $locks | ||
*/ | ||
private function releaseLocks(array $locks, Key $key): void | ||
{ | ||
$lockReleasingException = null; | ||
foreach ($locks as $lock) { | ||
try { | ||
$lock->release(); | ||
} catch (LockReleasingException $e) { // we still need to release all other locks before throw the exception | ||
$lockReleasingException ??= $e; | ||
} | ||
} | ||
|
||
if (null !== $lockReleasingException) { | ||
throw $lockReleasingException; | ||
} | ||
|
||
$key->setState(__CLASS__, null); | ||
} | ||
|
||
/** | ||
* @return array<LockInterface> | ||
*/ | ||
private function getExistingLocks(Key $key): array | ||
{ | ||
if ($key->hasState(__CLASS__)) { | ||
return $key->getState(__CLASS__); | ||
} | ||
|
||
return []; | ||
} | ||
|
||
/** | ||
* @return array<LockInterface> | ||
*/ | ||
private function createLocks(Key $key, float $ttlInSecond): array | ||
{ | ||
$locks = []; | ||
$lockName = $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); | ||
|
||
$previousException = null; | ||
try { | ||
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; | ||
} | ||
} | ||
} catch (\Throwable $e) { | ||
$previousException = $e; | ||
} | ||
|
||
if (\count($locks) !== $key->getWeight()) { | ||
try { | ||
// release already acquired locks because not got the amount of locks which were required | ||
$this->releaseLocks($locks, $key); | ||
} catch (\Throwable $e) { | ||
// we throw the previous exception or own SemaphoreAcquiringException here | ||
} | ||
|
||
$previousException ??= new SemaphoreAcquiringException($key, 'There were no free locks found'); | ||
|
||
throw $previousException; | ||
} | ||
|
||
return $locks; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?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\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 <alexander@sulu.io> | ||
*/ | ||
class LockStoreTest extends AbstractStoreTestCase | ||
{ | ||
public function getStore(): PersistingStoreInterface | ||
{ | ||
$lock = new FlockStore(); | ||
$factory = new LockFactory($lock); | ||
|
||
return new LockStore($factory); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we really want to allow those old versions here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can change to everything the core team wants. Currently it works on this versions and I started with the 5.4 as its still supported LTS until 2029. |
||
}, | ||
"conflict": { | ||
"symfony/cache": "<6.4" | ||
|
Uh oh!
There was an error while loading. Please reload this page.