-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
629b74e
7371d7c
ca585f7
4f7ca20
79c5b84
f4a3cb1
2d18f6c
84e0607
50b835e
387f6b3
2e1159c
6a3c484
92a1d0b
2f035bc
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,149 @@ | ||
<?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\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); | ||
} | ||
|
||
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<LockInterface> $locks | ||
*/ | ||
private function releaseLocks(array $locks, Key $key): void | ||
{ | ||
foreach ($locks as $lock) { | ||
$lock->release(); | ||
} | ||
|
||
$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 = 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; | ||
} | ||
} |
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.