Skip to content

[Lock] Include lock component in framework bundle #22113

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 3 commits into from

Conversation

jderusse
Copy link
Member

@jderusse jderusse commented Mar 22, 2017

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

Usage

use the best available "out of box" store (semaphore if available, filesyste otherwise)

framework:
    lock: ~
    # lock: true
$this->get('lock')->acquire();
$this->get('lock.factory')->createLock('my resource')->acquire();

use a specific store

framework:
    lock: flock
    # lock: semaphore
    # lock: redis://localhost
    # lock: "%env(MEMCACHED_DSN)%"
    # lock: ["%env(REDIS_DSN_1)%", "%env(REDIS_DSN_2)%"]
$this->get('lock')->acquire();
$this->get('lock.factory')->createLock('my resource')->acquire();

use a named lock

framework:
    lock: 
        foo: flock
        bar: redis://localhost
$this->get('lock.foo')->acquire();
$this->get('lock.bar.factory')->createLock('my resource')->acquire();

factory usage

        <service id="acme" class="AppBundle\Acme">
            <argument type="service">
                <service class="Symfony\Component\Lock\Lock">
                    <factory service="lock.foo.factory" method="createLock" />
                    <argument>my resource</argument>
                    <argument>30</argument> <!-- optional TTL -->
                </service>
            </argument>
        </service>
  • Tests


<service id="lock.store.combined.abstract" class="Symfony\Component\Lock\Store\CombinedStore" abstract="true" public="false">
<argument /> <!-- List of stores -->
<argument type="service" id="lock.quorum.majority" /> <!-- Quorum -->
Copy link
Member Author

Choose a reason for hiding this comment

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

Could be rename into strategy

@jderusse jderusse changed the title [Lock] Include lock component in framework bundle [WIP] [Lock] Include lock component in framework bundle Mar 22, 2017
@fbourigault
Copy link
Contributor

fbourigault commented Mar 23, 2017

What about adding a framework.lock.default config to override the lock.factory priority order? So the user can choose the implementation he wants as default.

@javiereguiluz
Copy link
Member

javiereguiluz commented Mar 23, 2017

Maybe I'm late, but I'd like to propose a simpler way to use and configure this component inside the Symfony Framework. The idea, as usual, is to optimize/simplify the common case and don't complicate much the complex case.

Common case

Application defines/needs just 1 lock type.

Configuration

This is the default config, where Symfony picks the best lock type for you: semaphore if the extension is installed; flock otherwise:

framework:
    lock: ~

This is the explicit config where you set the lock type (string or array and some smart conversion by the Config component, as we do everywhere else):

framework:
    lock: 'flock'
    lock: 'semaphore'
    lock: 'memcached://m1.docker'
    lock: ['memcached://m1.docker', 'memcached://m2.docker']
    lock: 'redis://r1.docker'
    lock: ['redis://r1.docker', 'redis://r2.docker']

Usage

// non-blocking, non-expiring
$lock = $this->get('lock')->acquire();
// $lock = $container->get('lock')->acquire();

// blocking, non-expiring
$lock = $this->get('lock')->acquire(true);

// non-blocking, expiring in 60 seconds
$lock = $this->get('lock')->acquire(false, 60);

// normal operations on the lock
$lock->isAcquired();
$lock->refresh();
$lock->release();

Complex case

Application defines and uses several lock types.

Configuration

framework:
    lock:
        users: ['memcached://m1.docker', 'memcached://m2.docker']
        admin: 'flock'
        partners: 'redis://r1.docker'
        invoices: 'semaphore'

Usage

$adminLock = $this->get('lock.admin')->acquire();
$invoicesLock = $this->get('lock.invoices')->acquire(true);
$partnersLock = $this->get('lock.partners')->acquire(false, 30);
// ...


if (count($hostsDefinitions) > 1) {
$definition = new ChildDefinition('lock.store.combined.abstract');
$definition->replaceArgument(0, $hostsDefinitions);
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to lazy load stores using an iterator? It seems the combined store can break in the middle of the store chain. Btw, why not ChainStore instead of CombinedStore for consistency?


if (null !== $username || null !== $password) {
if (!method_exists($client, 'setSaslAuthData')) {
trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
Copy link
Contributor

@ro0NL ro0NL Mar 23, 2017

Choose a reason for hiding this comment

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

What about a E_USER_WARNING? As we're clearly going to call an undefined method.

Edit: i see it's a copy paste of the cache component.. understandable. Probably not worth it then. Im not sure this is desirable maintenance-wise though (vs. soft-depending on symfony/cache).

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 choose to use the same error/format than src/Symfony/Component/Cache/Traits/MemcachedTrait.php for more consistency

@nicolas-grekas nicolas-grekas added this to the 3.3 milestone Mar 24, 2017
@jderusse
Copy link
Member Author

jderusse commented Apr 1, 2017

@javiereguiluz Today I start to implement your suggestion (much simpler, I like it).
But then, I realize, it'll not be compatible with dynamic resources.
I suggest a small change in your proposal by adding factories too.

$this->get('lock_factory.invoices')->createLock('invoices.'.$invoice->getId());

the service lock.invoices would be an alias of $this->get('lock_factory.invoices')->createLock('invoices').
WDYT?

I've got some other question:

When you says $lock = $this->get('lock')->acquire(); you means $lock = $this->get('lock') + $isLocked = $this->get('lock')->acquire(); right?

Last point you are moving the ttl from the lock constructor to the acquire method. Is it made on purpose? Shouldn't it be a parameter defined in the config file like:

framework:
    lock:
        users: ['memcached://m1.docker', 'memcached://m2.docker']
        invoices:
            store: ['memcached://m1.docker', 'memcached://m2.docker']
            ttl: 300

@nicolas-grekas
Copy link
Member

Moving to milestone 3.4 as this is not a blocker for 3.3. If you don't agree, please tell :)

@nicolas-grekas nicolas-grekas modified the milestones: 3.3, 3.4 Apr 28, 2017
@jderusse jderusse changed the base branch from master to 3.4 June 19, 2017 19:56
@jderusse jderusse force-pushed the lock-framework branch 7 times, most recently from ef4c5de to 68954c3 Compare June 19, 2017 21:11
@jderusse jderusse changed the title [WIP] [Lock] Include lock component in framework bundle [Lock] Include lock component in framework bundle Jun 19, 2017
@jderusse
Copy link
Member Author

PR rebased

xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>

Copy link
Member

Choose a reason for hiding this comment

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

<defaults public="false" />

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 catch

if ('default' === $resourceName) {
$container->setAlias('lock.store', new Alias('lock.'.$resourceName.'.store'));
$container->setAlias('lock.factory', new Alias('lock.'.$resourceName.'.factory'));
$container->setAlias('lock', new Alias('lock.'.$resourceName));
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 private unless strongly needed

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 moved .store services to private. But keep lock and lock.factory public. To let user retrieve the lock from a ContainerAware object (controller, command, ...)

$this->get('lock.foo')->acquire();
$this->get('lock.bar.factory')->createLock('my resource')->acquire();
$this->get('lock')->acquire();
$this->get('lock.factory')->createLock('my resource')->acquire();

Copy link
Member Author

Choose a reason for hiding this comment

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

id I missed something? Or am I using it wrongly?

Copy link
Member

@chalasr chalasr Jul 31, 2017

Choose a reason for hiding this comment

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

ContainerAware objects are still supported, but I think we all agree that they should not be promoted (for end applications esp.) and features should not be designed with that in mind.
Better inject @lock.factory (controllers/commands/everything as services, should be autoinjected quite easily also) rather than locate it using a DIC (here is the bad habit: a DIC is not meant to be used as a service locator but to wire services, see #21623 for reference).

If no internal need for get() this service, we should keep it private (regarding BC we can make it public afterwards if legit use case, not the inverse).

// Generate factories for each resource
$factoryDefinition = new ChildDefinition('lock.factory.abstract');
$factoryDefinition->replaceArgument(0, new Reference('lock.'.$resourceName.'.store'));
$factoryDefinition->setPublic(true);
Copy link
Member

Choose a reason for hiding this comment

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

unless strongly needed it should remain private

$combinedDefinition->replaceArgument(0, $storeDefinitions);
$container->setDefinition('lock.'.$resourceName.'.store', $combinedDefinition);
} else {
$container->setAlias('lock.'.$resourceName.'.store', new Alias((string) $storeDefinitions[0]));
Copy link
Member

Choose a reason for hiding this comment

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

private?

$container->setDefinition($connectionDefinitionId, $connectionDefinition);
}

$storeDefinition = new Definition(\stdClass::class);
Copy link
Member

Choose a reason for hiding this comment

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

StoreInterface::class?

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

@jderusse
Copy link
Member Author

@chalasr tests should be fixed by #23847

@chalasr
Copy link
Member

chalasr commented Aug 10, 2017

Cool! thanks

@chalasr
Copy link
Member

chalasr commented Aug 18, 2017

ping @symfony/deciders

{
$loader->load('lock.xml');

$container->getDefinition('lock.store.flock')->replaceArgument(0, sys_get_temp_dir());
Copy link
Member

@nicolas-grekas nicolas-grekas Sep 5, 2017

Choose a reason for hiding this comment

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

This inlines the tempdir used at compile time for reuse at build time, making the dumped container not-independent from the build env.

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 catch

$storeDefinition = new Reference('lock.store.semaphore');
break;
case $usedEnvs || preg_match('#^[a-z]++://#', $storeDsn):
if (!$container->hasDefinition($connectionDefinitionId = md5($storeDsn))) {
Copy link
Member

Choose a reason for hiding this comment

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

$container->hash() instead of md5

$container->setDefinition($connectionDefinitionId, $connectionDefinition);
}

$storeDefinition = new Definition(StoreInterface::class);
Copy link
Member

Choose a reason for hiding this comment

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

the definition should me made private

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

$storeDefinition->setFactory(array(StoreFactory::class, 'createStore'));
$storeDefinition->setArguments(array(new Reference($connectionDefinitionId)));

$container->setDefinition($storeDefinitionId = 'lock.'.$resourceName.'.store.'.md5($storeDsn), $storeDefinition);
Copy link
Member

Choose a reason for hiding this comment

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

$container->hash also here


$storeDefinition = new Reference($storeDefinitionId);
break;
case $usedEnvs:
Copy link
Member

Choose a reason for hiding this comment

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

dead code because of the previous case

$container->setDefinition('lock.'.$resourceName.'.factory', $factoryDefinition);

// Generate services for lock instances
$lockDefinition = new Definition(Lock::class);
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 private

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

if ('default' === $resourceName) {
$container->setAlias('lock.store', new Alias('lock.'.$resourceName.'.store', false));
$container->setAlias('lock.factory', new Alias('lock.'.$resourceName.'.factory', false));
$container->setAlias('lock', new Alias('lock.'.$resourceName, false));
Copy link
Member

Choose a reason for hiding this comment

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

missing FQCN aliases that could be use by autowiring

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

*
* @throws \ErrorEception When invalid options or dsn are provided
*/
public static function createConnection($dsn, array $options = array())
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 match #23978

@jderusse jderusse force-pushed the lock-framework branch 2 times, most recently from d65375a to 34a22f9 Compare September 5, 2017 22:34

<service id="lock.store.flock" class="Symfony\Component\Lock\Store\FlockStore">
<argument type="service">
<service class="string">
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've some doublt about the value of class here

<service id="lock.store.flock" class="Symfony\Component\Lock\Store\FlockStore">
<argument type="service">
<service class="string">
<factory function="sys_get_temp_dir"/>
Copy link
Member

@nicolas-grekas nicolas-grekas Sep 18, 2017

Choose a reason for hiding this comment

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

instead of this, what about making the $lockPath arg of FlockStore optional, and make it use sys_get_temp_dir then?

</argument>
</service>

<service id="lock.store.semaphore" class="Symfony\Component\Lock\Store\SemaphoreStore"/>
Copy link
Member

Choose a reason for hiding this comment

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

bad consistency about space before /> in the file

*
* @return \Redis|\Predis\Client According to the "class" option
*/
public static function createConnection($dsn, array $options = array())
Copy link
Member

Choose a reason for hiding this comment

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

We could maybe consider a new component, named Dsn, with factories for any kind of DSN we want to support?
not sure we can make it for 3.4, but what do others think about the idea?

@jderusse jderusse force-pushed the lock-framework branch 3 times, most recently from 52420e9 to 06342f4 Compare September 19, 2017 06:28
Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

👍

@@ -32,12 +32,15 @@ class FlockStore implements StoreInterface
private $lockPath;

/**
* @param string $lockPath the directory to store the lock
* @param string|null $lockPath the directory to store the lock. Default values will use temporary directory
Copy link
Member

Choose a reason for hiding this comment

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

the directory to store the lock, defaults to the system's temporary directory

Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

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

fine tuning

public static function createConnection($dsn, array $options = array())
{
if (!is_string($dsn)) {
throw new InvalidArgumentException(sprintf('The %s() method expect argument #1 to be string, %s given.', __METHOD__, gettype($dsn)));
Copy link
Member

Choose a reason for hiding this comment

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

expects

*/
class StoreFactory
{
public static function createConnection($dsn, array $options = array())
Copy link
Member

Choose a reason for hiding this comment

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

I think we should add a docblock listing supported dsn

Copy link
Member Author

Choose a reason for hiding this comment

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

will be removed replaced by the new Dns component

throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
}

public static function createStore($connection)
Copy link
Member

Choose a reason for hiding this comment

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

same

@param \Redis|\RedisArray|\RedisCluster|\Predis\Client|\Memcached $connection
@return RedisStore|MemcachedStore

@fabpot
Copy link
Member

fabpot commented Sep 27, 2017

Thank you @jderusse.

@fabpot fabpot closed this Sep 27, 2017
fabpot added a commit that referenced this pull request Sep 27, 2017
…russe)

This PR was squashed before being merged into the 3.4 branch (closes #22113).

Discussion
----------

[Lock] Include lock component in framework bundle

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | none
| License       | MIT
| Doc PR        | symfony/symfony-docs#8066

## Usage

use the best available "out of box" store (semaphore if available, filesyste otherwise)
```yml
framework:
    lock: ~
    # lock: true
```

```php
$this->get('lock')->acquire();
$this->get('lock.factory')->createLock('my resource')->acquire();
```

use a specific store
```yml
framework:
    lock: flock
    # lock: semaphore
    # lock: redis://localhost
    # lock: "%env(MEMCACHED_DSN)%"
    # lock: ["%env(REDIS_DSN_1)%", "%env(REDIS_DSN_2)%"]
```

```php
$this->get('lock')->acquire();
$this->get('lock.factory')->createLock('my resource')->acquire();
```

use a named lock
```yml
framework:
    lock:
        foo: flock
        bar: redis://localhost
```

```php
$this->get('lock.foo')->acquire();
$this->get('lock.bar.factory')->createLock('my resource')->acquire();
```

factory usage

```xml
        <service id="acme" class="AppBundle\Acme">
            <argument type="service">
                <service class="Symfony\Component\Lock\Lock">
                    <factory service="lock.foo.factory" method="createLock" />
                    <argument>my resource</argument>
                    <argument>30</argument> <!-- optional TTL -->
                </service>
            </argument>
        </service>
```

* [x] Tests

Commits
-------

b4b00c9 [Lock] Include lock component in framework bundle
This was referenced Oct 18, 2017
javiereguiluz added a commit to symfony/symfony-docs that referenced this pull request Jan 29, 2018
This PR was merged into the 3.4 branch.

Discussion
----------

[Lock] Integrate framework

Should probably be merged after #7866
Documentation for symfony/symfony#22113

Commits
-------

142e703 Fixed minor issues
7f1e087 Add lock configuration in framework bundle
@jderusse jderusse deleted the lock-framework branch August 2, 2019 12:17
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.

7 participants