├── bin ├── phpunit ├── composer └── composer.phar ├── script ├── test ├── bootstrap ├── setperm └── updatedeps ├── contributors.txt ├── src ├── Tests │ ├── DependencyInjection │ │ ├── fixtures │ │ │ └── config.yml │ │ ├── GloobyTaskExtensionTest.php │ │ ├── ConfigurationTest.php │ │ └── Complier │ │ │ └── RegisterSchedulesPassTest.php │ ├── Annotation │ │ └── ScheduleTest.php │ └── Schedule │ │ └── ScheduleRegistryTest.php ├── Task │ ├── TaskInterface.php │ ├── OutputAwareInterface.php │ ├── QueuedTaskAwareInterface.php │ ├── PingTask.php │ └── TaskRunner.php ├── GloobyTaskBundle.php ├── DependencyInjection │ ├── Configuration.php │ ├── GloobyTaskExtension.php │ └── Compiler │ │ └── RegisterSchedulesPass.php ├── Command │ ├── Scheduler │ │ ├── PruneCommand.php │ │ ├── SyncCommand.php │ │ └── RunCommand.php │ └── Task │ │ └── RunCommand.php ├── Queue │ ├── QueueMonitor.php │ ├── QueuePruner.php │ ├── QueueProcessor.php │ └── QueueScheduler.php ├── Entity │ ├── ScheduleRepository.php │ ├── Schedule.php │ ├── QueuedTask.php │ └── QueuedTaskRepository.php ├── Model │ ├── ScheduleInterface.php │ ├── QueuedTaskInterface.php │ ├── Schedule.php │ └── QueuedTask.php ├── Schedule │ └── ScheduleRegistry.php ├── Manager │ └── TaskManager.php ├── Synchronizer │ └── ScheduleSynchronizer.php ├── Resources │ └── config │ │ └── services.yml └── Annotation │ └── Schedule.php ├── .gitignore ├── CHANGELOG.md ├── .coveralls.yml ├── .travis.yml ├── composer.json ├── LICENSE.md ├── phpunit.xml.dist ├── CONTRIBUTING.md ├── .scrutinizer.yml └── README.md /bin/phpunit: -------------------------------------------------------------------------------- 1 | phpunit.phar -------------------------------------------------------------------------------- /bin/composer: -------------------------------------------------------------------------------- 1 | composer.phar -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | php bin/phpunit 4 | -------------------------------------------------------------------------------- /contributors.txt: -------------------------------------------------------------------------------- 1 | Emil Kilhage 2 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bin/composer install 4 | -------------------------------------------------------------------------------- /src/Tests/DependencyInjection/fixtures/config.yml: -------------------------------------------------------------------------------- 1 | glooby_task: ~ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /build/ 3 | /clover.cov 4 | .idea 5 | composer.lock 6 | -------------------------------------------------------------------------------- /bin/composer.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kilhage/task-bundle/HEAD/bin/composer.phar -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 3.0.0 4 | ----- 5 | 6 | * Enable PHP7 7 | 8 | 0.1.0 9 | ----- 10 | 11 | * Initial version 12 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | repo_token: kMpT0qkOPmPt8jjY03mlmvdB8F7nIdynx 3 | coverage_clover: build/logs/clover.xml 4 | json_path: build/logs/coveralls-upload.json 5 | -------------------------------------------------------------------------------- /script/setperm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | find . -type d -exec chmod 0755 {} \; 4 | find . -type f -exec chmod 0644 {} \; 5 | 6 | find script -type f -exec chmod 0744 {} \; 7 | find bin -type f -exec chmod 0744 {} \; 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | 7 | before_script: composer install -n 8 | 9 | script: vendor/phpunit/phpunit/phpunit --coverage-clover build/logs/clover.xml 10 | 11 | after_script: php vendor/bin/coveralls -v 12 | -------------------------------------------------------------------------------- /src/Task/TaskInterface.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new RegisterSchedulesPass()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | setParameter('kernel.debug', true); 21 | 22 | $extension = new GloobyTaskExtension(); 23 | 24 | $extension->load($config, $containerBuilder); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glooby/task-bundle", 3 | "license": "MIT", 4 | "type": "symfony-bundle", 5 | "description": "Scheduling of tasks for symfony made simple", 6 | "keywords": ["cron", "symfony", "scheduler", "task", "bundle", "glooby"], 7 | "homepage": "https://www.glooby.se", 8 | "authors": [ 9 | { 10 | "name": "Emil Kilhage", 11 | "email": "emil.kilhage@glooby.com", 12 | "homepage": "https://www.glooby.se" 13 | } 14 | ], 15 | "autoload": { 16 | "psr-4": { "Glooby\\TaskBundle\\": "src/" } 17 | }, 18 | "require": { 19 | "php": ">=7.1", 20 | "symfony/framework-bundle": "~4.3", 21 | "doctrine/orm": "~2.6", 22 | "dragonmantank/cron-expression": "~2.0" 23 | }, 24 | "require-dev": { 25 | "phpspec/prophecy": "~1.8", 26 | "php-coveralls/php-coveralls": "~2.1", 27 | "symfony/phpunit-bridge": "^4.3", 28 | "phpunit/phpunit": "^7" 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DependencyInjection/GloobyTaskExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 24 | 25 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 26 | $loader->load('services.yml'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tests/DependencyInjection/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | processor = new Processor(); 25 | } 26 | 27 | private function getConfigs(array $configArray) 28 | { 29 | $configuration = new Configuration(true); 30 | 31 | return $this->processor->processConfiguration($configuration, array($configArray)); 32 | } 33 | 34 | public function testUnConfiguredConfiguration() 35 | { 36 | $configuration = $this->getConfigs(array()); 37 | $this->assertSame(array(), $configuration); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Glooby AB 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | src/Tests 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | src 28 | 29 | src/*/Resources 30 | src/*/Tests 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Command/Scheduler/PruneCommand.php: -------------------------------------------------------------------------------- 1 | container = $container; 21 | } 22 | 23 | /** 24 | * Configures the current command. 25 | */ 26 | protected function configure() 27 | { 28 | $this->setName('scheduler:prune'); 29 | $this->addOption('all', 'A', InputOption::VALUE_NONE); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function execute(InputInterface $input, OutputInterface $output) 36 | { 37 | $pruner =$this->container->get('glooby_task.queue_pruner'); 38 | 39 | if ($input->getOption('all')) { 40 | $pruner->all(); 41 | } else { 42 | $pruner->run(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Command/Scheduler/SyncCommand.php: -------------------------------------------------------------------------------- 1 | container = $container; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected function configure() 27 | { 28 | $this->setName('scheduler:sync'); 29 | $this->addOption('silent', 'S', InputOption::VALUE_NONE); 30 | $this->addOption('force', 'F', InputOption::VALUE_NONE); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | protected function execute(InputInterface $input, OutputInterface $output) 37 | { 38 | $client = $this->container->get('glooby_task.schedule_synchronizer'); 39 | $client->setForce($input->getOption('force')); 40 | $client->sync(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Queue/QueueMonitor.php: -------------------------------------------------------------------------------- 1 | doctrine = $doctrine; 24 | } 25 | 26 | /** 27 | * @var TaskManager 28 | */ 29 | private $taskManager; 30 | 31 | /** 32 | * @param TaskManager $taskManager 33 | */ 34 | public function setTaskManager(TaskManager $taskManager) 35 | { 36 | $this->taskManager = $taskManager; 37 | } 38 | 39 | /** 40 | * 41 | */ 42 | public function monitor() 43 | { 44 | $taskRepo = $this->doctrine->getManager() 45 | ->getRepository('GloobyTaskBundle:QueuedTask'); 46 | 47 | foreach ($taskRepo->findRunning() as $task) { 48 | if ($task->hasPId() && false === posix_getpgid($task->getPId())) { 49 | $this->taskManager->failure($task, 'crashed'); 50 | } 51 | } 52 | 53 | $this->doctrine->getManager()->flush(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | First of all, **thank you** for contributing, **you are awesome**! 5 | 6 | Here are a few rules to follow in order to ease code reviews, and discussions before 7 | maintainers accept and merge your work. 8 | 9 | You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/) and 10 | [PSR-2](http://www.php-fig.org/psr/2/). If you don't know about any of them, you 11 | should really read the recommendations. Can't wait? Use the [PHP-CS-Fixer 12 | tool](http://cs.sensiolabs.org/). 13 | 14 | You MUST run the test suite. 15 | 16 | You MUST write (or update) unit tests. 17 | 18 | You SHOULD write documentation. 19 | 20 | Please, write [commit messages that make 21 | sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), 22 | and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing) 23 | before submitting your Pull Request. 24 | 25 | One may ask you to [squash your 26 | commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) 27 | too. This is used to "clean" your Pull Request before merging it (we don't want 28 | commits such as `fix tests`, `fix 2`, `fix 3`, etc.). 29 | 30 | Also, while creating your Pull Request on GitHub, you MUST write a description 31 | which gives the context and/or explains why you are creating it. 32 | 33 | Thank you! 34 | -------------------------------------------------------------------------------- /src/Command/Scheduler/RunCommand.php: -------------------------------------------------------------------------------- 1 | container = $container; 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function configure() 26 | { 27 | $this->setName('scheduler:run'); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function execute(InputInterface $input, OutputInterface $output) 34 | { 35 | $runner = $this->container->get('glooby_task.queue_processor'); 36 | $runner->setOutput($output); 37 | $runner->process(); 38 | 39 | $monitor = $this->container->get('glooby_task.queue_monitor'); 40 | $monitor->monitor(); 41 | 42 | $scheduler = $this->container->get('glooby_task.queue_scheduler'); 43 | $scheduler->schedule(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | # .scrutinizer.yml 2 | checks: 3 | php: 4 | code_rating: true 5 | duplication: true 6 | deadlock_detection_in_loops: true 7 | remove_extra_empty_lines: true 8 | remove_php_closing_tag: true 9 | remove_trailing_whitespace: true 10 | fix_use_statements: 11 | remove_unused: true 12 | preserve_multiple: false 13 | preserve_blanklines: true 14 | order_alphabetically: true 15 | fix_php_opening_tag: true 16 | fix_linefeed: true 17 | fix_line_ending: true 18 | fix_identation_4spaces: true 19 | fix_doc_comments: true 20 | 21 | tools: 22 | php_code_coverage: true 23 | php_code_sniffer: 24 | config: { standard: 'PSR2' } 25 | php_changetracking: true 26 | php_cpd: 27 | enabled: false 28 | excluded_dirs: [vendor] 29 | php_cs_fixer: 30 | config: { level: 'psr2' } 31 | php_mess_detector: true 32 | php_pdepend: true 33 | php_analyzer: true 34 | sensiolabs_security_checker: true 35 | php_sim: true 36 | php_loc: 37 | enabled: true 38 | excluded_dirs: [vendor] 39 | 40 | filter: 41 | paths: [ 'src/*' ] 42 | excluded_paths: 43 | - vendor/* 44 | - src/Tests/* 45 | 46 | application: 47 | environment: 48 | php: 49 | version: 5.6.0 50 | -------------------------------------------------------------------------------- /src/Entity/ScheduleRepository.php: -------------------------------------------------------------------------------- 1 | getEntityManager() 24 | ->createQuery('SELECT r FROM GloobyTaskBundle:Schedule r WHERE r.name = :name') 25 | ->setParameter('name', $name) 26 | ->useQueryCache(true) 27 | ->getSingleResult(); 28 | } 29 | 30 | /** 31 | * @param array $names 32 | * 33 | * @return ScheduleInterface[] 34 | */ 35 | public function findNotInNames(array $names) 36 | { 37 | return $this->getEntityManager() 38 | ->createQuery('SELECT r FROM GloobyTaskBundle:Schedule r WHERE r.name NOT IN (:names)') 39 | ->setParameter('names', $names) 40 | ->useQueryCache(true) 41 | ->getResult(); 42 | } 43 | 44 | /** 45 | * @return ScheduleInterface[] 46 | */ 47 | public function findActive() 48 | { 49 | return $this->getEntityManager() 50 | ->createQuery('SELECT r FROM GloobyTaskBundle:Schedule r WHERE r.active = true') 51 | ->useQueryCache(true) 52 | ->getResult(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Queue/QueuePruner.php: -------------------------------------------------------------------------------- 1 | doctrine = $doctrine; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function run(array $params = []) 36 | { 37 | /** @var Connection $connection */ 38 | $connection = $this->doctrine->getConnection(); 39 | 40 | $deleted = $connection->exec('DELETE FROM task_queue 41 | WHERE resolution = "success" 42 | AND created <= DATE_SUB(NOW(), INTERVAL 1 MONTH)'); 43 | 44 | $deleted += $connection->exec('DELETE FROM task_queue 45 | WHERE resolution != "success" 46 | AND created <= DATE_SUB(NOW(), INTERVAL 3 MONTH)'); 47 | 48 | return sprintf('%d deleted', $deleted); 49 | } 50 | 51 | /** 52 | * 53 | */ 54 | public function all() 55 | { 56 | /** @var Connection $connection */ 57 | $connection = $this->doctrine->getConnection(); 58 | $deleted = $connection->exec('DELETE FROM task_queue'); 59 | return sprintf('%d deleted', $deleted); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Model/ScheduleInterface.php: -------------------------------------------------------------------------------- 1 | '@daily']); 16 | 17 | $this->assertEquals('0 0 * * *', $schedule->interval); 18 | $this->assertTrue($schedule->active); 19 | } 20 | 21 | public function testInterval() 22 | { 23 | $schedule = new Schedule(['interval' => '@daily']); 24 | 25 | $this->assertEquals('0 0 * * *', $schedule->interval); 26 | $this->assertTrue($schedule->active); 27 | } 28 | 29 | public function testIntervalActiveParams() 30 | { 31 | $schedule = new Schedule([ 32 | 'interval' => '@daily', 33 | 'active' => false, 34 | 'params' => [1], 35 | ]); 36 | 37 | $this->assertEquals('0 0 * * *', $schedule->interval); 38 | $this->assertEquals([1], $schedule->params); 39 | $this->assertFalse($schedule->active); 40 | } 41 | 42 | public function testParamsNonArray() 43 | { 44 | $this->expectException('\InvalidArgumentException'); 45 | 46 | new Schedule([ 47 | 'interval' => '@daily', 48 | 'active' => false, 49 | 'params' => 'foo', 50 | ]); 51 | } 52 | 53 | public function testInvalidProperty() 54 | { 55 | $this->expectException('\InvalidArgumentException'); 56 | 57 | new Schedule(['fooo' => '@daily']); 58 | } 59 | 60 | public function testInvalidInterval() 61 | { 62 | $this->expectException('\InvalidArgumentException'); 63 | 64 | new Schedule(['interval' => 'fdsfds fds']); 65 | } 66 | 67 | public function testMissingInvalid() 68 | { 69 | $this->expectException('\InvalidArgumentException'); 70 | 71 | new Schedule([]); 72 | } 73 | 74 | public function testInvalidTimeout() 75 | { 76 | $this->expectException('\InvalidArgumentException'); 77 | 78 | new Schedule(['interval' => '@daily', 'timeout' => 'x']); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Schedule/ScheduleRegistry.php: -------------------------------------------------------------------------------- 1 | reader = $reader; 34 | } 35 | 36 | /** 37 | * @param string $id 38 | */ 39 | public function addTask($id) 40 | { 41 | $this->tasks[] = $id; 42 | } 43 | 44 | /** 45 | * @return Schedule[] 46 | */ 47 | public function getSchedules() 48 | { 49 | $schedules = []; 50 | 51 | foreach ($this->tasks as $taskId) { 52 | $schedules[$taskId] = $this->getAnnotation($taskId); 53 | } 54 | 55 | return $schedules; 56 | } 57 | 58 | /** 59 | * @param string $taskId 60 | * 61 | * @return array 62 | */ 63 | private function getAnnotation($taskId) 64 | { 65 | $task = $this->container->get($taskId); 66 | 67 | $reflectionObject = new \ReflectionObject($task); 68 | $annotation = $this->reader->getClassAnnotation($reflectionObject, self::ANNOTATION_CLASS); 69 | 70 | $this->guardAgainstInvalidAnnotation($annotation, $task); 71 | 72 | return $annotation; 73 | } 74 | 75 | /** 76 | * @param $annotation 77 | * @param $task 78 | */ 79 | private function guardAgainstInvalidAnnotation($annotation, $task) 80 | { 81 | if (!is_a($annotation, self::ANNOTATION_CLASS)) { 82 | throw new \InvalidArgumentException( 83 | sprintf( 84 | 'class %s is missing the Schedule annotation %s', 85 | get_class($task), 86 | self::ANNOTATION_CLASS 87 | ) 88 | ); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/RegisterSchedulesPass.php: -------------------------------------------------------------------------------- 1 | getDefinition('glooby_task.schedule_registry'); 29 | 30 | foreach ($container->findTaggedServiceIds('glooby.scheduled_task') as $taskId => $tags) { 31 | $this->add($container, $taskId, $registry); 32 | } 33 | } 34 | 35 | /** 36 | * Returns whether the class implements TaskInterface. 37 | * 38 | * @param string $class 39 | * 40 | * @return bool 41 | */ 42 | private function isTaskImplementation($class) 43 | { 44 | if (!isset($this->implementations[$class])) { 45 | $reflectionClass = new \ReflectionClass($class); 46 | $this->implementations[$class] = $reflectionClass->implementsInterface(TaskInterface::class); 47 | } 48 | 49 | return $this->implementations[$class]; 50 | } 51 | 52 | /** 53 | * @param string $class 54 | * @param string $taskId 55 | */ 56 | protected function validateClass($class, $taskId) 57 | { 58 | if (!empty($class)) { 59 | if (!class_exists($class)) { 60 | throw new \InvalidArgumentException('Invalid class: ' . $class.', task: '.$taskId); 61 | } 62 | 63 | if (!$this->isTaskImplementation($class)) { 64 | throw new \InvalidArgumentException(sprintf('schedule "%s" with class "%s" must implement TaskInterface.', $taskId, $class)); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * @param ContainerBuilder $container 71 | * @param string $taskId 72 | * @param Definition $registry 73 | */ 74 | protected function add(ContainerBuilder $container, $taskId, Definition $registry) 75 | { 76 | $definition = $container->getDefinition($taskId); 77 | $class = $definition->getClass(); 78 | 79 | $this->validateClass($class, $taskId); 80 | 81 | $registry->addMethodCall('addTask', [$taskId]); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Command/Task/RunCommand.php: -------------------------------------------------------------------------------- 1 | container = $container; 25 | } 26 | 27 | /** 28 | * Configures the current command. 29 | */ 30 | protected function configure() 31 | { 32 | $this->setName('task:run'); 33 | $this->addArgument('service', InputArgument::OPTIONAL); 34 | $this->addOption('silent', 'S', InputOption::VALUE_NONE); 35 | $this->addOption('id', null, InputOption::VALUE_REQUIRED); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function execute(InputInterface $input, OutputInterface $output) 42 | { 43 | $runner = $this->container->get('glooby_task.task_runner'); 44 | $runner->setOutput($output); 45 | 46 | if ($input->getOption('id')) { 47 | $response = $this->runId($input, $runner); 48 | 49 | if (!$input->getOption('silent')) { 50 | if (!empty($response)) { 51 | $output->writeln("task {$input->getOption('id')} finished: $response"); 52 | } else { 53 | $output->writeln("task {$input->getOption('id')} finished"); 54 | } 55 | } 56 | } else { 57 | $response = $runner->runTask($input->getArgument('service')); 58 | 59 | if (!$input->getOption('silent')) { 60 | $output->writeln($response); 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * @param InputInterface $input 67 | * @param TaskRunner $runner 68 | * @throws NoResultException 69 | */ 70 | protected function runId(InputInterface $input, TaskRunner $runner) 71 | { 72 | $task = $this->container 73 | ->get('doctrine') 74 | ->getManager() 75 | ->getRepository('GloobyTaskBundle:QueuedTask') 76 | ->find($input->getOption('id')); 77 | 78 | if (null === $task) { 79 | throw new NoResultException(); 80 | } 81 | 82 | return $runner->run($task); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Model/QueuedTaskInterface.php: -------------------------------------------------------------------------------- 1 | doctrine = $doctrine; 27 | } 28 | 29 | /** 30 | * @param string $service 31 | * @param \DateTime|null $executeAt 32 | * @param array|null $params 33 | * @return QueuedTaskInterface 34 | */ 35 | public function queue($service, \DateTime $executeAt = null, array $params = null) 36 | { 37 | $task = new QueuedTask($service, $params, $executeAt); 38 | $this->populateSchedule($task, $service); 39 | $this->doctrine->getManager()->persist($task); 40 | return $task; 41 | } 42 | 43 | /** 44 | * @param QueuedTaskInterface $task 45 | */ 46 | public function start(QueuedTaskInterface $task) 47 | { 48 | $task->start(); 49 | } 50 | 51 | /** 52 | * @param string $service 53 | * @param array $params 54 | * @return QueuedTaskInterface 55 | */ 56 | public function run($service, array $params = null) 57 | { 58 | $task = new QueuedTask($service, $params); 59 | $task->start(); 60 | $this->populateSchedule($task, $service); 61 | 62 | $this->doctrine->getManager()->persist($task); 63 | $this->save(); 64 | 65 | return $task; 66 | } 67 | 68 | /** 69 | * @param QueuedTaskInterface $task 70 | * @param $response 71 | */ 72 | public function success(QueuedTaskInterface $task, $response) 73 | { 74 | $task->success($response); 75 | $this->save(); 76 | } 77 | 78 | /** 79 | * @param QueuedTaskInterface $task 80 | * @param $response 81 | */ 82 | public function failure(QueuedTaskInterface $task, $response) 83 | { 84 | $task->failure($response); 85 | $this->save(); 86 | } 87 | 88 | /** 89 | * @param QueuedTaskInterface $task 90 | * @param string $service 91 | */ 92 | private function populateSchedule(QueuedTaskInterface $task, $service) 93 | { 94 | try { 95 | /** @var ScheduleRepository $repo */ 96 | $repo = $this->doctrine->getManager() 97 | ->getRepository('GloobyTaskBundle:Schedule'); 98 | $schedule = $repo->findByName($service); 99 | $task->setSchedule($schedule); 100 | } catch (NoResultException $e) { 101 | // ignore if not found 102 | } 103 | } 104 | 105 | /** 106 | * 107 | */ 108 | private function save() 109 | { 110 | $this->doctrine->getManager()->flush(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Tests/Schedule/ScheduleRegistryTest.php: -------------------------------------------------------------------------------- 1 | prophesize(ContainerInterface::class); 20 | $reader = $this->prophesize(Reader::class); 21 | 22 | $registry = new ScheduleRegistry(); 23 | $registry->setContainer($container->reveal()); 24 | $registry->setReader($reader->reveal()); 25 | 26 | $registry->addTask('task.foo.a'); 27 | $registry->addTask('task.foo.b'); 28 | 29 | $container->get('task.foo.a')->willReturn(new \stdClass()); 30 | $container->get('task.foo.b')->willReturn(new \stdClass()); 31 | 32 | $schedule1 = new Schedule(['value' => '@daily']); 33 | 34 | $reader->getClassAnnotation(Argument::type(\ReflectionObject::class), ScheduleRegistry::ANNOTATION_CLASS) 35 | ->shouldBeCalled() 36 | ->willReturn($schedule1); 37 | 38 | $schedules = $registry->getSchedules(); 39 | 40 | $this->assertTrue(isset($schedules['task.foo.a'])); 41 | $this->assertSame($schedule1, $schedules['task.foo.a']); 42 | 43 | $this->assertTrue(isset($schedules['task.foo.b'])); 44 | $this->assertSame($schedule1, $schedules['task.foo.b']); 45 | } 46 | 47 | public function testEmpty() 48 | { 49 | $container = $this->prophesize(ContainerInterface::class); 50 | $reader = $this->prophesize(Reader::class); 51 | 52 | $registry = new ScheduleRegistry(); 53 | $registry->setContainer($container->reveal()); 54 | $registry->setReader($reader->reveal()); 55 | 56 | $reader->getClassAnnotation(Argument::type(\ReflectionObject::class), ScheduleRegistry::ANNOTATION_CLASS) 57 | ->shouldNotBeCalled() 58 | ->willReturn(null); 59 | 60 | $schedules = $registry->getSchedules(); 61 | 62 | $this->assertEmpty($schedules); 63 | } 64 | 65 | public function testMissingScheduleAnnotation() 66 | { 67 | $container = $this->prophesize(ContainerInterface::class); 68 | $reader = $this->prophesize(Reader::class); 69 | 70 | $registry = new ScheduleRegistry(); 71 | $registry->setContainer($container->reveal()); 72 | $registry->setReader($reader->reveal()); 73 | 74 | $registry->addTask('task.foo.a'); 75 | $registry->addTask('task.foo.b'); 76 | 77 | $container->get('task.foo.a')->willReturn(new \stdClass()); 78 | $container->get('task.foo.b')->willReturn(new \stdClass()); 79 | 80 | $reader->getClassAnnotation(Argument::type(\ReflectionObject::class), ScheduleRegistry::ANNOTATION_CLASS) 81 | ->shouldBeCalled() 82 | ->willReturn(null); 83 | 84 | $this->expectException('\InvalidArgumentException'); 85 | 86 | $registry->getSchedules(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Entity/QueuedTask.php: -------------------------------------------------------------------------------- 1 | output = $output; 31 | } 32 | 33 | /** 34 | * @var TaskManager 35 | */ 36 | private $taskManager; 37 | 38 | /** 39 | * @param TaskManager $taskManager 40 | */ 41 | public function setTaskManager(TaskManager $taskManager) 42 | { 43 | $this->taskManager = $taskManager; 44 | } 45 | 46 | /** 47 | * @param QueuedTask $run 48 | * @return array 49 | * @throws \Exception 50 | */ 51 | public function run(QueuedTask $run) 52 | { 53 | $task = $this->container->get($run->getName()); 54 | 55 | if (!($task instanceof TaskInterface)) { 56 | throw new \InvalidArgumentException($run->getName().' does not implement TaskInterface'); 57 | } 58 | 59 | $this->taskManager->start($run); 60 | 61 | return $this->execute($task, $run->getParams(), $run); 62 | } 63 | 64 | /** 65 | * @param string $name 66 | * @param array $params 67 | * @return array 68 | * @throws \Exception 69 | */ 70 | public function runTask($name, array $params = []) 71 | { 72 | $task = $this->container->get($name); 73 | 74 | if (!($task instanceof TaskInterface)) { 75 | throw new \InvalidArgumentException($name.' does not implement TaskInterface'); 76 | } 77 | 78 | $run = $this->taskManager->run($name, $params); 79 | 80 | return $this->execute($task, $params, $run); 81 | } 82 | 83 | /** 84 | * @param TaskInterface $task 85 | * @param array $params 86 | * @param QueuedTaskInterface $run 87 | * @return mixed 88 | * @throws \Exception 89 | */ 90 | protected function execute(TaskInterface $task, array $params, QueuedTaskInterface $run) 91 | { 92 | try { 93 | if ($task instanceof QueuedTaskAwareInterface) { 94 | $task->setQueuedTask($run); 95 | } 96 | 97 | if ($task instanceof OutputAwareInterface) { 98 | $task->setOutput($this->output); 99 | } 100 | 101 | if (count($params) > 0) { 102 | $response = $task->run($params); 103 | } else { 104 | $response = $task->run(); 105 | } 106 | 107 | $this->taskManager->success($run, $response); 108 | } catch (\Exception $e) { 109 | $this->logger->error("$e"); 110 | $this->taskManager->failure($run, "$e"); 111 | throw $e; 112 | } 113 | 114 | return $response; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Synchronizer/ScheduleSynchronizer.php: -------------------------------------------------------------------------------- 1 | doctrine = $doctrine; 27 | } 28 | 29 | /** 30 | * @var ScheduleRegistry 31 | */ 32 | private $scheduleRegistry; 33 | 34 | /** 35 | * @var bool 36 | */ 37 | private $force = false; 38 | 39 | /** 40 | * @param boolean $force 41 | */ 42 | public function setForce($force) 43 | { 44 | $this->force = $force; 45 | } 46 | 47 | /** 48 | * @param ScheduleRegistry $scheduleRegistry 49 | */ 50 | public function setScheduleRegistry(ScheduleRegistry $scheduleRegistry) 51 | { 52 | $this->scheduleRegistry = $scheduleRegistry; 53 | } 54 | 55 | /** 56 | * 57 | */ 58 | public function sync() 59 | { 60 | /** @var ScheduleRepository $repo */ 61 | $repo = $this->doctrine->getManager() 62 | ->getRepository('GloobyTaskBundle:Schedule'); 63 | 64 | $schedules = $this->scheduleRegistry->getSchedules(); 65 | 66 | foreach ($schedules as $id => $def) { 67 | $this->syncSchedule($id, $def); 68 | } 69 | 70 | foreach ($repo->findNotInNames(array_keys($schedules)) as $schedule) { 71 | $this->doctrine->getManager()->remove($schedule); 72 | } 73 | 74 | $this->doctrine->getManager()->flush(); 75 | } 76 | 77 | /** 78 | * @param Schedule $schedule 79 | * @param string $id 80 | * @param Def $def 81 | */ 82 | private function update(Schedule $schedule, $id, Def $def) 83 | { 84 | $schedule->setName($id); 85 | $schedule->setActive($def->active); 86 | $schedule->setInterval($def->interval); 87 | $schedule->setTimeout($def->timeout); 88 | $schedule->setParams($def->params); 89 | $schedule->setVersion($def->version); 90 | } 91 | 92 | /** 93 | * @param string $id 94 | * @param Def $def 95 | * @return array 96 | */ 97 | private function syncSchedule($id, Def $def) 98 | { 99 | /** @var ScheduleRepository $repo */ 100 | $repo = $this->doctrine->getManager() 101 | ->getRepository('GloobyTaskBundle:Schedule'); 102 | 103 | try { 104 | /** @var Schedule $schedule */ 105 | $schedule = $repo->findByName($id); 106 | 107 | if ($this->force || $schedule->getVersion() !== $def->version) { 108 | $this->update($schedule, $id, $def); 109 | } 110 | } catch (\Exception $e) { 111 | $this->create($id, $def); 112 | } 113 | } 114 | 115 | /** 116 | * @param string $id 117 | * @param Def $def 118 | */ 119 | private function create($id, Def $def) 120 | { 121 | $schedule = new Schedule(); 122 | $this->update($schedule, $id, $def); 123 | $this->doctrine->getManager()->persist($schedule); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Tests/DependencyInjection/Complier/RegisterSchedulesPassTest.php: -------------------------------------------------------------------------------- 1 | setDefinition('glooby_task.schedule_registry', $registryDefinition); 24 | 25 | $container->setDefinition('task.foo.a', $this->createScheduledTaskDefinition()); 26 | $container->setDefinition('task.foo.b', $this->createScheduledTaskDefinition()); 27 | $container->setDefinition('task.bar.a', $this->createScheduledTaskDefinition()); 28 | $container->setDefinition('task.foo.f', $this->createTaskDefinition()); 29 | 30 | $pass->process($container); 31 | 32 | $calls = $registryDefinition->getMethodCalls(); 33 | 34 | $this->assertCount(3, $calls); 35 | 36 | $this->assertEquals(array('addTask', array('task.foo.a')), $calls[0]); 37 | $this->assertEquals(array('addTask', array('task.foo.b')), $calls[1]); 38 | $this->assertEquals(array('addTask', array('task.bar.a')), $calls[2]); 39 | } 40 | 41 | public function testEmptyClass() 42 | { 43 | $container = new ContainerBuilder(); 44 | $pass = new RegisterSchedulesPass(); 45 | 46 | $registryDefinition = new Definition(); 47 | 48 | $container->setDefinition('glooby_task.schedule_registry', $registryDefinition); 49 | 50 | $definition = new Definition(); 51 | $definition->addTag('glooby.scheduled_task'); 52 | 53 | $container->setDefinition('task.foo.f', $definition); 54 | 55 | $pass->process($container); 56 | } 57 | 58 | public function testInvalidClass() 59 | { 60 | $container = new ContainerBuilder(); 61 | $pass = new RegisterSchedulesPass(); 62 | 63 | $registryDefinition = new Definition(); 64 | 65 | $container->setDefinition('glooby_task.schedule_registry', $registryDefinition); 66 | 67 | $definition = new Definition(self::class); 68 | $definition->addTag('glooby.scheduled_task'); 69 | 70 | $container->setDefinition('task.foo.f', $definition); 71 | 72 | $this->expectException('\InvalidArgumentException'); 73 | 74 | $pass->process($container); 75 | } 76 | 77 | public function testNoTag() 78 | { 79 | $container = new ContainerBuilder(); 80 | $pass = new RegisterSchedulesPass(); 81 | 82 | $registryDefinition = new Definition(); 83 | 84 | $container->setDefinition('glooby_task.schedule_registry', $registryDefinition); 85 | 86 | $container->setDefinition('task.foo.f', new Definition(self::class)); 87 | $container->setDefinition('task.foo.f', new Definition()); 88 | 89 | $pass->process($container); 90 | 91 | $calls = $registryDefinition->getMethodCalls(); 92 | 93 | $this->assertCount(0, $calls); 94 | } 95 | 96 | private function createScheduledTaskDefinition(array $attributes = array()) 97 | { 98 | $definition = $this->createTaskDefinition(); 99 | $definition->addTag('glooby.scheduled_task', $attributes); 100 | 101 | return $definition; 102 | } 103 | 104 | private function createTaskDefinition() 105 | { 106 | $task = $this->prophesize(TaskInterface::class); 107 | 108 | return new Definition(get_class($task->reveal())); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Entity/QueuedTaskRepository.php: -------------------------------------------------------------------------------- 1 | getEntityManager() 22 | ->createQuery('SELECT r 23 | FROM GloobyTaskBundle:QueuedTask r 24 | WHERE r.name = :name AND r.executeAt = :executeAt') 25 | ->setParameter('name', $name) 26 | ->setParameter('executeAt', $executeAt) 27 | ->useQueryCache(true) 28 | ->setMaxResults(1) 29 | ->getSingleResult(); 30 | } 31 | 32 | /** 33 | * @param string $name 34 | * @return QueuedTaskInterface 35 | */ 36 | public function getByNameAndExecuteAtBeforeNow($name) 37 | { 38 | return $this->getEntityManager() 39 | ->createQuery('SELECT r 40 | FROM GloobyTaskBundle:QueuedTask r 41 | WHERE r.name = :name AND r.executeAt <= :now') 42 | ->setParameter('name', $name) 43 | ->setParameter('now', new \DateTime()) 44 | ->useQueryCache(true) 45 | ->setMaxResults(1) 46 | ->getSingleResult(); 47 | } 48 | 49 | /** 50 | * @param int $limit 51 | * @return QueuedTaskInterface[] 52 | */ 53 | public function findQueued($limit) 54 | { 55 | return $this->getEntityManager() 56 | ->createQuery('SELECT r 57 | FROM GloobyTaskBundle:QueuedTask r 58 | WHERE r.status = :status AND r.executeAt <= :now 59 | ORDER BY r.executeAt ASC') 60 | ->setParameter('status', QueuedTaskInterface::STATUS_QUEUED) 61 | ->setParameter('now', new \DateTime()) 62 | ->setMaxResults($limit) 63 | ->useQueryCache(true) 64 | ->getResult(); 65 | } 66 | 67 | /** 68 | * @return QueuedTaskInterface[] 69 | */ 70 | public function findRunning() 71 | { 72 | return $this->getEntityManager() 73 | ->createQuery('SELECT r 74 | FROM GloobyTaskBundle:QueuedTask r 75 | WHERE r.status = :status') 76 | ->setParameter('status', QueuedTaskInterface::STATUS_RUNNING) 77 | ->useQueryCache(true) 78 | ->getResult(); 79 | } 80 | 81 | /** 82 | * @param string $name 83 | * @return bool 84 | */ 85 | public function isRunning($name) 86 | { 87 | return $this->isStatus($name, QueuedTaskInterface::STATUS_RUNNING); 88 | } 89 | 90 | /** 91 | * @param string $name 92 | * @return bool 93 | */ 94 | public function isQueued($name) 95 | { 96 | return $this->isStatus($name, QueuedTaskInterface::STATUS_QUEUED); 97 | } 98 | 99 | /** 100 | * @param string $name 101 | * @param string $status 102 | * @return bool 103 | */ 104 | private function isStatus($name, $status) 105 | { 106 | try { 107 | $this->getEntityManager() 108 | ->createQuery('SELECT r 109 | FROM GloobyTaskBundle:QueuedTask r 110 | WHERE r.name = :name AND r.status = :status') 111 | ->setParameter('name', $name) 112 | ->setParameter('status', $status) 113 | ->useQueryCache(true) 114 | ->setMaxResults(1) 115 | ->getSingleResult(); 116 | 117 | return true; 118 | } catch (NoResultException $e) { 119 | return false; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Model/Schedule.php: -------------------------------------------------------------------------------- 1 | created = new \DateTime(); 65 | $this->runs = new ArrayCollection(); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function getId() 72 | { 73 | return $this->id; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function getName() 80 | { 81 | return $this->name; 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function setName($name) 88 | { 89 | $this->name = $name; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function getCreated() 96 | { 97 | return $this->created; 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function setCreated($created) 104 | { 105 | $this->created = $created; 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function parseExpression() 112 | { 113 | return CronExpression::factory($this->getInterval()); 114 | } 115 | 116 | /** 117 | * {@inheritdoc} 118 | */ 119 | public function getInterval() 120 | { 121 | return $this->interval; 122 | } 123 | 124 | /** 125 | * {@inheritdoc} 126 | */ 127 | public function setInterval($interval) 128 | { 129 | $this->interval = $interval; 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | */ 135 | public function isActive() 136 | { 137 | return $this->active; 138 | } 139 | 140 | /** 141 | * {@inheritdoc} 142 | */ 143 | public function setActive($active) 144 | { 145 | $this->active = $active; 146 | } 147 | 148 | /** 149 | * {@inheritdoc} 150 | */ 151 | public function getTimeout() 152 | { 153 | return $this->timeout; 154 | } 155 | 156 | /** 157 | * {@inheritdoc} 158 | */ 159 | public function setTimeout($timeout) 160 | { 161 | $this->timeout = $timeout; 162 | } 163 | 164 | /** 165 | * {@inheritdoc} 166 | */ 167 | public function getParams() 168 | { 169 | return $this->params; 170 | } 171 | 172 | /** 173 | * {@inheritdoc} 174 | */ 175 | public function hasParams() 176 | { 177 | return count($this->params) > 0; 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | */ 183 | public function setParams(array $params) 184 | { 185 | $this->params = $params; 186 | } 187 | 188 | /** 189 | * {@inheritdoc} 190 | */ 191 | public function getVersion() 192 | { 193 | return $this->version; 194 | } 195 | 196 | /** 197 | * {@inheritdoc} 198 | */ 199 | public function setVersion($version) 200 | { 201 | $this->version = $version; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | glooby_task.queue_processor.limit: 10 3 | 4 | services: 5 | _defaults: { public: true } 6 | 7 | Glooby\TaskBundle\Task\PingTask: '@glooby_task.ping' 8 | Glooby\TaskBundle\Schedule\ScheduleRegistry: '@glooby_task.schedule_registry' 9 | Glooby\TaskBundle\Synchronizer\ScheduleSynchronizer: '@glooby_task.schedule_synchronizer' 10 | Glooby\TaskBundle\Manager\TaskManager: '@glooby_task.task_manager' 11 | Glooby\TaskBundle\Task\TaskRunner: '@glooby_task.task_runner' 12 | Glooby\TaskBundle\Queue\QueuePruner: '@glooby_task.queue_pruner' 13 | Glooby\TaskBundle\Queue\QueueScheduler: '@glooby_task.queue_scheduler' 14 | Glooby\TaskBundle\Queue\QueueProcessor: '@glooby_task.queue_processor' 15 | Glooby\TaskBundle\Queue\QueueMonitor: '@glooby_task.queue_monitor' 16 | 17 | glooby_task.ping: 18 | class: Glooby\TaskBundle\Task\PingTask 19 | tags: 20 | - { name: glooby.scheduled_task } 21 | 22 | glooby_task.schedule_registry: 23 | class: Glooby\TaskBundle\Schedule\ScheduleRegistry 24 | calls: 25 | - [setReader, ['@annotations.reader']] 26 | - [setContainer, ['@service_container']] 27 | 28 | glooby_task.schedule_synchronizer: 29 | class: Glooby\TaskBundle\Synchronizer\ScheduleSynchronizer 30 | calls: 31 | - [setDoctrine, ['@doctrine']] 32 | - [setScheduleRegistry, ['@glooby_task.schedule_registry']] 33 | 34 | glooby_task.task_manager: 35 | class: Glooby\TaskBundle\Manager\TaskManager 36 | calls: 37 | - [setDoctrine, ['@doctrine']] 38 | 39 | glooby_task.task_runner: 40 | class: Glooby\TaskBundle\Task\TaskRunner 41 | calls: 42 | - [setLogger, ['@logger']] 43 | - [setContainer, ['@service_container']] 44 | - [setTaskManager, ['@glooby_task.task_manager']] 45 | 46 | glooby_task.queue_pruner: 47 | class: Glooby\TaskBundle\Queue\QueuePruner 48 | calls: 49 | - [setDoctrine, ['@doctrine']] 50 | tags: 51 | - { name: glooby.scheduled_task } 52 | 53 | glooby_task.queue_scheduler: 54 | class: Glooby\TaskBundle\Queue\QueueScheduler 55 | calls: 56 | - [setDoctrine, ['@doctrine']] 57 | - [setTaskManager, ['@glooby_task.task_manager']] 58 | 59 | glooby_task.queue_processor: 60 | class: Glooby\TaskBundle\Queue\QueueProcessor 61 | calls: 62 | - [setDoctrine, ['@doctrine']] 63 | - [setLimit, ['%glooby_task.queue_processor.limit%']] 64 | - [setDebug, ['%kernel.debug%']] 65 | 66 | glooby_task.queue_monitor: 67 | class: Glooby\TaskBundle\Queue\QueueMonitor 68 | calls: 69 | - [setDoctrine, ['@doctrine']] 70 | - [setTaskManager, ['@glooby_task.task_manager']] 71 | 72 | glooby_task.task_run_command: 73 | class: 74 | Glooby\TaskBundle\Command\Task\RunCommand 75 | arguments: 76 | - '@service_container' 77 | tags: 78 | - { name: 'console.command', command: 'task:run' } 79 | 80 | glooby_task.scheduler_run_command: 81 | class: 82 | Glooby\TaskBundle\Command\Scheduler\RunCommand 83 | arguments: 84 | - '@service_container' 85 | tags: 86 | - { name: 'console.command', command: 'scheduler:run' } 87 | 88 | glooby_task.scheduler_prune_command: 89 | class: 90 | Glooby\TaskBundle\Command\Scheduler\PruneCommand 91 | arguments: 92 | - '@service_container' 93 | tags: 94 | - { name: 'console.command', command: 'scheduler:prune' } 95 | 96 | glooby_task.scheduler_sync_command: 97 | class: 98 | Glooby\TaskBundle\Command\Scheduler\SyncCommand 99 | arguments: 100 | - '@service_container' 101 | tags: 102 | - { name: 'console.command', command: 'scheduler:sync' } -------------------------------------------------------------------------------- /src/Annotation/Schedule.php: -------------------------------------------------------------------------------- 1 | '0 0 1 1 *', 18 | '@annually' => '0 0 1 1 *', 19 | '@monthly' => '0 0 1 * *', 20 | '@weekly' => '0 0 * * 0', 21 | '@daily' => '0 0 * * *', 22 | '@hourly' => '0 * * * *', 23 | '@semi_hourly' => '*/30 * * * *', 24 | '@twenty_minutes' => '*/20 * * * *', 25 | '@quarter_hourly' => '*/15 * * * *', 26 | '@ten_minutes' => '*/10 * * * *', 27 | '@five_minutes' => '*/5 * * * *', 28 | '*' => '* * * * *', 29 | ]; 30 | 31 | /** 32 | * @var string 33 | */ 34 | public $interval; 35 | 36 | /** 37 | * @var bool 38 | */ 39 | public $active = true; 40 | 41 | /** 42 | * @var null|int 43 | */ 44 | public $timeout = 0; 45 | 46 | /** 47 | * @var int 48 | */ 49 | public $version = 10; 50 | 51 | /** 52 | * @var array 53 | */ 54 | public $params = []; 55 | 56 | /** 57 | * @param array $options 58 | * @throws \InvalidArgumentException 59 | */ 60 | public function __construct(array $options) 61 | { 62 | $options = $this->setDefault($options); 63 | $options = $this->ensureExpressionExist($options); 64 | $options = $this->mapExpression($options); 65 | 66 | $this->populate($options); 67 | 68 | $this->validateExpression(); 69 | $this->validateTimeout(); 70 | $this->validateParams(); 71 | } 72 | 73 | /** 74 | * @throws \InvalidArgumentException 75 | */ 76 | public function validateTimeout() 77 | { 78 | if (isset($this->timeout) && !is_numeric($this->timeout)) { 79 | throw new \InvalidArgumentException('Property "timeout" must be an int'); 80 | } 81 | } 82 | 83 | /** 84 | * @throws \InvalidArgumentException 85 | */ 86 | public function validateParams() 87 | { 88 | if (!is_array($this->params)) { 89 | throw new \InvalidArgumentException('Property "params" must be an array'); 90 | } 91 | } 92 | 93 | /** 94 | * @throws \InvalidArgumentException 95 | */ 96 | public function validateExpression() 97 | { 98 | CronExpression::factory($this->interval); 99 | } 100 | 101 | /** 102 | * @param array $options 103 | * @throws \InvalidArgumentException 104 | */ 105 | public function populate(array $options) 106 | { 107 | foreach ($options as $key => $value) { 108 | if (!property_exists($this, $key)) { 109 | throw new \InvalidArgumentException(sprintf('Property "%s" does not exist', $key)); 110 | } 111 | 112 | $this->$key = $value; 113 | } 114 | } 115 | 116 | /** 117 | * @param array $options 118 | * @return array 119 | */ 120 | public function mapExpression(array $options) 121 | { 122 | if (isset(self::$map[$options['interval']])) { 123 | $options['interval'] = self::$map[$options['interval']]; 124 | } 125 | 126 | return $options; 127 | } 128 | 129 | /** 130 | * @param array $options 131 | * @return array 132 | */ 133 | public function setDefault(array $options) 134 | { 135 | if (isset($options['value'])) { 136 | $options['interval'] = $options['value']; 137 | unset($options['value']); 138 | } 139 | 140 | return $options; 141 | } 142 | 143 | /** 144 | * @param array $options 145 | * @throws \InvalidArgumentException 146 | * @return array 147 | */ 148 | public function ensureExpressionExist(array $options) 149 | { 150 | if (empty($options['interval'])) { 151 | throw new \InvalidArgumentException('Missing property interval'); 152 | } 153 | return $options; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Queue/QueueProcessor.php: -------------------------------------------------------------------------------- 1 | doctrine = $doctrine; 46 | } 47 | 48 | /** 49 | * @param OutputInterface $output 50 | */ 51 | public function setOutput(OutputInterface $output) 52 | { 53 | $this->output = $output; 54 | } 55 | 56 | /** 57 | * @param boolean $debug 58 | */ 59 | public function setDebug($debug) 60 | { 61 | $this->debug = $debug; 62 | } 63 | 64 | /** 65 | * @param int $limit 66 | */ 67 | public function setLimit($limit) 68 | { 69 | $this->limit = $limit; 70 | } 71 | 72 | /** 73 | * @throws \Exception 74 | */ 75 | public function process() 76 | { 77 | $queueRepo = $this->doctrine->getManager() 78 | ->getRepository('GloobyTaskBundle:QueuedTask'); 79 | 80 | $started = []; 81 | foreach ($queueRepo->findQueued($this->limit) as $queuedTask) { 82 | if (!$queueRepo->isRunning($queuedTask->getName()) && !in_array($queuedTask->getName(), $started)) { 83 | $started[] = $queuedTask->getName(); 84 | $this->start($queuedTask); 85 | } 86 | } 87 | 88 | $this->wait(); 89 | } 90 | 91 | /** 92 | * @return string 93 | */ 94 | private function getProcessParams() 95 | { 96 | $params = []; 97 | 98 | if (!$this->debug) { 99 | $params[] = '--env=prod'; 100 | } 101 | 102 | return implode(' ', $params); 103 | } 104 | 105 | /** 106 | * 107 | */ 108 | private function wait() 109 | { 110 | while (count($this->processes) > 0) { 111 | sleep(1); 112 | 113 | foreach ($this->processes as $i => $process) { 114 | if (!$process->isRunning()) { 115 | unset($this->processes[$i]); 116 | echo $process->getOutput(); 117 | } 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * @param QueuedTaskInterface $queuedTask 124 | */ 125 | private function start(QueuedTaskInterface $queuedTask) 126 | { 127 | $command = $this->createCommand($queuedTask); 128 | $process = $this->createProcess($command); 129 | 130 | $this->processes[] = $process; 131 | 132 | if (null !== $this->output) { 133 | $this->output->writeln("$command"); 134 | } 135 | } 136 | 137 | /** 138 | * @param string $command 139 | * @return Process 140 | */ 141 | private function createProcess($command) 142 | { 143 | $that = $this; 144 | $nl = false; 145 | 146 | $process = new Process($command); 147 | $process->setTimeout(0); 148 | $process->start(); 149 | 150 | return $process; 151 | } 152 | 153 | /** 154 | * @param QueuedTaskInterface $queuedTask 155 | * @return string 156 | */ 157 | private function createCommand(QueuedTaskInterface $queuedTask) 158 | { 159 | $command = sprintf( 160 | '%s -d memory_limit=%s bin/console task:run --id=%s %s', 161 | exec("readlink -f /proc/".posix_getpid()."/exe"), 162 | ini_get('memory_limit'), 163 | $queuedTask->getId(), 164 | $this->getProcessParams() 165 | ); 166 | 167 | return $command; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Queue/QueueScheduler.php: -------------------------------------------------------------------------------- 1 | doctrine = $doctrine; 28 | } 29 | 30 | /** 31 | * @var TaskManager 32 | */ 33 | private $taskManager; 34 | 35 | /** 36 | * @param TaskManager $taskManager 37 | */ 38 | public function setTaskManager(TaskManager $taskManager) 39 | { 40 | $this->taskManager = $taskManager; 41 | } 42 | 43 | /** 44 | * 45 | */ 46 | public function schedule() 47 | { 48 | $queueRepo = $this->doctrine->getManager() 49 | ->getRepository('GloobyTaskBundle:QueuedTask'); 50 | 51 | $scheduleRepo = $this->doctrine->getManager() 52 | ->getRepository('GloobyTaskBundle:Schedule'); 53 | 54 | foreach ($scheduleRepo->findActive() as $schedule) { 55 | if (!$queueRepo->isQueued($schedule->getName())) { 56 | $this->queue($schedule); 57 | } 58 | } 59 | 60 | $this->doctrine->getManager()->flush(); 61 | } 62 | 63 | /** 64 | * @param ScheduleInterface $schedule 65 | */ 66 | private function queue(ScheduleInterface $schedule) 67 | { 68 | $executeAt = $this->getExecuteAt($schedule); 69 | 70 | if (null !== $executeAt) { 71 | $this->taskManager->queue($schedule->getName(), $executeAt, $schedule->getParams()); 72 | } 73 | } 74 | 75 | /** 76 | * @param ScheduleInterface $schedule 77 | * @return \DateTime|null 78 | */ 79 | private function checkNextRunDate(ScheduleInterface $schedule) 80 | { 81 | return $this->checkRunDate($schedule, $schedule->parseExpression()->getNextRunDate()); 82 | } 83 | 84 | /** 85 | * @param ScheduleInterface $schedule 86 | * @return \DateTime|null 87 | */ 88 | private function checkPreviousRunDate(ScheduleInterface $schedule) 89 | { 90 | return $this->checkRunDate($schedule, $schedule->parseExpression()->getPreviousRunDate()); 91 | } 92 | 93 | /** 94 | * @param ScheduleInterface $schedule 95 | * @param \DateTime $date 96 | * @return \DateTime|null 97 | */ 98 | private function checkRunDate(ScheduleInterface $schedule, \DateTime $date) 99 | { 100 | $queueRepo = $this->getQueuedTaskRepo(); 101 | $executeAt = null; 102 | 103 | try { 104 | $queueRepo->getByNameAndExecuteAt($schedule->getName(), $date); 105 | } catch (NoResultException $e) { 106 | $executeAt = $date; 107 | } 108 | 109 | return $executeAt; 110 | } 111 | 112 | /** 113 | * @param ScheduleInterface $schedule 114 | * @return \DateTime|null 115 | */ 116 | private function checkExecutedBeforeNow(ScheduleInterface $schedule) 117 | { 118 | $queueRepo = $this->getQueuedTaskRepo(); 119 | $executeAt = null; 120 | 121 | try { 122 | $queueRepo->getByNameAndExecuteAtBeforeNow($schedule->getName()); 123 | } catch (NoResultException $e) { 124 | $executeAt = $schedule->parseExpression()->getPreviousRunDate(); 125 | } 126 | 127 | return $executeAt; 128 | } 129 | 130 | /** 131 | * @return QueuedTaskRepository 132 | */ 133 | private function getQueuedTaskRepo() 134 | { 135 | return $this->doctrine->getManager() 136 | ->getRepository('GloobyTaskBundle:QueuedTask'); 137 | } 138 | 139 | /** 140 | * @param ScheduleInterface $schedule 141 | * @return \DateTime|null 142 | */ 143 | private function getExecuteAt(ScheduleInterface $schedule) 144 | { 145 | $executeAt = $this->checkExecutedBeforeNow($schedule); 146 | 147 | if (null === $executeAt) { 148 | $executeAt = $this->checkPreviousRunDate($schedule); 149 | } 150 | 151 | if (null === $executeAt) { 152 | $executeAt = $this->checkNextRunDate($schedule); 153 | } 154 | 155 | return $executeAt; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # task-bundle 2 | [![Build Status](https://travis-ci.org/glooby/task-bundle.svg?branch=master)](https://travis-ci.org/glooby/task-bundle) 3 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/glooby/task-bundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/glooby/task-bundle/?branch=master) 4 | [![Coverage Status](https://coveralls.io/repos/github/glooby/task-bundle/badge.svg)](https://coveralls.io/github/glooby/task-bundle) 5 | [![Latest Stable Version](https://poser.pugx.org/glooby/task-bundle/version)](https://packagist.org/packages/glooby/task-bundle) 6 | [![Total Downloads](https://poser.pugx.org/glooby/task-bundle/downloads)](https://packagist.org/packages/glooby/task-bundle) 7 | [![License](https://poser.pugx.org/glooby/task-bundle/license)](https://packagist.org/packages/glooby/task-bundle) 8 | 9 | Provides a simple framework to manage scheduling and execution of tasks Symfony application. 10 | 11 | Prerequisite 12 | ----------------- 13 | 14 | This bundle requires cron to be installed on the server to be able to execute scheduled tasks 15 | 16 | Installation 17 | ----------------- 18 | 19 | Add the `glooby/task-bundle` package to your `require` section in the `composer.json` file. 20 | 21 | ``` bash 22 | $ composer require glooby/task-bundle ~3.0 23 | ``` 24 | 25 | Add the GloobyTaskBundle to your application's kernel: 26 | 27 | ``` php 28 | /dev/null 2>&1 44 | ``` 45 | 46 | Documentation 47 | ----------------- 48 | 49 | ### Create a executable Task 50 | 51 | To setup a new runnable task you should follow these steps 52 | 53 | #### Implement the TaskInterface 54 | 55 | example: src/Glooby/Api/TaskBundle/Task/PingTask.php 56 | 57 | ```php 58 | 59 | class PingTask implements TaskInterface 60 | { 61 | /** 62 | * @inheritdoc 63 | */ 64 | public function run(array $params = []) 65 | { 66 | return 'pong'; 67 | } 68 | } 69 | ``` 70 | 71 | Add a service for your task 72 | 73 | ```yaml 74 | services: 75 | glooby_task.ping: 76 | class: Glooby\TaskBundle\Task\PingTask 77 | ``` 78 | 79 | #### Try and run the task trough cli 80 | 81 | ```bash 82 | 83 | $ bin/console task:run glooby_task.ping 84 | 85 | "pong" 86 | 87 | ``` 88 | ### Setup Scheduled task 89 | 90 | To setup a new schedule you should follow the steps below 91 | 92 | #### Make your service runnable 93 | 94 | Follow the steps in [Create a executable Task](#Create a executable Task) 95 | 96 | #### Tag your service 97 | 98 | By tagging your service with the glooby.scheduled_task 99 | tag it will be treated as a scheduled task 100 | 101 | example: 102 | 103 | src/Glooby/Api/TaskBundle/Resources/config/services.yml 104 | 105 | ```yml 106 | 107 | services: 108 | glooby_task.ping: 109 | class: Glooby\TaskBundle\Task\PingTask 110 | tags: 111 | - { name: glooby.scheduled_task } 112 | ``` 113 | 114 | #### Annotate your class 115 | 116 | Annotate your class with this annotation: Glooby\TaskBundle\Annotation\Schedule 117 | 118 | ##### Parameters 119 | 120 | ###### interval 121 | 122 | The first parameter to the annotation is defaulted to the **interval** parameter. In this parameter you configure the 123 | interval that the service should be executed. 124 | 125 | The **interval** is a string of five or optional six subexpressions that describe details of the schedule. The syntax is based on the Linux cron daemon definition. 126 | ``` 127 | * * * * * * 128 | - - - - - - 129 | | | | | | | 130 | | | | | | + year [optional] 131 | | | | | +----- day of week (0 - 7) (Sunday=0 or 7) 132 | | | | +---------- month (1 - 12) 133 | | | +--------------- day of month (1 - 31) 134 | | +-------------------- hour (0 - 23) 135 | +------------------------- min (0 - 59) 136 | ``` 137 | 138 | This is the only required parameter 139 | 140 | ```php 141 | 142 | use Glooby\TaskBundle\Annotation\Schedule; 143 | 144 | /** 145 | * @Schedule("* * * * *") 146 | */ 147 | class PingTask implements TaskInterface 148 | { 149 | 150 | ``` 151 | 152 | Here you have several shortcuts that you can use instead for most common use cases 153 | 154 | | value | interval | 155 | |:---------------:|:------------:| 156 | | @yearly | 0 0 1 1 * | 157 | | @annually | 0 0 1 1 * | 158 | | @monthly | 0 0 1 * * | 159 | | @weekly | 0 0 * * 0 | 160 | | @daily | 0 0 * * * | 161 | | @hourly | 0 * * * * | 162 | | @semi_hourly | */30 * * * * | 163 | | @quarter_hourly | */15 * * * * | 164 | | * | * * * * * | 165 | 166 | ```php 167 | 168 | use Glooby\TaskBundle\Annotation\Schedule; 169 | 170 | /** 171 | * @Schedule("@hourly") 172 | */ 173 | class PingTask implements TaskInterface 174 | { 175 | 176 | ``` 177 | 178 | ###### params 179 | 180 | The **params** that should be used when calling 181 | 182 | ```php 183 | 184 | use Glooby\TaskBundle\Annotation\Schedule; 185 | 186 | /** 187 | * @Schedule("@weekly", params={"wash": true, "flush": 500}) 188 | */ 189 | class CityImporter implements TaskInterface 190 | { 191 | 192 | ``` 193 | 194 | ###### active 195 | 196 | Phe **active** parameter tells if the schedule should be active or not, default=true 197 | 198 | ```php 199 | 200 | use Glooby\TaskBundle\Annotation\Schedule; 201 | 202 | /** 203 | * @Schedule("*/6", active=false) 204 | */ 205 | class PingTask implements TaskInterface 206 | { 207 | 208 | ``` 209 | 210 | ### Sync schedules to the database, this has to be run after each update 211 | 212 | ```php 213 | 214 | bin/console scheduler:run 215 | 216 | ``` 217 | 218 | Running the Tests 219 | ----------------- 220 | 221 | Install the dependencies: 222 | 223 | ``` bash 224 | $ script/bootstrap 225 | ``` 226 | 227 | Then, run the test suite: 228 | 229 | ``` bash 230 | $ script/test 231 | ``` 232 | 233 | Contributing 234 | ------------ 235 | 236 | See 237 | [CONTRIBUTING](https://github.com/glooby/task-bundle/blob/master/CONTRIBUTING.md) 238 | file. 239 | 240 | License 241 | ------- 242 | 243 | This bundle is released under the MIT license. See the complete license in the 244 | bundle: 245 | [LICENSE.md](https://github.com/glooby/task-bundle/blob/master/LICENSE.md) 246 | 247 | [www.glooby.com](https://www.glooby.com) 248 | [www.glooby.se](https://www.glooby.se) 249 | -------------------------------------------------------------------------------- /src/Model/QueuedTask.php: -------------------------------------------------------------------------------- 1 | name = $name; 94 | $this->params = null === $params ? $params : []; 95 | $this->executeAt = null === $executeAt ? new \DateTime() : $executeAt; 96 | $this->created = new \DateTime(); 97 | $this->updated = new \DateTime(); 98 | $this->status = self::STATUS_QUEUED; 99 | $this->resolution = self::RESOLUTION_QUEUED; 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function getId() 106 | { 107 | return $this->id; 108 | } 109 | 110 | /** 111 | * @return int 112 | */ 113 | public function getPid() 114 | { 115 | return $this->pid; 116 | } 117 | 118 | /** 119 | * @return boolean 120 | */ 121 | public function hasPId() 122 | { 123 | return null !== $this->pid; 124 | } 125 | 126 | /** 127 | * @param int $pid 128 | */ 129 | public function setPid(int $pid) 130 | { 131 | $this->pid = $pid; 132 | } 133 | 134 | /** 135 | * {@inheritdoc} 136 | */ 137 | public function getName() 138 | { 139 | return $this->name; 140 | } 141 | 142 | /** 143 | * {@inheritdoc} 144 | */ 145 | public function getCreated() 146 | { 147 | return $this->created; 148 | } 149 | 150 | /** 151 | * @return \DateTime 152 | */ 153 | public function getUpdated() 154 | { 155 | return $this->updated; 156 | } 157 | 158 | /** 159 | * @param \DateTime $updated 160 | */ 161 | public function setUpdated(\DateTime $updated) 162 | { 163 | $this->updated = $updated; 164 | } 165 | 166 | /** 167 | * {@inheritdoc} 168 | */ 169 | public function getExecuteAt() 170 | { 171 | return $this->executeAt; 172 | } 173 | 174 | /** 175 | * {@inheritdoc} 176 | */ 177 | public function getSchedule() 178 | { 179 | return $this->schedule; 180 | } 181 | 182 | /** 183 | * {@inheritdoc} 184 | */ 185 | public function setSchedule(ScheduleInterface $schedule) 186 | { 187 | $this->schedule = $schedule; 188 | } 189 | 190 | /** 191 | * {@inheritdoc} 192 | */ 193 | public function getParams() 194 | { 195 | return $this->params; 196 | } 197 | 198 | /** 199 | * {@inheritdoc} 200 | */ 201 | public function hasParams() 202 | { 203 | return count($this->params) > 0; 204 | } 205 | 206 | /** 207 | * {@inheritdoc} 208 | */ 209 | public function getResult() 210 | { 211 | return $this->result; 212 | } 213 | 214 | /** 215 | * {@inheritdoc} 216 | */ 217 | public function getResolution() 218 | { 219 | return $this->resolution; 220 | } 221 | 222 | /** 223 | * {@inheritdoc} 224 | */ 225 | public function getFinished() 226 | { 227 | return $this->finished; 228 | } 229 | 230 | /** 231 | * {@inheritdoc} 232 | */ 233 | public function getStatus() 234 | { 235 | return $this->status; 236 | } 237 | 238 | /** 239 | * {@inheritdoc} 240 | */ 241 | public function getStarted() 242 | { 243 | return $this->started; 244 | } 245 | 246 | /** 247 | * @param \DateTime $started 248 | */ 249 | protected function setStarted(\DateTime $started) 250 | { 251 | $this->started = $started; 252 | } 253 | 254 | /** 255 | * @param \DateTime $finished 256 | */ 257 | protected function setFinished(\DateTime $finished) 258 | { 259 | $this->finished = $finished; 260 | } 261 | 262 | /** 263 | * @param string $status 264 | */ 265 | protected function setStatus($status) 266 | { 267 | $this->status = $status; 268 | } 269 | 270 | /** 271 | * @param string $resolution 272 | */ 273 | protected function setResolution($resolution) 274 | { 275 | $this->resolution = $resolution; 276 | } 277 | 278 | /** 279 | * @param string $result 280 | */ 281 | protected function setResult($result) 282 | { 283 | $this->result = $result; 284 | } 285 | 286 | /** 287 | * @param string $resolution 288 | * @param mixed $response 289 | */ 290 | protected function resolve($resolution, $response) 291 | { 292 | if (!empty($response)) { 293 | $this->setResult(print_r($response, true)); 294 | } 295 | 296 | $this->setUpdated(new \DateTime()); 297 | $this->setStatus(QueuedTaskInterface::STATUS_DONE); 298 | $this->setResolution($resolution); 299 | $this->setFinished(new \DateTime()); 300 | } 301 | 302 | /** 303 | * {@inheritdoc} 304 | */ 305 | public function start() 306 | { 307 | $this->setUpdated(new \DateTime()); 308 | $this->setPid(posix_getpid()); 309 | $this->setStatus(QueuedTaskInterface::STATUS_RUNNING); 310 | $this->setStarted(new \DateTime()); 311 | $this->progress(0); 312 | } 313 | 314 | /** 315 | * {@inheritdoc} 316 | */ 317 | public function success($response) 318 | { 319 | $this->resolve(QueuedTaskInterface::RESOLUTION_SUCCESS, $response); 320 | $this->progress(100); 321 | } 322 | 323 | /** 324 | * {@inheritdoc} 325 | */ 326 | public function failure($response) 327 | { 328 | $this->resolve(QueuedTaskInterface::RESOLUTION_FAILURE, $response); 329 | $this->progress(100); 330 | } 331 | 332 | /** 333 | * {@inheritdoc} 334 | */ 335 | public function progress(int $progress, ?string $info = null) 336 | { 337 | $this->progress = $progress; 338 | 339 | if ($info) { 340 | $this->progressInfo = $info; 341 | } 342 | } 343 | } 344 | --------------------------------------------------------------------------------