Skip to content

[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

Open
wants to merge 15 commits into
base: 7.4
Choose a base branch
from

Conversation

alexander-schranz
Copy link
Contributor

@alexander-schranz alexander-schranz commented Dec 12, 2024

Q A
Branch? 7.3
Bug fix? no
New feature? yes
Deprecations? no
Issues Fix #...
License MIT

I really liked the idea of a semaphore component but not have the possibility to use it without Redis it did not get a lot a drive and usage in our projects and we workaround using Locks.

So this idea did come up to have a Semaphore store which can use the existing lock component for it.

@lyrixx @jderusse

@alexander-schranz alexander-schranz marked this pull request as ready for review December 12, 2024 18:29
@carsonbot carsonbot added this to the 7.3 milestone Dec 12, 2024
@alexander-schranz alexander-schranz force-pushed the feature/semphore-store-based-on-locks branch 3 times, most recently from 6921d4f to 38cb65b Compare December 12, 2024 18:50
@alexander-schranz alexander-schranz force-pushed the feature/semphore-store-based-on-locks branch from 1fef9d7 to 84e0607 Compare January 6, 2025 12:45
@symfony symfony deleted a comment from carsonbot Jan 8, 2025
@alexander-schranz
Copy link
Contributor Author

@jderusse if you have some free time maybe you can have a relook at the changes we did since your last review as you are I think most familiar with semaphore component.

@alexander-schranz
Copy link
Contributor Author

@jderusse what I'm stuck of how we can make the framework bundle integration for this.

@@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really want to allow those old versions here?

Copy link
Contributor Author

@alexander-schranz alexander-schranz May 26, 2025

Choose a reason for hiding this comment

The 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.

@Toflar
Copy link
Contributor

Toflar commented May 13, 2025

I really like this proposal! Is @jderusse's feedback to #59202 (comment) the only blocker?

@fabpot fabpot modified the milestones: 7.3, 7.4 May 26, 2025
@alexander-schranz
Copy link
Contributor Author

Yes would be nice to get some feedback from @jderusse specially not sure how we can create the Store in the Framework integration where the Semaphore Store is created via the createStore method which sadly is currently a static method here:

public static function createStore(#[\SensitiveParameter] object|string $connection): PersistingStoreInterface

And the new Store lock:// requires the LockFactory to be injected. The idea was something like:

framework:
    lock: '%env(LOCK_DSN)%'
    semaphore: 'lock://'

but doesn't work aslong as the factories are static methods.

private function createLocks(Key $key, float $ttlInSecond): array
{
$locks = [];
$lockName = base64_encode($key->__toString());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why encoding the key?

Copy link
Contributor Author

@alexander-schranz alexander-schranz Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think avoid problems with special chars as not all chars are supported for locks. The RedisStore seems also use base64_encode for its token.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not all chars are supported for locks

humm, I think this assumption is wrong. Yes, some stores need to convert the key for their own reason (performance, security (file system), readability, ...), But the lock interface accepts any key (opposite to Cache)

And here we are presuming that the implementation needs to "sanitize" the key, so I believe the conversion is not the responsibility of the semaphore and should not be there.

for ($i = 0; $i < $limit; ++$i) {
$index = ($startPoint + $i) % $limit;

$lock = $this->lockFactory->createLock($lockName.'_'.$index, $ttlInSecond, false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you disable the auto-release feature, you should wrap this function in a try/catch to release locks in case of an exception (ie. network error)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about the whole try catch handling can you have a look at it if you find a more readable way maybe?

private function releaseLocks(array $locks, Key $key): void
{
foreach ($locks as $lock) {
$lock->release();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrap in a try/catch in case one release fails?

Copy link
Contributor Author

@alexander-schranz alexander-schranz Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still throw an exception?

If yes which exception?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock::release could throw a LockReleasingException

Copy link
Contributor Author

@alexander-schranz alexander-schranz Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean what should we do with the catched exceptions?

Throw the first appeared exception again after all locks released:

$exceptions = [];
foreach ($locks as $lock) {
    try {
        $lock->release();
    } catch (LockReleasingException $e) {
        $exceptions[] = $e;
    }
    
    if ([] !== $exceptions) {
        throw new $exceptions[0]; // ??
    }
    
    // ...
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes proabably the first one (or if you want combine all exception in a new object, but I think it's overkill)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jderusse implemented but as in the previous comment not 100% sure how to make it better readable


$locks = $this->createLocks($key, $ttlInSecond);

$key->setState(__CLASS__, $locks);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might need to implement the markUnserializable (like we do for LOck) to prevent people from serializing such key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean this part:

public function __sleep(): array
{
if (!$this->serializable) {
throw new UnserializableKeyException('The key cannot be serialized.');
}
return ['resource', 'expiringTime', 'state'];
}
?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implemented

{
$locks = $this->getExistingLocks($key);

if (\count($locks) === $key->getWeight()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we use === or >= ? I know the createLock method stops when the counters match, and this code is enough.
But what if by "chance" we acquire MORE lock than necessary? ie. for whatever reason, the value in getWeight is dynamic.

IMHO, it doesn't hurt readability to use if (\count($locks) >= $key->getWeight()) (here and bellow,) but it makes the code stronger.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implemented

@alexander-schranz alexander-schranz force-pushed the feature/semphore-store-based-on-locks branch from 2f035bc to 9e2fc0e Compare August 21, 2025 07:09
Co-Authored-By: jeremy@derusse.com<Jérémy Derussé>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants