-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Lock] Create a lock component #21093
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
Conversation
When I worked on the Filesystem/LockHandler we (fab, niko, me) decided to not implement a "lock component" because it was always very specific to each use case: one vs multiple server. lock in redis with a ttl, Redlock, etc. May be it's time to reconsider this and to introduce in symfony a new lock component. Anyway, about the end user API: I saw you arguments about More over, I would prefer a system based on |
Since you said the component is already full of Lock names, what about call the component |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor points, architectural I think it looks fine. I'm not too experienced with locking itself, but this component is similar an in-house Mutex component my company wrote, thus I do like it a lot.
src/Symfony/Component/Lock/Lock.php
Outdated
throw $e; | ||
} catch (\Exception $e) { | ||
throw new LockAcquiringException('', 0, $e); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't you be catching \Throwable
as well?
src/Symfony/Component/Lock/Lock.php
Outdated
throw $e; | ||
} catch (\Exception $e) { | ||
throw new LockAcquiringException('', 0, $e); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't you be catching \Throwable
as well?
Sorry, something went wrong.
All reactions
-
👍 1 reaction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on Error, it should let the error throw imho. throwing a LockAcquiringException would make no sense. Error is a dev mistake, not a runtime one.
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, I wanted to make sure this was considered
Sorry, something went wrong.
All reactions
src/Symfony/Component/Lock/Lock.php
Outdated
} else { | ||
throw new InvalidArgumentException( | ||
sprintf('Unable to acquire a blocking lock on a instance of "%s".', get_class($this->store)) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should go on 1 line
Sorry, something went wrong.
All reactions
-
👍 2 reactions
* | ||
* @author Jérémy Derussé <jeremy@derusse.com> | ||
*/ | ||
class CombinedStore implements StoreInterface, ExpirableStoreInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably be made final
Sorry, something went wrong.
All reactions
* | ||
* @author Jérémy Derussé <jeremy@derusse.com> | ||
*/ | ||
class UnanimousQuorum implements QuorumInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably be made final
Sorry, something went wrong.
All reactions
src/Symfony/Component/Lock/Lock.php
Outdated
use Symfony\Component\Lock\Store\ExpirableStoreInterface; | ||
|
||
/** | ||
* Lock is the default implementation to the LockInterface. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
implementation to of the LockInterface
Sorry, something went wrong.
All reactions
{ | ||
/** | ||
* Acquire the lock. If the lock is acquired by someone else, the parameter `blocking` determine whether or not | ||
* the the call should block until the release of the lock. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the parameter blocking
determines whether
Sorry, something went wrong.
All reactions
* Acquire the lock. If the lock is acquired by someone else, the parameter `blocking` determine whether or not | ||
* the the call should block until the release of the lock. | ||
* | ||
* @param bool $blocking whether or not the Lock should wait the release of someone else |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should wait for the release of 8000 p>
Sorry, something went wrong.
All reactions
* Returns Whether or not the quorum *could* be met. | ||
* This method does not means the quorum *would* be met for sur. But can be useful to early stop a process when you | ||
* known there isn't any chance to met the quorum. | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there should be a blank line between the main and sub description
This method does not means the quorum *would* be met for sur. But sure, but can be useful to early stop a process early when you known there isn't any is no chance to meet the quorum.
Sorry, something went wrong.
All reactions
interface BlockingStoreInterface | ||
{ | ||
/** | ||
* Wait a key became free then stores the resource. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait a key became becomes free**,** then stores the resource.
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree on the finals. Im not into locking myself as well.. at first it looked slightly over-engineered.. then again i could understand we're super flexible here in terms of supporting various complex strategies.
I think i'd prefer true/false
API as well.
And not sure.. but cant we do closures somehow to streamline the process?
Sorry, something went wrong.
All reactions
public function __construct(StoreInterface $decorated, $retrySleep = 50, $retryCount = PHP_INT_MAX) | ||
{ | ||
$this->decorated = $decorated; | ||
$this->retryCount = $retryCount; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps use $retryCount ?: PHP_INT_MAX
and go 0
in the cons
9E88
truct. To keep up with the promise like @iltar mentioned 👍
Sorry, something went wrong.
All reactions
Regarding the naming of the methods, I've been checking the practices of other languages: Python: lock = Lock()
lock.acquire() # waits if lock is already held
lock.acquire(False) # doesn't wait if lock is already held
lock.release()
if lock.locked() Ruby: lock = Mutex.new
lock.lock # waits if lock is already held
lock.try_lock # doesn't wait if lock is already held
lock.unlock
if lock.locked? PHP pthreads extension $lock = Mutex::create();
Mutex::lock($lock); // waits if lock is already held
Mutex::trylock($lock); // doesn't wait if lock is already held
Mutex::unlock($lock);
// no method to check if the lock is locked Java: Lock lock = ...;
lock.lock(); // waits if lock is already held
lock.tryLock(); // doesn't wait if lock is already held
lock.unlock();
// no method to check if the lock is locked It looks like the most common choice is: |
All reactions
-
👍 7 reactions
Sorry, something went wrong.
*/ | ||
public function generateToken() | ||
{ | ||
return base64_encode(random_bytes($this->entropy / 8)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest inlining this and remove RandomTokenGenerator
and TokenGeneratorInterface
: they are just internal details.
Sorry, something went wrong.
All reactions
*/ | ||
public function createLock($resource) | ||
{ | ||
return new Lock(new Key($resource), $this->store); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about removing the factory and move createLock to the store interface?
Sorry, something went wrong.
All reactions
); | ||
|
||
// Silence error reporting | ||
set_error_handler(function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
single line
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed
Sorry, something went wrong.
All reactions
return; | ||
} | ||
|
||
$fileName = sprintf( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
single line
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there 10000 a limit to the line length in CS?
Sorry, something went wrong.
All reactions
API choices: IMO lock and unlock fit well when the subject is the resource. ie. I like the split in lock/tryLock, I think I'll apply this change unless someone prefer the $blocking flag? About the return true/false vs \Exception. Personally, I see it like "lock me the resource right now" instead of "can you please lock the resource". If the lock is not acquired, it means sometinhg goes wrong, that's why I excpect an exception. |
All reactions
-
👍 2 reactions
Sorry, something went wrong.
src/Symfony/Component/Lock/Key.php
Outdated
$this->resource = (string) $resource; | ||
} | ||
|
||
public function getResource() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
__toString()
?
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea
Sorry, something went wrong.
All reactions
src/Symfony/Component/Lock/Key.php
Outdated
} | ||
|
||
/** | ||
* @return mixed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be removed as it does not say anything useful
Sorry, something went wrong.
All reactions
src/Symfony/Component/Lock/Lock.php
Outdated
$this->store->expire($this->key, $ttl); | ||
$this->logger->info('Expiration defined for lock "{resource}" for "{ttl}" seconds', array('resource' => $this->key->getResource(), 'ttl' => $ttl)); | ||
} catch (LockConflictedException $e) { | ||
$this->logger->info('Fail to define a expiration for the lock "{resource}". Someone else acquired the lock', array('resource' => $this->key->getResource())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
an expiration
, same below.
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use a single sentence here instead of 2.
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I don't see how to merge this two sentences without removing a part (which are both important IMO) :(
Failed to define an expiration for the "{resource}" lock because someone else acquired it
?
Sorry, something went wrong.
All reactions
src/Symfony/Component/Lock/Lock.php
Outdated
$this->logger->info('Fail to define a expiration for the lock "{resource}". Someone else acquired the lock', array('resource' => $this->key->getResource())); | ||
throw $e; | ||
} catch (\Exception $e) { | ||
$this->logger->info('Fail to define a expiration for the lock "{resource}"', array('resource' => $this->key->getResource(), 'exception' => $e)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the {resource} lock.
Sorry, something went wrong.
All reactions
interface LockInterface | ||
{ | ||
/** | ||
* Acquire the lock. If the lock is acquired by someone else, the parameter `blocking` determines whether or not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acquires, same everywhere else.
Sorry, something went wrong.
All reactions
interface QuorumInterface | ||
{ | ||
/** | ||
* Returns Whether or not the quorum is met. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extra cap (same below)
Sorry, something went wrong.
All reactions
); | ||
|
||
// Silence error reporting | ||
set_error_handler(function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed
Sorry, something went wrong.
All reactions
|
||
/** | ||
* RetryTillSaveStore is a StoreInterface implementation which decorate a non blocking StoreInterface to provide a | ||
* kind of blocking storage. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"a kind of" should probably be removed.
Sorry, something went wrong.
All reactions
public function exists(Key $key); | ||
|
||
/** | ||
* Retrieves a lock for the given resource. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It retrieves or it creates?
Sorry, something went wrong.
All reactions
src/Symfony/Component/Lock/Lock.php
Outdated
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function acquire($blocking = false, $ttl = 300.0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about moving the default ttl
value to the constructor instead?
Sorry, something went wrong.
All reactions
-
👍 1 reaction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The purpose of this parameter was to let the user choose a specific duration for one call.
Given the easiest (and probably the most used) workflow would be to use the Store
to create the Lock
, it does not make sens to move this parameter in the __constructor: If we want a fixed ttl value, we can just remove it and let the store use what he want.
I'll replace the default value 300.0
by a null
. It let the user to override the default store configuration.
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finally move the parameter in the constructor (as suggested) and remove it from the refresh method too (because this method serves the same purpose).
Sorry, something went wrong.
All reactions
src/Symfony/Component/Lock/Lock.php
Outdated
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function refresh($ttl = 300.0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would make the ttl
argument required, without a default value.
Sorry, something went wrong.
All reactions
-
👍 1 reaction
|
||
return; | ||
} catch (LockConflictedException $e) { | ||
usleep($this->retrySleep * 1000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should add randomness to avoid eternal collision
Sorry, something went wrong.
All reactions
namespace Symfony\Component\Lock\Exception; | ||
|
||
/** | ||
* InvalidArgumentException is thrown when a service had been badly initialized. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it has a specific usage, the name should reflect it.
Sorry, something went wrong.
All reactions
Pushed a new commit to remove the 2 interfaces At first it was a god thing to split it 3 interfaces to separate responsibilities, but I realize that it was a pain to decorate the stores because we both need to implement every interfaces and are not sure that the decorated store will implement every interfaces. In this case, the decorator don't know what to do. |
All reactions
Sorry, something went wrong.
@jderusse you need to also add the new component in the |
All reactions
Sorry, something went wrong.
src/Symfony/Component/Lock/LICENSE
Outdated
@@ -0,0 +1,19 @@ | |||
Copyright (c) 2016 Fabien Potencier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be updated to 2016-2017
I suppose
Sorry, something went wrong.
All reactions
816523b
to
f5b20bd
Compare
src/Symfony/Component/Lock/Lock.php
Outdated
* | ||
* @author Jérémy Derussé <jeremy@derusse.com> | ||
*/ | ||
final class Lock implements LockInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this not implement LoggerAwareInterface
as well?
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
Sorry, something went wrong.
All reactions
*/ | ||
public function createLock($resource, $ttl = 300.0) | ||
{ | ||
return new Lock(new Key($resource), $this, $ttl); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
making stores create the lock by passing $this
to the Lock instance makes it very hard to use composition because the Lock instance must always be created by the outside store in this case instead of delegating the call to createLock
(otherwise, the lock has the wrong store, as it ends up calling directly the inner store).
IMO, the Store should not be the lock factory at the same time. Mixing these responsibilities is what makes things hard.
We should either have a separate factory (in which the store is injected, and so decoration works fine as the factory would get the outside store) or let the instantiation be done in userland when using the library.
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was the first implementation (I like the separation of responsibility too).
But, as suggested by @nicolas-grekas in this comment I move the factory into the StoreInterface and that's why I create the AbstractStore to avoid code duplication (which I don't really like too).
The issue with the factory is was an empty class like without real value:
class StoreFactory
{
private $store;
public function __construct(StoreInterface $store)
{
$this->store = $store;
}
public function createLock($resource, $ttl = 300.0)
{
return new Lock(new Key($resource), $this->lock, $ttl);
}
}
And IMO, the outer store may create the lock instead of delegating the creation to the inner store. The abstractStore do the job (maybe a trait could be a better solution than the abstraction)
What do you think ?
(just to be sure, you are talking about decoration instead of composition ?)
Sorry, something went wrong.
All reactions
try { | ||
$store->delete($key); | ||
} catch (\Exception $e) { | ||
} catch (\Throwable $e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
catching all exceptions silently would be a debugging nightmare
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, but with a notice level given the deletion failure may not be an issue
Sorry, something went wrong.
All reactions
$store->putOffExpiration($key, $ttl); | ||
++$successCount; | ||
} catch (\Exception $e) { | ||
++$failureCount; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we are loosing all exceptions messages here, making debugging really hard
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
Sorry, something went wrong.
All reactions
|
||
/** | ||
* Tests blocking locks thanks to pcntl. | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing @requires extension pcntl
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't aware of this feature. Thanks. I also annotate \Redis tests
Sorry, something went wrong.
All reactions
$drift = 20000; | ||
|
||
if (PHP_VERSION_ID < 50600 || defined('HHVM_VERSION_ID')) { | ||
$this->markTestSkipped('Php 5.5 does not keep resource in child forks'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
message is wrong in the case of HHVM
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
Sorry, something went wrong.
All reactions
|
||
public function getStore() | ||
{ | ||
$redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if there is no such env variable ?
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part of the test is mostly a copy/paste of the RedisAdapterTest from the Cache component.
The variable is define is the phpunit.xml
file and should exists.
I don't think it worst it to check whether or not someone destroy the config file and expect a successful result.
Sorry, something went wrong.
All reactions
I think a little explanation about clock drift (which is introduced) could be a plus, a link in code comment, or in the docs to automatically answer about the "why" when people look at the code could be useful:) |
All reactions
Sorry, something went wrong.
@Prophet777 You're absolutely right, but this PR don't yet introduce a clock drift imlplementation. I'll do it in a next PR or in a other PR |
All reactions
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My 2 (literally) cents.
Sorry, something went wrong.
All reactions
*/ | ||
public function putOffExpiration(Key $key, $ttl) | ||
{ | ||
// do nothing, the flock locks forever. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Invalid comment, probably copy-pasted.
Sorry, something went wrong.
All reactions
} | ||
|
||
// Have to be a \Predis\Client | ||
return call_user_func_array(array($this->redis, 'eval'), array_merge(array($script, 1, $resource), $args)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know that constructor allows only some classes and most of them are used above, but wouldn't be more readable to check instance of \Predis\Client
too? After all if
statements exception could be thrown that script could not be evaluated.
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, something went wrong.
All reactions
Probably I'm late, but in the past I've worked with an almost identical lock model, and it almost never worked. If the process responsible for releasing the lock dies, most of the time the lock needs to be released manually (deleting the lock file or the redis record). The timeout mitigates the problem but does not solve it. |
All reactions
Sorry, something went wrong.
{ | ||
$this->store->delete($this->key); | ||
|
||
if ($this->store->exists($this->key)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a concurrent environment, another process might have created the same lock here. exists
will return true even if the previous delete
was successful.
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the lock had been acquired by an other instance of key
, the store will treat them like 2 different keys and will return false.
Sorry, something went wrong.
All reactions
-
👍 1 reaction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, i got it. there is a "token" in between, that is always random, for the same key
you have different "tokens"
Sorry, something went wrong.
All reactions
* @throws LockConflictedException If the lock is acquired by someone else in blocking mode | ||
* @throws LockAcquiringException If the lock can not be acquired | ||
*/ | ||
public function acquire($blocking = false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid concurrency issues this should return a unique reference for the lock, so the release should work by using this reference
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This unique reference is in the key. By passing the key to the methods save/delete/exists the concurrency is already handled
Sorry, something went wrong.
All reactions
-
👍 1 reaction
What about adding a MysqlStore using GET_LOCK() / RELEASE_LOCK() / IS_FREE_LOCK() functions ? It's robust and available in all MySQL versions. |
All reactions
Sorry, something went wrong.
@goetas Then I've got a good news to you, the actual implementation does not suffer for such issue. Thanks to @jocel1 I didn't dig into MySQL store, but it's on my todo list. Do you want to create that PR? |
All reactions
Sorry, something went wrong.
@jocel1 In mysql there is a fundamental concept that PHP has not, the connection. As the documentation says:
At the end of the connection, the lock will be automatically released. |
All reactions
Sorry, something went wrong.
@goetas yup that's why it also great to easily handle the dying script case: there's nothing specific to do :) |
All reactions
Sorry, something went wrong.
*/ | ||
public function __construct(\Memcached $memcached, $initialTtl = 300) | ||
{ | ||
if (!static::isSupported()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it possible to have an object instance of \Memcached
and not having the memcached
extension loaded?
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, something went wrong.
All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's technically possible that someone implement a \Memcached
class.
If the class cover the original extension behaviors and is compatible, everything'll be fine.
If not, that's a weird case and should be addressed IMO.
Sorry, something went wrong.
All reactions
@jderusse Can you point me what guarantees that |
All reactions
Sorry, something went wrong.
I have tested the With Flock, if the scripts terminates anomaly, the lock gets released by the kernel, but for redis this does not happen. I guess that memcache suffers from the same problem. $predis = new \Predis\Client('tcp://localhost:6379');
$store = new \Symfony\Component\Lock\Store\RedisStore($predis);
$factory = new \Symfony\Component\Lock\Factory($store);
$lock = $factory->createLock('foo');
if ($lock->acquire()) {
if (!rand(0, 1)) exit; // simulating failure
$lock->release();
} After the failure occurs, the only way to unlock the lock, is to remove manually the entry from redis. |
All reactions
Sorry, something went wrong.
@goetas None. But I can guarantee that Memcache will delete the key bat it own, when the TTL will expire. @jocel1 it could be an issue to me. What if the connection closes while you think you are in lock phase (network issue, or mysql read timeout, ...) |
All reactions
Sorry, something went wrong.
i knew this, the standard TTL that many key-value store have |
All reactions
Sorry, something went wrong.
I suggest to read the (not yet merged) documentation about this https://github.com/symfony/symfony-docs/pull/7364/files |
All reactions
-
👍 1 reaction
Sorry, something went wrong.
@jderusse thanks for the explanation. The documentation is clear and highlights the differences between different stores (advantages and disadvantages). At the beginning I was thinking that the all the stores implementations have to fulfill all the requirements (event when hardy possible) |
All reactions
Sorry, something went wrong.
Mysql and postgres locks can be interesting to implement since they offer both advantages of local and remote locks :) |
All reactions
Sorry, something went wrong.
return false; | ||
} catch (\Exception $e) { | ||
$this->logger->warning('Failed to lock the "{resource}".', array('resource' => $this->key, 'exception' => $e)); | ||
throw new LockAcquiringException('', 0, $e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jderusse I suggest using a non-empty exception message for better DX
Sorry, something went wrong.
All reactions
-
👍 1 reaction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed..
Sorry, something went wrong.
All reactions
If the connection closes on the locker side, it could indeed be problematic if you're not making subsequent requests on the db (in this case, the subsequent query would fail as well). But I would expect this to occur far less often than a TTL timeout or even an eviction prior to key expiry on memcached. |
All reactions
Sorry, something went wrong.
Can we eliminate the memcached store? Memcached is notorious for evicting keys that aren't even to their exptime because they are LRU than something else in the same slab class. By having it, developers will trust it more than they should. It's great as a cache, but really shouldn't be used as a persistent concurrent locking mechanism. Thoughts? |
All reactions
Sorry, something went wrong.
Hi @patrick-mcdougle, can you please open an issue in order to follow this point? |
All reactions
Sorry, something went wrong.
This PR was merged into the master branch. Discussion ---------- Add docs for the Lock component See symfony/symfony#21093 Commits ------- 3035fe9 Rename quorum into strategy d4326c0 Reduce usage of "you" d6a218c Rename quorum strategy 17248e1 Remove codeblock 6b6d865 Final review and some minor rewords/simplifications 2498ccd Fix typo d1d3b71 Fixed minor typos and syntax issues 60bfe03 Fix typo 6469016 Add the Lock Component
…ry (corphi) This PR was merged into the 3.4 branch. Discussion ---------- [Lock][PhpDoc] There is no attempt to create the directory | Q | A | ------------- | --- | Branch? | 3.4 | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - When the lock component [was](#21093) [created](#22597), the `FlockStore` implementation was copied from the filesystem component. But the dependency to its origin was dropped, and as part of that, the attempt to create the parent directory for the locks. The PhpDoc wasn’t updated to reflect this change. Commits ------- 93a9bd3 PhpDoc: There is no attempt to create the directory
fabpot
pierredup
nicolas-grekas
stof
Wirone
goetas
ro0NL
linaori
thewilkybarkid
HeahDude
Successfully merging this pull request may close these issues.
[HttpFoundation] Session handlers should use a lock
This PR aim to add a new component Lock going further than the FileSystem\LockHandler by allowing remote backend (like Redis, memcache, etc)
Inspired by
Usage
The simplest way to use lock is to inject an instance of a Lock in your service
Configured with something like
If you need to lock serveral resource on runtime, wou'll nneed to inject the LockFactory.
Configured with something like
This component allow you to refresh an expirable lock.
This is usefull, if you run a long operation split in several small parts.
If you lock with a ttl for the overall operatoin time and your process crash, the lock will block everybody for the defined TTL.
But thank to the refresh method, you're able to lock for a small TTL, and refresh it between each parts.
Naming anc implementation choise
Choose to use acquire, because this component is full of
lock
Symfony\Component\Lock\Lock::Lock` raised a E_TOO_MANY_LOCK in my head.Not a big fan of flag feature and 2. But I choose to use the blocking flag to offer a simple (and common usecase) implementation
I choose to a the pool of locks implementation. It allow the user to create 2 instances and use cross lock even in the same process.
I choose to use a Interface even if there is only one implementaiton to offer an extension point here
TODO
In this PR
In other PR