Skip to content

Commit fbd2bcc

Browse files
wachterjohannesalexander-schranz
authored andcommitted
Added locking mechanism (#39)
* improved interface for storage * added lock to config * removed strategy from locking configuration * renamed file-lok * added logger to runner * added skipped to execution-repository * fixed composer.json
1 parent 4541089 commit fbd2bcc

File tree

13 files changed

+230
-20
lines changed

13 files changed

+230
-20
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
],
1212
"require": {
1313
"php": "~5.5 || ~7.0",
14-
"php-task/php-task": "dev-develop",
14+
"php-task/php-task": "dev-master",
1515
"symfony/http-kernel": "^2.6 || ^3.0",
1616
"symfony/dependency-injection": "^2.6 || ^3.0",
17+
"symfony/expression-language": "^2.6 || ^3.0",
1718
"symfony/config": "^2.6 || ^3.0",
1819
"symfony/console": "^2.6 || ^3.0",
1920
"doctrine/orm": "^2.5"

src/DependencyInjection/Configuration.php

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@
1919
*/
2020
class Configuration implements ConfigurationInterface
2121
{
22+
/**
23+
* @var string[]
24+
*/
25+
private $lockingStorageAliases = [];
26+
27+
/**
28+
* @param \string[] $lockingStorageAliases
29+
*/
30+
public function __construct(array $lockingStorageAliases)
31+
{
32+
$this->lockingStorageAliases = $lockingStorageAliases;
33+
}
34+
2235
/**
2336
* {@inheritdoc}
2437
*/
@@ -43,9 +56,28 @@ public function getConfigTreeBuilder()
4356
->arrayNode('run')
4457
->addDefaultsIfNotSet()
4558
->children()
46-
->enumNode('mode')
47-
->values(['off', 'listener'])
48-
->defaultValue('off')
59+
->enumNode('mode')->values(['off', 'listener'])->defaultValue('off')->end()
60+
->end()
61+
->end()
62+
->arrayNode('locking')
63+
->canBeEnabled()
64+
->addDefaultsIfNotSet()
65+
->children()
66+
->enumNode('storage')
67+
->values(array_keys($this->lockingStorageAliases))
68+
->defaultValue('file')
69+
->end()
70+
->integerNode('ttl')->defaultValue(600)->end()
71+
->arrayNode('storages')
72+
->addDefaultsIfNotSet()
73+
->children()
74+
->arrayNode('file')
75+
->addDefaultsIfNotSet()
76+
->children()
77+
->scalarNode('directory')->defaultValue('%kernel.cache_dir%/tasks')->end()
78+
->end()
79+
->end()
80+
->end()
4981
->end()
5082
->end()
5183
->end()
@@ -63,4 +95,16 @@ public function getConfigTreeBuilder()
6395

6496
return $treeBuilder;
6597
}
98+
99+
/**
100+
* Returns id for given storage-alias.
101+
*
102+
* @param string $alias
103+
*
104+
* @return string
105+
*/
106+
public function getLockingStorageId($alias)
107+
{
108+
return $this->lockingStorageAliases[$alias];
109+
}
66110
}

src/DependencyInjection/TaskExtension.php

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Task\TaskBundle\DependencyInjection;
1313

1414
use Symfony\Component\Config\FileLocator;
15+
use Symfony\Component\Config\Loader\LoaderInterface;
1516
use Symfony\Component\DependencyInjection\ContainerBuilder;
1617
use Symfony\Component\DependencyInjection\ContainerInterface;
1718
use Symfony\Component\DependencyInjection\Definition;
@@ -31,23 +32,29 @@ class TaskExtension extends Extension
3132
*/
3233
public function load(array $configs, ContainerBuilder $container)
3334
{
34-
$configuration = new Configuration();
35+
$configuration = $this->getConfiguration($configs, $container);
3536
$config = $this->processConfiguration($configuration, $configs);
3637

3738
$container->setParameter('task.system_tasks', $config['system_tasks']);
38-
$container->setParameter('task.storage', $config['storage']);
39+
40+
$container->setAlias('task.lock.storage', $configuration->getLockingStorageId($config['locking']['storage']));
41+
foreach (array_keys($config['locking']['storages']) as $key) {
42+
$container->setParameter('task.lock.storages.' . $key, $config['locking']['storages'][$key]);
43+
}
3944

4045
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
4146
$loader->load(sprintf('storage/%s.xml', $config['storage']));
4247
$loader->load('task_event_listener.xml');
4348
$loader->load('scheduler.xml');
4449
$loader->load('command.xml');
50+
$loader->load('locking/services.xml');
4551

4652
if ($config['run']['mode'] === 'listener') {
4753
$loader->load('listener.xml');
4854
}
4955

5056
$this->loadDoctrineAdapter($config['adapters']['doctrine'], $container);
57+
$this->loadLockingComponent($config['locking'], $container, $loader);
5158
}
5259

5360
/**
@@ -70,4 +77,53 @@ private function loadDoctrineAdapter(array $config, ContainerBuilder $container)
7077
$container->setDefinition('task.adapter.doctrine.execution_listener', $definition);
7178
}
7279
}
80+
81+
/**
82+
* Load services for locking component.
83+
*
84+
* @param array $config
85+
* @param LoaderInterface $loader
86+
* @param ContainerBuilder $container
87+
*/
88+
private function loadLockingComponent(array $config, ContainerBuilder $container, LoaderInterface $loader)
89+
{
90+
if (!$config['enabled']) {
91+
return $loader->load('locking/null.xml');
92+
}
93+
94+
$loader->load('locking/services.xml');
95+
$container->setParameter('task.lock.ttl', $config['ttl']);
96+
}
97+
98+
/**
99+
* Find storage aliases and related ids.
100+
*
101+
* @param ContainerBuilder $container
102+
*
103+
* @return array
104+
*/
105+
private function getLockingStorageAliases(ContainerBuilder $container)
106+
{
107+
$taggedServices = $container->findTaggedServiceIds('task.lock.storage');
108+
109+
$result = [];
110+
foreach ($taggedServices as $id => $tags) {
111+
foreach ($tags as $tag) {
112+
$result[$tag['alias']] = $id;
113+
}
114+
}
115+
116+
return $result;
117+
}
118+
119+
/**
120+
* {@inheritdoc}
121+
*/
122+
public function getConfiguration(array $config, ContainerBuilder $container)
123+
{
124+
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
125+
$loader->load('locking/storages.xml');
126+
127+
return new Configuration($this->getLockingStorageAliases($container));
128+
}
73129
}

src/Entity/TaskExecutionRepository.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,16 +129,26 @@ public function findByTaskUuid($taskUuid)
129129
/**
130130
* {@inheritdoc}
131131
*/
132-
public function findScheduled()
132+
public function findNextScheduled(\DateTime $dateTime = null, array $skippedExecutions = [])
133133
{
134-
$query = $this->createQueryBuilder('e')
134+
$queryBuilder = $this->createQueryBuilder('e')
135135
->innerJoin('e.task', 't')
136136
->where('e.status = :status')
137137
->andWhere('e.scheduleTime < :date')
138-
->setParameter('date', new \DateTime())
138+
->setParameter('date', $dateTime ?: new \DateTime())
139139
->setParameter('status', TaskStatus::PLANNED)
140-
->getQuery();
140+
->setMaxResults(1);
141141

142-
return $query->getResult();
142+
$expr = $queryBuilder->expr();
143+
if (!empty($skippedExecutions)) {
144+
$queryBuilder->andWhere($expr->not($expr->in('e.uuid', ':skipped')))
145+
->setParameter('skipped', $skippedExecutions);
146+
}
147+
148+
try {
149+
return $queryBuilder->getQuery()->getSingleResult();
150+
} catch (NoResultException $exception) {
151+
return null;
152+
}
143153
}
144154
}

src/Locking/NullLock.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of php-task library.
5+
*
6+
* (c) php-task
7+
*
8+
* This source file is subject to the MIT license that is bundled
9+
* with this source code in the file LICENSE.
10+
*/
11+
12+
namespace Task\TaskBundle\Locking;
13+
14+
use Task\Lock\LockInterface;
15+
16+
/**
17+
* Implements LockInterface which does nothing.
18+
*/
19+
class NullLock implements LockInterface
20+
{
21+
/**
22+
* {@inheritdoc}
23+
*/
24+
public function acquire($task)
25+
{
26+
return true;
27+
}
28+
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function refresh($task)
33+
{
34+
return true;
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function release($task)
41+
{
42+
return true;
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public function isAcquired($task)
49+
{
50+
return false;
51+
}
52+
}

src/Resources/config/locking/null.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" ?>
2+
<container xmlns="http://symfony.com/schema/dic/services"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
5+
<services>
6+
<service id="task.lock" class="Task\TaskBundle\Locking\NullLock"/>
7+
</services>
8+
</container>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" ?>
2+
<container xmlns="http://symfony.com/schema/dic/services"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
5+
<services>
6+
<service id="task.lock" class="Task\Lock\Lock">
7+
<argument type="service" id="task.lock.storage"/>
8+
<argument>%task.lock.ttl%</argument>
9+
</service>
10+
</services>
11+
</container>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" ?>
2+
<container xmlns="http://symfony.com/schema/dic/services"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
5+
<services>
6+
<service id="task.lock.storage.file" class="Task\Lock\Storage\FileLockStorage">
7+
<argument type="expression">parameter('task.lock.storages.file')['directory']</argument>
8+
9+
<tag name="task.lock.storage" alias="file"/>
10+
</service>
11+
</services>
12+
</container>

src/Resources/config/scheduler.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
<service id="task.runner" class="Task\Runner\TaskRunner">
2020
<argument type="service" id="task.storage.task_execution"/>
2121
<argument type="service" id="task.handler.factory"/>
22+
<argument type="service" id="task.lock"/>
2223
<argument type="service" id="event_dispatcher"/>
24+
<argument type="service" id="logger" on-invalid="ignore"/>
2325
</service>
2426
</services>
2527
</container>

tests/Functional/Command/ScheduleTaskCommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public function testExecuteWithWorkloadAndIntervalAndEndDate()
9494
$this->assertEquals(TestHandler::class, $tasks[0]->getHandlerClass());
9595
$this->assertEquals('Test workload 1', $tasks[0]->getWorkload());
9696
$this->assertEquals('0 * * * *', $tasks[0]->getInterval());
97-
$this->assertEquals($date, $tasks[0]->getLastExecution());
97+
$this->assertEquals($date, $tasks[0]->getLastExecution(), '', 2);
9898
}
9999

100100
public function testExecuteWithExecutionDate()

tests/Functional/Entity/TaskExecutionRepositoryTest.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,8 @@ public function testFindScheduledPast()
154154

155155
$execution = $this->save($task, new \DateTime('-1 hour'));
156156

157-
$result = $this->taskExecutionRepository->findScheduled();
158-
159-
$this->assertCount(1, $result);
160-
$this->assertEquals($execution->getUuid(), $result[0]->getUuid());
157+
$result = $this->taskExecutionRepository->findNextScheduled();
158+
$this->assertEquals($execution->getUuid(), $result->getUuid());
161159
}
162160

163161
public function testFindScheduledFuture()
@@ -167,7 +165,17 @@ public function testFindScheduledFuture()
167165

168166
$this->save($task, new \DateTime('+1 hour'));
169167

170-
$this->assertEmpty($this->taskExecutionRepository->findScheduled());
168+
$this->assertNull($this->taskExecutionRepository->findNextScheduled());
169+
}
170+
171+
public function testFindScheduledSkipped()
172+
{
173+
$task = $this->createTask();
174+
$this->taskRepository->save($task);
175+
176+
$this->save($task, new \DateTime('+1 hour'));
177+
178+
$this->assertNull($this->taskExecutionRepository->findNextScheduled());
171179
}
172180

173181
/**

tests/Functional/Handler/TaskHandlerFactoryTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Task\TaskBundle\Tests\Functional\Handler;
1313

1414
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
15-
use Task\Handler\TaskHandlerNotExistsException;
1615
use Task\TaskBundle\Handler\TaskHandlerFactory;
1716
use Task\TaskBundle\Tests\Functional\TestHandler;
1817

@@ -39,10 +38,11 @@ public function testCreate()
3938
$this->assertInstanceOf(TestHandler::class, $this->taskHandlerFactory->create(TestHandler::class));
4039
}
4140

41+
/**
42+
* @expectedException \Task\Handler\TaskHandlerNotExistsException
43+
*/
4244
public function testCreateNotExists()
4345
{
44-
$this->setExpectedException(TaskHandlerNotExistsException::class);
45-
4646
$this->taskHandlerFactory->create(\stdClass::class);
4747
}
4848
}

tests/app/config/config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
parameters:
22
kernel.secret: 12345
3+
4+
task:
5+
locking:
6+
storages:
7+
file:
8+
directory: %kernel.cache_dir%/locks

0 commit comments

Comments
 (0)