Navigation Menu

Skip to content

Commit

Permalink
feature #22113 [Lock] Include lock component in framework bundle (jde…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
fabpot committed Sep 27, 2017
2 parents 9ebe218 + b4b00c9 commit e5ddd14
Show file tree
Hide file tree
Showing 18 changed files with 618 additions and 5 deletions.
Expand Up @@ -18,6 +18,8 @@
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Form\Form;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Validation;
Expand Down Expand Up @@ -129,6 +131,7 @@ public function getConfigTreeBuilder()
$this->addCacheSection($rootNode);
$this->addPhpErrorsSection($rootNode);
$this->addWebLinkSection($rootNode);
$this->addLockSection($rootNode);

return $treeBuilder;
}
Expand Down Expand Up @@ -875,6 +878,49 @@ private function addPhpErrorsSection(ArrayNodeDefinition $rootNode)
;
}

private function addLockSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('lock')
->info('Lock configuration')
->{!class_exists(FullStack::class) && class_exists(Lock::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->beforeNormalization()
->ifString()->then(function ($v) { return array('enabled' => true, 'resources' => $v); })
->end()
->beforeNormalization()
->ifTrue(function ($v) { return is_array($v) && !isset($v['resources']); })
->then(function ($v) {
$e = $v['enabled'];
unset($v['enabled']);

return array('enabled' => $e, 'resources' => $v);
})
->end()
->addDefaultsIfNotSet()
->fixXmlConfig('resource')
->children()
->arrayNode('resources')
->requiresAtLeastOneElement()
->defaultValue(array('default' => array(class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphore' : 'flock')))
->beforeNormalization()
->ifString()->then(function ($v) { return array('default' => $v); })
->end()
->beforeNormalization()
->ifTrue(function ($v) { return is_array($v) && array_keys($v) === range(0, count($v) - 1); })
->then(function ($v) { return array('default' => $v); })
->end()
->prototype('array')
->beforeNormalization()->ifString()->then(function ($v) { return array($v); })->end()
->prototype('scalar')->end()
->end()
->end()
->end()
->end()
->end()
;
}

private function addWebLinkSection(ArrayNodeDefinition $rootNode)
{
$rootNode
Expand Down
Expand Up @@ -38,6 +38,7 @@
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
Expand All @@ -54,6 +55,11 @@
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Lock\Factory;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\Store\StoreFactory;
use Symfony\Component\Lock\StoreInterface;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
Expand Down Expand Up @@ -298,6 +304,10 @@ public function load(array $configs, ContainerBuilder $container)
$this->registerPropertyInfoConfiguration($config['property_info'], $container, $loader);
}

if ($this->isConfigEnabled($container, $config['lock'])) {
$this->registerLockConfiguration($config['lock'], $container, $loader);
}

if ($this->isConfigEnabled($container, $config['web_link'])) {
if (!class_exists(HttpHeaderSerializer::class)) {
throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed.');
Expand Down Expand Up @@ -1672,6 +1682,84 @@ private function registerPropertyInfoConfiguration(array $config, ContainerBuild
}
}

private function registerLockConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
{
$loader->load('lock.xml');

foreach ($config['resources'] as $resourceName => $resourceStores) {
if (0 === count($resourceStores)) {
continue;
}

// Generate stores
$storeDefinitions = array();
foreach ($resourceStores as $storeDsn) {
$storeDsn = $container->resolveEnvPlaceholders($storeDsn, null, $usedEnvs);
switch (true) {
case 'flock' === $storeDsn:
$storeDefinition = new Reference('lock.store.flock');
break;
case 'semaphore' === $storeDsn:
$storeDefinition = new Reference('lock.store.semaphore');
break;
case $usedEnvs || preg_match('#^[a-z]++://#', $storeDsn):
if (!$container->hasDefinition($connectionDefinitionId = $container->hash($storeDsn))) {
$connectionDefinition = new Definition(\stdClass::class);
$connectionDefinition->setPublic(false);
$connectionDefinition->setFactory(array(StoreFactory::class, 'createConnection'));
$connectionDefinition->setArguments(array($storeDsn));
$container->setDefinition($connectionDefinitionId, $connectionDefinition);
}

$storeDefinition = new Definition(StoreInterface::class);
$storeDefinition->setPublic(false);
$storeDefinition->setFactory(array(StoreFactory::class, 'createStore'));
$storeDefinition->setArguments(array(new Reference($connectionDefinitionId)));

$container->setDefinition($storeDefinitionId = 'lock.'.$resourceName.'.store.'.$container->hash($storeDsn), $storeDefinition);

$storeDefinition = new Reference($storeDefinitionId);
break;
default:
throw new InvalidArgumentException(sprintf('Lock store DSN "%s" is not valid in resource "%s"', $storeDsn, $resourceName));
}

$storeDefinitions[] = $storeDefinition;
}

// Wrap array of stores with CombinedStore
if (count($storeDefinitions) > 1) {
$combinedDefinition = new ChildDefinition('lock.store.combined.abstract');
$combinedDefinition->replaceArgument(0, $storeDefinitions);
$container->setDefinition('lock.'.$resourceName.'.store', $combinedDefinition);
} else {
$container->setAlias('lock.'.$resourceName.'.store', new Alias((string) $storeDefinitions[0], false));
}

// Generate factories for each resource
$factoryDefinition = new ChildDefinition('lock.factory.abstract');
$factoryDefinition->replaceArgument(0, new Reference('lock.'.$resourceName.'.store'));
$container->setDefinition('lock.'.$resourceName.'.factory', $factoryDefinition);

// Generate services for lock instances
$lockDefinition = new Definition(Lock::class);
$lockDefinition->setPublic(false);
$lockDefinition->setFactory(array(new Reference('lock.'.$resourceName.'.factory'), 'createLock'));
$lockDefinition->setArguments(array($resourceName));
$container->setDefinition('lock.'.$resourceName, $lockDefinition);

// provide alias for default resource
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));
$container->setAlias(StoreInterface::class, new Alias('lock.store', false));
$container->setAlias(Factory::class, new Alias('lock.factory', false));
$container->setAlias(LockInterface::class, new Alias('lock', false));
}
}
}

private function registerCacheConfiguration(array $config, ContainerBuilder $container)
{
$version = substr(str_replace('/', '-', base64_encode(hash('sha256', uniqid(mt_rand(), true), true))), 0, 22);
Expand Down
38 changes: 38 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml
@@ -0,0 +1,38 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<defaults public="false" />

<service id="lock.store.flock" class="Symfony\Component\Lock\Store\FlockStore" />

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

<service id="lock.store.memcached.abstract" class="Symfony\Component\Lock\Store\MemcachedStore" abstract="true">
<argument /> <!-- Memcached connection service -->
</service>

<service id="lock.store.redis.abstract" class="Symfony\Component\Lock\Store\RedisStore" abstract="true">
<argument /> <!-- Redis connection service -->
</service>

<service id="lock.store.combined.abstract" class="Symfony\Component\Lock\Store\CombinedStore" abstract="true">
<argument /> <!-- List of stores -->
<argument type="service" id="lock.strategy.majority" /> <!-- Strategy -->
</service>

<service id="lock.strategy.majority" class="Symfony\Component\Lock\Strategy\ConsensusStrategy" />

<service id="lock.factory.abstract" class="Symfony\Component\Lock\Factory" abstract="true">
<tag name="monolog.logger" channel="lock" />
<argument /> <!-- Store -->
<call method="setLogger">
<argument type="service" id="logger" on-invalid="ignore" />
</call>
</service>

</services>
</container>
Expand Up @@ -29,6 +29,7 @@
<xsd:element name="property-info" type="property_info" minOccurs="0" maxOccurs="1" />
<xsd:element name="cache" type="cache" minOccurs="0" maxOccurs="1" />
<xsd:element name="workflow" type="workflow" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="lock" type="lock" minOccurs="0" maxOccurs="1" />
</xsd:choice>

<xsd:attribute name="http-method-override" type="xsd:boolean" />
Expand Down Expand Up @@ -296,4 +297,19 @@
<xsd:enumeration value="workflow" />
</xsd:restriction>
</xsd:simpleType>

<xsd:complexType name="lock">
<xsd:sequence>
<xsd:element name="resource" type="lock_resource" minOccurs="1" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="enabled" type="xsd:boolean" />
</xsd:complexType>

<xsd:complexType name="lock_resource">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="name" type="xsd:string" />
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
</xsd:schema>
Expand Up @@ -16,6 +16,7 @@
use Symfony\Bundle\FullStack;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Lock\Store\SemaphoreStore;

class ConfigurationTest extends TestCase
{
Expand Down Expand Up @@ -343,6 +344,14 @@ protected static function getBundleDefaultConfig()
'web_link' => array(
'enabled' => !class_exists(FullStack::class),
),
'lock' => array(
'enabled' => !class_exists(FullStack::class),
'resources' => array(
'default' => array(
class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphore' : 'flock',
),
),
),
);
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

<framework:config>
<framework:lock/>
</framework:config>
</container>
@@ -0,0 +1,22 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">


<parameters>
<parameter key="env(REDIS_URL)">redis://paas.com</parameter>
</parameters>

<framework:config>
<framework:lock>
<framework:resource name="foo">semaphore</framework:resource>
<framework:resource name="bar">flock</framework:resource>
<framework:resource name="baz">semaphore</framework:resource>
<framework:resource name="baz">flock</framework:resource>
<framework:resource name="qux">%env(REDIS_URL)%</framework:resource>
</framework:lock>
</framework:config>
</container>
@@ -0,0 +1,2 @@
framework:
lock: ~
@@ -0,0 +1,9 @@
parameters:
env(REDIS_DSN): redis://paas.com

framework:
lock:
foo: semaphore
bar: flock
baz: [semaphore, flock]
qux: "%env(REDIS_DSN)%"
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/composer.json
Expand Up @@ -54,6 +54,7 @@
"symfony/workflow": "~3.3|~4.0",
"symfony/yaml": "~3.2|~4.0",
"symfony/property-info": "~3.3|~4.0",
"symfony/lock": "~3.4|~4.0",
"symfony/web-link": "~3.3|~4.0",
"doctrine/annotations": "~1.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0",
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Console/Command/LockableTrait.php
Expand Up @@ -46,7 +46,7 @@ private function lock($name = null, $blocking = false)
if (SemaphoreStore::isSupported($blocking)) {
$store = new SemaphoreStore();
} else {
$store = new FlockStore(sys_get_temp_dir());
$store = new FlockStore();
}

$this->lock = (new Factory($store))->createLock($name ?: $this->getName());
Expand Down
Expand Up @@ -44,7 +44,7 @@ public function testLockReturnsFalseIfAlreadyLockedByAnotherCommand()
if (SemaphoreStore::isSupported(false)) {
$store = new SemaphoreStore();
} else {
$store = new FlockStore(sys_get_temp_dir());
$store = new FlockStore();
}

$lock = (new Factory($store))->createLock($command->getName());
Expand Down
7 changes: 5 additions & 2 deletions src/Symfony/Component/Lock/Store/FlockStore.php
Expand Up @@ -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, defaults to the system's temporary directory
*
* @throws LockStorageException If the lock directory could not be created or is not writable
*/
public function __construct($lockPath)
public function __construct($lockPath = null)
{
if (null === $lockPath) {
$lockPath = sys_get_temp_dir();
}
if (!is_dir($lockPath) || !is_writable($lockPath)) {
throw new InvalidArgumentException(sprintf('The directory "%s" is not writable.', $lockPath));
}
Expand Down

0 comments on commit e5ddd14

Please sign in to comment.