Skip to content

Commit

Permalink
Base cron bundle implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
vtsykun committed Jan 12, 2020
0 parents commit a0d00a1
Show file tree
Hide file tree
Showing 31 changed files with 1,141 additions and 0 deletions.
20 changes: 20 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
The MIT License (MIT).

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
123 changes: 123 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Okvpn - Cron Bundle

This bundle provides interfaces for registering scheduled tasks within your Symfony application.

### Purpose
This is a more simpler alternative of existing cron bundle without doctrine deps,
supporting invoke a service as cron job.
Here also added support middleware for customization handling cron jobs across a cluster install:
(Send jobs to message queue, like Symfony Messenger; locking, etc.)

Features
--------

- Not need doctrine/database.
- Load a cron job from a different storage.
- Support many engines to run cron (in parallel process, message queue, consistently).
- Support many types of cron handlers/command: (services, symfony commands, UNIX commands).
- Middleware and customization.

Usage
-----

To regularly run a set of commands from your application, configure your system to run the
oro:cron command every minute. On UNIX-based systems, you can simply set up a crontab entry for this:

```
*/1 * * * * /path/to/php /path/to/bin/console okvpn:cron:run --env=prod > /dev/null
```

Add cron commands

```
services:
app.you_cron_service:
class: App/Cron/YouService
tags:
- { name: okvpn.cron, cron: '*/5 * * * *', lock: true, arguments: {'arg1': 5}, async: true }
```

where:

- `cron` - A cron expression. (Optional). If empty, the command will run always.
- `lock` - Prevent to run the command again, if prev. command is not finished yet. (Optional).
To use it required symfony [lock component](https://symfony.com/doc/4.4/components/lock.html)
- `async` - Run command async in the new process without blocking main thread
- `arguments` - Array command of arguments. (Optional).
- `lockName` - Lock name. (Optional).
- `lockTtl` - Set ttl (Time To Live) for expiring locks. (Optional).

### Cron Handlers

1. Service.

```php
<?php

namespace App\Cron;

class MyCron
{
public function __invoke($arguments)
{
// processing...
}
}
```

```
services:
App\Cron\MyCron:
tags:
- { name: okvpn.cron, cron: '*/5 * * * *' }
```

2. Command

```
services:
App\Command\CronCommand:
tags:
- { name: console.command }
- { name: okvpn.cron, cron: '*/5 * * * *' }
```

### Custom cron loaders

```php
<?php declare(strict_types=1);

use Okvpn\Bundle\CronBundle\Loader\ScheduleLoaderInterface;

final class DatabaseScheduleLoader implements ScheduleLoaderInterface
{
private $configuration;
private $factory;

public function __construct(array $configuration, ScheduleFactoryInterface $factory)
{
$this->factory = $factory;
$this->configuration = $configuration;
}

/**
* @inheritDoc
*/
public function getSchedules(): iterable
{
foreach ($this->configuration as $config) {
yield $this->factory->create($config);
}
}
}

```

License
---

MIT License.

40 changes: 40 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "okvpn/cron-bundle",
"description": "Symfony Cron Bundle for registering and execute scheduled tasks",
"type": "symfony-bundle",
"license": "MIT",
"homepage": "https://github.com/vtsykun/cron-bundle",
"support": {
"email": "[email protected]",
"issues": "https://github.com/vtsykun/cron-bundle/issues",
"source": "https://github.com/vtsykun/cron-bundle/releases"
},
"keywords": [
"cron-bundle",
"symfony-cron",
"cron",
"symfony",
"bundle"
],
"authors": [
{
"name": "Uladzimir Tsykun",
"email": "[email protected]"
}
],
"require": {
"php": "^7.2.5",
"symfony/framework-bundle": "^3.4|^4.2|^5.0",
"mtdowling/cron-expression": "^1.1"
},
"autoload": {
"psr-4": {
"Okvpn\\Bundle\\CronBundle\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
}
}
58 changes: 58 additions & 0 deletions src/Command/CronCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Okvpn\Bundle\CronBundle\Command;

use Okvpn\Bundle\CronBundle\Loader\ScheduleLoaderInterface;
use Okvpn\Bundle\CronBundle\Runner\ScheduleRunnerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class CronCommand extends Command
{
private $scheduleRunner;
private $loader;

/**
* @param ScheduleRunnerInterface $scheduleRunner
* @param ScheduleLoaderInterface $loader
*/
public function __construct(ScheduleRunnerInterface $scheduleRunner, ScheduleLoaderInterface $loader)
{
$this->scheduleRunner = $scheduleRunner;
$this->loader = $loader;

parent::__construct();
}

/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this->setName('okvpn:cron:run')
->addOption('with', null, InputOption::VALUE_IS_ARRAY, 'StampFqcn to add command stamp to all schedules')
->addOption('without', null, InputOption::VALUE_IS_ARRAY, 'StampFqcn to remove command stamp from all schedules.')
->addOption('command', null, InputOption::VALUE_OPTIONAL, 'Run only selected command')
->setDescription('Runs any currently schedule cron');
}

/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$command = $input->getOption('command');
foreach ($this->loader->getSchedules() as $schedule) {
if (null !== $command && $schedule->getCommand() !== $command) {
continue;
}

$output->writeln(" > Scheduling run for command {$schedule->getCommand()} ...");
$this->scheduleRunner->execute($schedule);
}
}
}
46 changes: 46 additions & 0 deletions src/Command/CronExecuteCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Okvpn\Bundle\CronBundle\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CronExecuteCommand extends Command
{
public static $defaultName = 'okvpn:cron:execute';

private $redis;
private $cronEngine;

public function __construct()
{
parent::__construct(null);
}

/**
* {@inheritDoc}
*/
protected function configure()
{
$this->addArgument('job', InputArgument::REQUIRED, 'Cron job id')
->setDescription('Execute cron command for job id');
}

/**
* {@inheritDoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$command = $this->redis->get($input->getArgument('job'));
if (empty($command)) {
$output->writeln('Job not found');
}
$command = json_decode($command, true);

$this->cronEngine->run($command['command'], $command['arguments'] ?? []);
}
}
29 changes: 29 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Okvpn\Bundle\CronBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
* This is the class that validates and merges configuration from your app/config files.
*
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/configuration.html}
*/
class Configuration implements ConfigurationInterface
{
/**
* {@inheritdoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('okvpn_cron');

// Here you should define the parameters that are allowed to
// configure your bundle. See the documentation linked above for
// more information on that topic.

return $treeBuilder;
}
}
28 changes: 28 additions & 0 deletions src/DependencyInjection/OkvpnCronExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Okvpn\Bundle\CronBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

/**
* This is the class that loads and manages your bundle configuration.
*
* @link http://symfony.com/doc/current/cookbook/bundles/extension.html
*/
class OkvpnCronExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);

$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}
27 changes: 27 additions & 0 deletions src/Loader/ArrayScheduleLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Okvpn\Bundle\CronBundle\Loader;

final class ArrayScheduleLoader implements ScheduleLoaderInterface
{
private $configuration;
private $factory;

public function __construct(array $configuration, ScheduleFactoryInterface $factory)
{
$this->factory = $factory;
$this->configuration = $configuration;
}

/**
* @inheritDoc
*/
public function getSchedules(): iterable
{
foreach ($this->configuration as $config) {
yield $this->factory->create($config);
}
}
}
Loading

0 comments on commit a0d00a1

Please sign in to comment.