Permalink
Browse files

feature #22113 [Lock] Include lock component in framework bundle (jde…

…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 e5ddd14b1d6e42c9bbc5492771d82ca288daf5b1
@@ -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;
@@ -129,6 +131,7 @@ public function getConfigTreeBuilder()
$this->addCacheSection($rootNode);
$this->addPhpErrorsSection($rootNode);
$this->addWebLinkSection($rootNode);
$this->addLockSection($rootNode);
return $treeBuilder;
}
@@ -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
@@ -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;
@@ -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;
@@ -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.');
@@ -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);
@@ -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>
@@ -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" />
@@ -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>
@@ -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
{
@@ -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,9 @@
parameters:
env(REDIS_DSN): redis://paas.com
framework:
lock:
foo: semaphore
bar: flock
baz: [semaphore, flock]
qux: "%env(REDIS_DSN)%"
@@ -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",
@@ -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());
@@ -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());
@@ -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));
}
Oops, something went wrong.

0 comments on commit e5ddd14

Please sign in to comment.