diff --git a/composer.json b/composer.json index 37c18da..a61abd1 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,10 @@ ], "require": { "php": "~5.5 || ~7.0", - "php-task/php-task": "dev-develop", + "php-task/php-task": "dev-master", "symfony/http-kernel": "^2.6 || ^3.0", "symfony/dependency-injection": "^2.6 || ^3.0", + "symfony/expression-language": "^2.6 || ^3.0", "symfony/config": "^2.6 || ^3.0", "symfony/console": "^2.6 || ^3.0", "doctrine/orm": "^2.5" diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 7e8ba92..553fd1f 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -19,6 +19,19 @@ */ class Configuration implements ConfigurationInterface { + /** + * @var string[] + */ + private $lockingStorageAliases = []; + + /** + * @param \string[] $lockingStorageAliases + */ + public function __construct(array $lockingStorageAliases) + { + $this->lockingStorageAliases = $lockingStorageAliases; + } + /** * {@inheritdoc} */ @@ -43,9 +56,28 @@ public function getConfigTreeBuilder() ->arrayNode('run') ->addDefaultsIfNotSet() ->children() - ->enumNode('mode') - ->values(['off', 'listener']) - ->defaultValue('off') + ->enumNode('mode')->values(['off', 'listener'])->defaultValue('off')->end() + ->end() + ->end() + ->arrayNode('locking') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->children() + ->enumNode('storage') + ->values(array_keys($this->lockingStorageAliases)) + ->defaultValue('file') + ->end() + ->integerNode('ttl')->defaultValue(600)->end() + ->arrayNode('storages') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('file') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('directory')->defaultValue('%kernel.cache_dir%/tasks')->end() + ->end() + ->end() + ->end() ->end() ->end() ->end() @@ -63,4 +95,16 @@ public function getConfigTreeBuilder() return $treeBuilder; } + + /** + * Returns id for given storage-alias. + * + * @param string $alias + * + * @return string + */ + public function getLockingStorageId($alias) + { + return $this->lockingStorageAliases[$alias]; + } } diff --git a/src/DependencyInjection/TaskExtension.php b/src/DependencyInjection/TaskExtension.php index c22ec65..9452d54 100644 --- a/src/DependencyInjection/TaskExtension.php +++ b/src/DependencyInjection/TaskExtension.php @@ -12,6 +12,7 @@ namespace Task\TaskBundle\DependencyInjection; use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; @@ -31,23 +32,29 @@ class TaskExtension extends Extension */ public function load(array $configs, ContainerBuilder $container) { - $configuration = new Configuration(); + $configuration = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($configuration, $configs); $container->setParameter('task.system_tasks', $config['system_tasks']); - $container->setParameter('task.storage', $config['storage']); + + $container->setAlias('task.lock.storage', $configuration->getLockingStorageId($config['locking']['storage'])); + foreach (array_keys($config['locking']['storages']) as $key) { + $container->setParameter('task.lock.storages.' . $key, $config['locking']['storages'][$key]); + } $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load(sprintf('storage/%s.xml', $config['storage'])); $loader->load('task_event_listener.xml'); $loader->load('scheduler.xml'); $loader->load('command.xml'); + $loader->load('locking/services.xml'); if ($config['run']['mode'] === 'listener') { $loader->load('listener.xml'); } $this->loadDoctrineAdapter($config['adapters']['doctrine'], $container); + $this->loadLockingComponent($config['locking'], $container, $loader); } /** @@ -70,4 +77,53 @@ private function loadDoctrineAdapter(array $config, ContainerBuilder $container) $container->setDefinition('task.adapter.doctrine.execution_listener', $definition); } } + + /** + * Load services for locking component. + * + * @param array $config + * @param LoaderInterface $loader + * @param ContainerBuilder $container + */ + private function loadLockingComponent(array $config, ContainerBuilder $container, LoaderInterface $loader) + { + if (!$config['enabled']) { + return $loader->load('locking/null.xml'); + } + + $loader->load('locking/services.xml'); + $container->setParameter('task.lock.ttl', $config['ttl']); + } + + /** + * Find storage aliases and related ids. + * + * @param ContainerBuilder $container + * + * @return array + */ + private function getLockingStorageAliases(ContainerBuilder $container) + { + $taggedServices = $container->findTaggedServiceIds('task.lock.storage'); + + $result = []; + foreach ($taggedServices as $id => $tags) { + foreach ($tags as $tag) { + $result[$tag['alias']] = $id; + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function getConfiguration(array $config, ContainerBuilder $container) + { + $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('locking/storages.xml'); + + return new Configuration($this->getLockingStorageAliases($container)); + } } diff --git a/src/Entity/TaskExecutionRepository.php b/src/Entity/TaskExecutionRepository.php index bf897e9..3bb6db5 100644 --- a/src/Entity/TaskExecutionRepository.php +++ b/src/Entity/TaskExecutionRepository.php @@ -129,16 +129,26 @@ public function findByTaskUuid($taskUuid) /** * {@inheritdoc} */ - public function findScheduled() + public function findNextScheduled(\DateTime $dateTime = null, array $skippedExecutions = []) { - $query = $this->createQueryBuilder('e') + $queryBuilder = $this->createQueryBuilder('e') ->innerJoin('e.task', 't') ->where('e.status = :status') ->andWhere('e.scheduleTime < :date') - ->setParameter('date', new \DateTime()) + ->setParameter('date', $dateTime ?: new \DateTime()) ->setParameter('status', TaskStatus::PLANNED) - ->getQuery(); + ->setMaxResults(1); - return $query->getResult(); + $expr = $queryBuilder->expr(); + if (!empty($skippedExecutions)) { + $queryBuilder->andWhere($expr->not($expr->in('e.uuid', ':skipped'))) + ->setParameter('skipped', $skippedExecutions); + } + + try { + return $queryBuilder->getQuery()->getSingleResult(); + } catch (NoResultException $exception) { + return null; + } } } diff --git a/src/Locking/NullLock.php b/src/Locking/NullLock.php new file mode 100644 index 0000000..cb0c914 --- /dev/null +++ b/src/Locking/NullLock.php @@ -0,0 +1,52 @@ + + + + + + diff --git a/src/Resources/config/locking/services.xml b/src/Resources/config/locking/services.xml new file mode 100644 index 0000000..25488c8 --- /dev/null +++ b/src/Resources/config/locking/services.xml @@ -0,0 +1,11 @@ + + + + + + %task.lock.ttl% + + + diff --git a/src/Resources/config/locking/storages.xml b/src/Resources/config/locking/storages.xml new file mode 100644 index 0000000..35e2572 --- /dev/null +++ b/src/Resources/config/locking/storages.xml @@ -0,0 +1,12 @@ + + + + + parameter('task.lock.storages.file')['directory'] + + + + + diff --git a/src/Resources/config/scheduler.xml b/src/Resources/config/scheduler.xml index 983dd6a..9b5c183 100644 --- a/src/Resources/config/scheduler.xml +++ b/src/Resources/config/scheduler.xml @@ -19,7 +19,9 @@ + + diff --git a/tests/Functional/Command/ScheduleTaskCommandTest.php b/tests/Functional/Command/ScheduleTaskCommandTest.php index 85d1451..1551532 100644 --- a/tests/Functional/Command/ScheduleTaskCommandTest.php +++ b/tests/Functional/Command/ScheduleTaskCommandTest.php @@ -94,7 +94,7 @@ public function testExecuteWithWorkloadAndIntervalAndEndDate() $this->assertEquals(TestHandler::class, $tasks[0]->getHandlerClass()); $this->assertEquals('Test workload 1', $tasks[0]->getWorkload()); $this->assertEquals('0 * * * *', $tasks[0]->getInterval()); - $this->assertEquals($date, $tasks[0]->getLastExecution()); + $this->assertEquals($date, $tasks[0]->getLastExecution(), '', 2); } public function testExecuteWithExecutionDate() diff --git a/tests/Functional/Entity/TaskExecutionRepositoryTest.php b/tests/Functional/Entity/TaskExecutionRepositoryTest.php index f5cb54e..1716f3a 100644 --- a/tests/Functional/Entity/TaskExecutionRepositoryTest.php +++ b/tests/Functional/Entity/TaskExecutionRepositoryTest.php @@ -154,10 +154,8 @@ public function testFindScheduledPast() $execution = $this->save($task, new \DateTime('-1 hour')); - $result = $this->taskExecutionRepository->findScheduled(); - - $this->assertCount(1, $result); - $this->assertEquals($execution->getUuid(), $result[0]->getUuid()); + $result = $this->taskExecutionRepository->findNextScheduled(); + $this->assertEquals($execution->getUuid(), $result->getUuid()); } public function testFindScheduledFuture() @@ -167,7 +165,17 @@ public function testFindScheduledFuture() $this->save($task, new \DateTime('+1 hour')); - $this->assertEmpty($this->taskExecutionRepository->findScheduled()); + $this->assertNull($this->taskExecutionRepository->findNextScheduled()); + } + + public function testFindScheduledSkipped() + { + $task = $this->createTask(); + $this->taskRepository->save($task); + + $this->save($task, new \DateTime('+1 hour')); + + $this->assertNull($this->taskExecutionRepository->findNextScheduled()); } /** diff --git a/tests/Functional/Handler/TaskHandlerFactoryTest.php b/tests/Functional/Handler/TaskHandlerFactoryTest.php index 5510b6c..df20342 100644 --- a/tests/Functional/Handler/TaskHandlerFactoryTest.php +++ b/tests/Functional/Handler/TaskHandlerFactoryTest.php @@ -12,7 +12,6 @@ namespace Task\TaskBundle\Tests\Functional\Handler; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Task\Handler\TaskHandlerNotExistsException; use Task\TaskBundle\Handler\TaskHandlerFactory; use Task\TaskBundle\Tests\Functional\TestHandler; @@ -39,10 +38,11 @@ public function testCreate() $this->assertInstanceOf(TestHandler::class, $this->taskHandlerFactory->create(TestHandler::class)); } + /** + * @expectedException \Task\Handler\TaskHandlerNotExistsException + */ public function testCreateNotExists() { - $this->setExpectedException(TaskHandlerNotExistsException::class); - $this->taskHandlerFactory->create(\stdClass::class); } } diff --git a/tests/app/config/config.yml b/tests/app/config/config.yml index c751a29..71793c9 100644 --- a/tests/app/config/config.yml +++ b/tests/app/config/config.yml @@ -1,2 +1,8 @@ parameters: kernel.secret: 12345 + +task: + locking: + storages: + file: + directory: %kernel.cache_dir%/locks