Skip to content

[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

Closed
wants to merge 4 commits into from
Closed

[Lock] Create a lock component #21093

wants to merge 4 commits into from

Conversation

jderusse
Copy link
Member

@jderusse jderusse commented Dec 29, 2016

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? they will
Fixed tickets #20382
License MIT
Doc PR symfony/symfony-docs#7364

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

class MyService
{
    private $lock;

    public function __construct(LockInterface $lock)
    {
        $this->lock = $lock;
    }

    public function run()
    {
        $this->lock->acquire(true);

        // If I'm here, no exception had been raised. Lock is acquired
        try {
            // do my job
        } finally {
            $this->lock->release();
        }
    }
}

Configured with something like

services:
    app.my_service:
        class: AppBundle\MyService
        arguments:
            - app.lock.my_service
    app.lock.my_service:
        class: Symfony\Component\Lock\Lock
        factory: ['@locker', createLock]
        arguments: ['my_service']

If you need to lock serveral resource on runtime, wou'll nneed to inject the LockFactory.

class MyService
{
    private $lockFactory;

    public function __construct(LockFactoryInterface $lockFactory)
    {
        $this->lockFactory = $lockFactory;
    }

    public function run()
    {
        foreach ($this->items as $item) {
            $lock = $this->lockFactory->createLock((string) $item);

            try {
                $lock->acquire();
            } catch (LockConflictedException $e) {
                continue;
            }

            // When I'm here, no exception had been, raised. Lock is acquired
            try {
                // do my job
            } finally {
                $lock->release();
            }
        }
    }
}

Configured with something like

services:
    app.my_service:
        class: AppBundle\MyService
        arguments:
            - '@locker'

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.

class MyService
{
    private $lock;

    public function __construct(LockInterface $lock)
    {
        $this->lock = $lock;
    }

    public function run()
    {
        $this->lock->acquire(true);

        try {
            do {
                $finished = $this->performLongTask();

                // Increase the expire date by 300 more seconds
                $this->lock->refresh();
            } while (!$finished)
            // do my job
        } finally {
            $this->lock->release();
        }
    }
}

Naming anc implementation choise

$lock->acquire()
vs
$lock->lock()

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.

$lock->acquire(false);
$lock->acquire(true);
vs
$lock->aquire()
$lock->waitAndAquire()

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

$lock = $factory->createLock($key);
$lock->acquire();
vs
$lock->aquire($key)

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.

interface LockInterface 
final class Lock implements LockInterface
vs
final class Lock

I choose to use a Interface even if there is only one implementaiton to offer an extension point here

TODO

In this PR

  • tests
  • add logs
  • offer several redis connectors
  • try other store implementation to validate the architecture/interface

In other PR

  • documentation
  • add configuration in framework bundle
  • add stop watch in the debug bar
  • improve the combined store (takes the drift into account and elapsed time between each store)
  • implement other stores (memcache, ...)
  • use this component in session manipulation (fixes [HttpFoundation] Session handlers should use a lock #4976)

@lyrixx
Copy link
Member

lyrixx commented Dec 29, 2016

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 aquire vs lock and for what is worth is prefer lock and release.

More over, I would prefer a system based on false/true than on exception.

@andersonamuller
Copy link
Contributor

Since you said the component is already full of Lock names, what about call the component Mutex?

Copy link
Contributor

@linaori linaori left a 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.

throw $e;
} catch (\Exception $e) {
throw new LockAcquiringException('', 0, $e);
}
Copy link
Contributor

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

I wonder if it make sens to wrap every exception with "LockException" here?
Or I should just let the original exception been thrown.

If yes, you're right, I should catch everything.
If no, I can remove this statement.

WDYT?

throw $e;
} catch (\Exception $e) {
throw new LockAcquiringException('', 0, $e);
}
Copy link
Contributor

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?

Copy link
Member

@nicolas-grekas nicolas-grekas Dec 29, 2016

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.

Copy link
Contributor

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

} else {
throw new InvalidArgumentException(
sprintf('Unable to acquire a blocking lock on a instance of "%s".', get_class($this->store))
);
Copy link
Contributor

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

*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class CombinedStore implements StoreInterface, ExpirableStoreInterface
Copy link
Contributor

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

*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class UnanimousQuorum implements QuorumInterface
Copy link
Contributor

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

use Symfony\Component\Lock\Store\ExpirableStoreInterface;

/**
* Lock is the default implementation to the LockInterface.
Copy link
Contributor

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

{
/**
* 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.
Copy link
Contributor

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

* 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
Copy link
Contributor

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

* 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.
*
Copy link
Contributor

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.

interface BlockingStoreInterface
{
/**
* Wait a key became free then stores the resource.
Copy link
Contributor

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.

Copy link
Contributor

@ro0NL ro0NL left a 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?

public function __construct(StoreInterface $decorated, $retrySleep = 50, $retryCount = PHP_INT_MAX)
{
$this->decorated = $decorated;
$this->retryCount = $retryCount;
Copy link
Contributor

@ro0NL ro0NL Dec 29, 2016

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 construct. To keep up with the promise like @iltar mentioned 👍

@javiereguiluz
Copy link
Member

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: lock, tryLock and unlock

*/
public function generateToken()
{
return base64_encode(random_bytes($this->entropy / 8));
Copy link
Member

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.

*/
public function createLock($resource)
{
return new Lock(new Key($resource), $this->store);
Copy link
Member

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?

);

// Silence error reporting
set_error_handler(function () {
Copy link
Member

Choose a reason for hiding this comment

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

single line

Copy link
Member

Choose a reason for hiding this comment

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

indeed

return;
}

$fileName = sprintf(
Copy link
Member

Choose a reason for hiding this comment

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

single line

Copy link
Member Author

Choose a reason for hiding this comment

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

Is there a limit to the line length in CS?

@jderusse
Copy link
Member Author

jderusse commented Dec 29, 2016

  • Moved the createLock factory in the StoreInterface
    Create an AbstractClass to avoid dupplication of code (Could be a trait too, WDYT)
  • Remove the TokenGenerator

API choices:

IMO lock and unlock fit well when the subject is the resource. ie. $resource->lock() or $handler->lock($resource);
In this PR the subject is not the resource, but the Lock itself, in this case, I think the python naming is meaningful $lock->acquire/release.

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.
But that's just my taste/opinion

$this->resource = (string) $resource;
}

public function getResource()
Copy link
Member

Choose a reason for hiding this comment

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

__toString()?

Copy link
Member Author

Choose a reason for hiding this comment

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

good idea

}

/**
* @return mixed
Copy link
Member

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

$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()));
Copy link
Member

Choose a reason for hiding this comment

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

an expiration, same below.

Copy link
Member

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.

Copy link
Member Author

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?

$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));
Copy link
Member

Choose a reason for hiding this comment

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

the {resource} lock.

interface LockInterface
{
/**
* Acquire the lock. If the lock is acquired by someone else, the parameter `blocking` determines whether or not
Copy link
Member

Choose a reason for hiding this comment

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

Acquires, same everywhere else.

interface QuorumInterface
{
/**
* Returns Whether or not the quorum is met.
Copy link
Member

Choose a reason for hiding this comment

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

extra cap (same below)

);

// Silence error reporting
set_error_handler(function () {
Copy link
Member

Choose a reason for hiding this comment

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

indeed


/**
* RetryTillSaveStore is a StoreInterface implementation which decorate a non blocking StoreInterface to provide a
* kind of blocking storage.
Copy link
Member

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.

public function exists(Key $key);

/**
* Retrieves a lock for the given resource.
Copy link
Member

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?

/**
* {@inheritdoc}
*/
public function acquire($blocking = false, $ttl = 300.0)
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member Author

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

/**
* {@inheritdoc}
*/
public function refresh($ttl = 300.0)
Copy link
Member

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.


return;
} catch (LockConflictedException $e) {
usleep($this->retrySleep * 1000);
Copy link
Member Author

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

namespace Symfony\Component\Lock\Exception;

/**
* InvalidArgumentException is thrown when a service had been badly initialized.
Copy link
Contributor

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.

@jderusse
Copy link
Member Author

jderusse commented Jan 1, 2017

Pushed a new commit to remove the 2 interfaces BlockingStoreInterface and ExpirableStoreInterface and merge the methods into StoreInterface.

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.

@nicolas-grekas nicolas-grekas added this to the 3.x milestone Jan 2, 2017
@fabpot
Copy link
Member

fabpot commented Jan 10, 2017

@jderusse you need to also add the new component in the replace key of the root composer.json file.

@@ -0,0 +1,19 @@
Copyright (c) 2016 Fabien Potencier
Copy link
Member

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

@jderusse jderusse force-pushed the lock branch 2 times, most recently from 816523b to f5b20bd Compare January 10, 2017 23:17
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
final class Lock implements LockInterface
Copy link
Contributor

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

*/
public function createLock($resource, $ttl = 300.0)
{
return new Lock(new Key($resource), $this, $ttl);
Copy link
Member

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.

Copy link
Member Author

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 ?)

try {
$store->delete($key);
} catch (\Exception $e) {
} catch (\Throwable $e) {
Copy link
Member

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

Copy link
Member Author

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

$store->putOffExpiration($key, $ttl);
++$successCount;
} catch (\Exception $e) {
++$failureCount;
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed


/**
* Tests blocking locks thanks to pcntl.
*
Copy link
Member

Choose a reason for hiding this comment

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

missing @requires extension pcntl

Copy link
Member Author

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

$drift = 20000;

if (PHP_VERSION_ID < 50600 || defined('HHVM_VERSION_ID')) {
$this->markTestSkipped('Php 5.5 does not keep resource in child forks');
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed


public function getStore()
{
$redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
Copy link
Member

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 ?

Copy link
Member Author

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.

@jjsaunier
Copy link

jjsaunier commented Jan 11, 2017

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:)

@jderusse
Copy link
Member Author

@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

Copy link
Contributor

@Wirone Wirone left a 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.

*/
public function putOffExpiration(Key $key, $ttl)
{
// do nothing, the flock locks forever.
Copy link
Contributor

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.

}

// Have to be a \Predis\Client
return call_user_func_array(array($this->redis, 'eval'), array_merge(array($script, 1, $resource), $args));
Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in #22117
Thank you @Wirone

@goetas
Copy link
Contributor

goetas commented Mar 24, 2017

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.

{
$this->store->delete($this->key);

if ($this->store->exists($this->key)) {
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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"

* @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);
Copy link
Contributor

@goetas goetas Mar 24, 2017

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

Copy link
Member Author

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

@jocel1
Copy link

jocel1 commented Mar 24, 2017

What about adding a MysqlStore using GET_LOCK() / RELEASE_LOCK() / IS_FREE_LOCK() functions ?
https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock

It's robust and available in all MySQL versions.

@jderusse
Copy link
Member Author

@goetas Then I've got a good news to you, the actual implementation does not suffer for such issue. Thanks to flock (or equivalent mechanism in other stores) the system kernel will automatically free the lock even if you forget to release it, or die, or segfault, ...

@jocel1 I didn't dig into MySQL store, but it's on my todo list. Do you want to create that PR?

@goetas
Copy link
Contributor

goetas commented Mar 24, 2017

@jocel1 In mysql there is a fundamental concept that PHP has not, the connection.

As the documentation says:

A lock obtained with GET_LOCK() is released explicitly by executing RELEASE_LOCK() or implicitly when your session terminates (either normally or abnormally)

At the end of the connection, the lock will be automatically released.

@jocel1
Copy link

jocel1 commented Mar 24, 2017

@goetas yup that's why it also great to easily handle the dying script case: there's nothing specific to do :)

*/
public function __construct(\Memcached $memcached, $initialTtl = 300)
{
if (!static::isSupported()) {
Copy link
Contributor

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

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.

@goetas
Copy link
Contributor

goetas commented Mar 24, 2017

@jderusse Can you point me what guarantees that delete gets called for the memcache store as example? (https://github.com/jderusse/symfony/blob/48c998a097e8c71d90c5e8b508d21c8b866f2db1/src/Symfony/Component/Lock/Store/MemcachedStore.php#L113)

@goetas
Copy link
Contributor

goetas commented Mar 24, 2017

I have tested the Flock and the Redis store.

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.

@jderusse
Copy link
Member Author

@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, ...)
Second issue. how do you know if you are the locker?

@goetas
Copy link
Contributor

goetas commented Mar 24, 2017

@jderusse

None. But I can guarantee that Memcache will delete the key bat it own, when the TTL will expire

i knew this, the standard TTL that many key-value store have

@jderusse
Copy link
Member Author

I suggest to read the (not yet merged) documentation about this https://github.com/symfony/symfony-docs/pull/7364/files

@goetas
Copy link
Contributor

goetas commented Mar 24, 2017

@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)

@goetas
Copy link
Contributor

goetas commented Mar 24, 2017

Mysql and postgres locks can be interesting to implement since they offer both advantages of local and remote locks :)

return false;
} catch (\Exception $e) {
$this->logger->warning('Failed to lock the "{resource}".', array('resource' => $this->key, 'exception' => $e));
throw new LockAcquiringException('', 0, $e);
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

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

indeed..

@jocel1
Copy link

jocel1 commented Mar 24, 2017

@jderusse

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, ...)
Second issue. how do you know if you are the locker?

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.
For the second issue, if you're the locker, you can call another GET_LOCK, the call will not be blocking. It's only blocking if you're calling the GET_LOCK in another session.

@nicolas-grekas nicolas-grekas modified the milestone: 3.x Mar 24, 2017
@patrick-mcdougle
Copy link
Contributor

patrick-mcdougle commented Apr 10, 2017

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?

@jderusse
Copy link
Member Author

Hi @patrick-mcdougle, can you please open an issue in order to follow this point?

javiereguiluz added a commit to symfony/symfony-docs that referenced this pull request Apr 24, 2017
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
@fabpot fabpot mentioned this pull request May 1, 2017
nicolas-grekas added a commit that referenced this pull request Apr 30, 2018
…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
@jderusse jderusse deleted the lock branch August 2, 2019 12:16
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.

[HttpFoundation] Session handlers should use a lock