mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'master' into VSR-issues
This commit is contained in:
commit
2deed644b6
93
docs/source/development/cronjob.rst
Normal file
93
docs/source/development/cronjob.rst
Normal file
@ -0,0 +1,93 @@
|
||||
|
||||
.. Copyright (C) 2014-2023 Champs Libres Cooperative SCRLFS
|
||||
Permission is granted to copy, distribute and/or modify this document
|
||||
under the terms of the GNU Free Documentation License, Version 1.3
|
||||
or any later version published by the Free Software Foundation;
|
||||
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
|
||||
A copy of the license is included in the section entitled "GNU
|
||||
Free Documentation License".
|
||||
|
||||
.. _cronjob:
|
||||
|
||||
Cron jobs
|
||||
*********
|
||||
|
||||
Some tasks must be executed regularly: refresh some materialized views, remove old data, ...
|
||||
|
||||
For this purpose, one can programmatically implements a "cron job", which will be scheduled by a specific command.
|
||||
|
||||
The command :code:`chill:cron-job:execute`
|
||||
==========================================
|
||||
|
||||
The command :code:`chill:cron-job:execute` will schedule a task, one by one. In a classical implementation, it should
|
||||
be executed every 15 minutes (more or less), to ensure that every task can be executed.
|
||||
|
||||
.. warning::
|
||||
|
||||
This command should not be executed in parallel. The installer should ensure that two job are executed concurrently.
|
||||
|
||||
How to implements a cron job ?
|
||||
==============================
|
||||
|
||||
Implements a :code:`Chill\MainBundle\Cron\CronJobInterface`. Here is an example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
namespace Chill\MainBundle\Service\Something;
|
||||
|
||||
use Chill\MainBundle\Cron\CronJobInterface;
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
|
||||
class MyCronJob implements CronJobInterface
|
||||
{
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||
{
|
||||
// the parameter $cronJobExecution contains data about the last execution of the cronjob
|
||||
// if it is null, it should be executed immediatly
|
||||
if (null === $cronJobExecution) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($cronJobExecution->getKey() !== $this->getKey()) {
|
||||
throw new UnexpectedValueException();
|
||||
}
|
||||
|
||||
// this cron job should be executed if the last execution is greater than one day, but only during the night
|
||||
|
||||
$now = new DateTimeImmutable('now');
|
||||
|
||||
return $cronJobExecution->getLastStart() < $now->sub(new DateInterval('P1D'))
|
||||
&& in_array($now->format('H'), self::ACCEPTED_HOURS, true)
|
||||
// introduce a random component to ensure a roll of task execution when multiple instances are hosted on same machines
|
||||
&& mt_rand(0, 5) === 0;
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return 'arbitrary-and-unique-key';
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// here, we execute the command
|
||||
}
|
||||
}
|
||||
|
||||
How are cron job scheduled ?
|
||||
============================
|
||||
|
||||
If the command :code:`chill:cron-job:execute` is run with one or more :code:`job` argument, those jobs are run, **without checking that the job can run** (the method :code:`canRun` is not executed).
|
||||
|
||||
If any :code:`job` argument is given, the :code:`CronManager` schedule job with those steps:
|
||||
|
||||
* the tasks are ordered, with:
|
||||
* a priority is given for tasks that weren't never executed;
|
||||
* then, the tasks are ordered, the last executed are the first in the list
|
||||
* then, for each tasks, and in the given order, the first task where :code:`canRun` return :code:`TRUE` will be executed.
|
||||
|
||||
The command :code:`chill:cron-job:execute` execute **only one** task.
|
||||
|
||||
|
||||
|
@ -34,6 +34,7 @@ As Chill rely on the `symfony <http://symfony.com>`_ framework, reading the fram
|
||||
Useful snippets <useful-snippets.rst>
|
||||
manual/index.rst
|
||||
Assets <assets.rst>
|
||||
Cron Jobs <cronjob.rst>
|
||||
|
||||
Layout and UI
|
||||
**************
|
||||
|
@ -18,6 +18,7 @@ Installation & Usage
|
||||
:maxdepth: 2
|
||||
|
||||
prod.rst
|
||||
load-addresses.rst
|
||||
prod-calendar-sms-sending.rst
|
||||
msgraph-configure.rst
|
||||
|
||||
@ -170,7 +171,7 @@ There are several users available:
|
||||
|
||||
The password is always ``password``.
|
||||
|
||||
Now, read `Operations` below.
|
||||
Now, read `Operations` below. For running in production, read `prod_`.
|
||||
|
||||
|
||||
Operations
|
||||
|
50
docs/source/installation/load-addresses.rst
Normal file
50
docs/source/installation/load-addresses.rst
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
.. _addresses:
|
||||
|
||||
Addresses
|
||||
*********
|
||||
|
||||
Chill can store a list of geolocated address references, which are used to suggest address and ensure that the data is correctly stored.
|
||||
|
||||
Those addresses may be load from a dedicated source.
|
||||
|
||||
In France
|
||||
=========
|
||||
|
||||
The address are loaded from the `BANO <https://bano.openstreetmap.fr/>`_. The postal codes are loaded from `the official list of
|
||||
postal codes <https://datanova.laposte.fr/explore/dataset/laposte_hexasmal/information/>`_
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# first, load postal codes
|
||||
bin/console chill:main:postal-code:load:FR
|
||||
# then, load all addresses, by departement (multiple departement can be loaded by repeating the departement code
|
||||
bin/console chill:main:address-ref-from-bano 57 54 51
|
||||
|
||||
In Belgium
|
||||
==========
|
||||
|
||||
Addresses are prepared from the `BeST Address data <https://www.geo.be/catalog/details/ca0fd5c0-8146-11e9-9012-482ae30f98d9>`_.
|
||||
|
||||
Postal code are loaded from this database. There is no need to load postal codes from another source (actually, this is strongly discouraged).
|
||||
|
||||
The data are prepared for Chill (`See this repository <https://gitea.champs-libres.be/Chill-project/belgian-bestaddresses-transform/releases>`_).
|
||||
One can select postal code by his first number (:code:`1xxx` for postal codes from 1000 to 1999), or a limited list for development purpose.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# load postal code from 1000 to 3999:
|
||||
bin/console chill:main:address-ref-from-best-addresse 1xxx 2xxx 3xxx
|
||||
|
||||
# load only an extract (for dev purposes)
|
||||
bin/console chill:main:address-ref-from-best-addresse extract
|
||||
|
||||
# load full addresses (discouraged)
|
||||
bin/console chill:main:address-ref-from-best-addresse full
|
||||
|
||||
.. note::
|
||||
|
||||
There is a possibility to load the full list of addresses is discouraged: the loading is optimized with smaller extracts.
|
||||
|
||||
Once you load the full list, it is not possible to load smaller extract: each extract loaded **after** will not
|
||||
delete the addresses loaded with the full extract (and some addresses will be present twice).
|
@ -1,10 +1,12 @@
|
||||
.. Copyright (C) 2014-2019 Champs Libres Cooperative SCRLFS
|
||||
Permission is granted to copy, distribute and/or modify this document
|
||||
under the terms of the GNU Free Documentation License, Version 1.3
|
||||
or any later version published by the Free Software Foundation;
|
||||
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
|
||||
A copy of the license is included in the section entitled "GNU
|
||||
Free Documentation License".
|
||||
Permission is granted to copy, distribute and/or modify this document
|
||||
under the terms of the GNU Free Documentation License, Version 1.3
|
||||
or any later version published by the Free Software Foundation;
|
||||
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
|
||||
A copy of the license is included in the section entitled "GNU
|
||||
Free Documentation License".
|
||||
|
||||
.. _prod:
|
||||
|
||||
Installation for production
|
||||
###########################
|
||||
@ -36,12 +38,25 @@ This should be adapted to your needs:
|
||||
|
||||
* Think about how you will backup your database. Some adminsys find easier to store database outside of docker, which might be easier to administrate or replicate.
|
||||
|
||||
Cron jobs
|
||||
=========
|
||||
|
||||
The command :code:`chill:cron-job:execute` should be executed every 15 minutes (more or less).
|
||||
|
||||
This command should never be executed concurrently. It should be not have more than one process for a single instance.
|
||||
|
||||
Post-install tasks
|
||||
==================
|
||||
|
||||
- import addresses. See :ref:`addresses`.
|
||||
|
||||
|
||||
Tweak symfony messenger
|
||||
=======================
|
||||
|
||||
Calendar sync is processed using symfony messenger.
|
||||
|
||||
You can tweak the configuration
|
||||
You can tweak the configuration
|
||||
|
||||
Going further:
|
||||
|
||||
|
@ -51,8 +51,13 @@ class CalendarForShortMessageProvider
|
||||
*/
|
||||
public function getCalendars(DateTimeImmutable $at): iterable
|
||||
{
|
||||
['startDate' => $startDate, 'endDate' => $endDate] = $this->rangeGenerator
|
||||
->generateRange($at);
|
||||
$range = $this->rangeGenerator->generateRange($at);
|
||||
|
||||
if (null === $range) {
|
||||
return;
|
||||
}
|
||||
|
||||
['startDate' => $startDate, 'endDate' => $endDate] = $range;
|
||||
|
||||
$offset = 0;
|
||||
$batchSize = 10;
|
||||
|
@ -31,14 +31,14 @@ use UnexpectedValueException;
|
||||
*/
|
||||
class DefaultRangeGenerator implements RangeGeneratorInterface
|
||||
{
|
||||
public function generateRange(\DateTimeImmutable $date): array
|
||||
public function generateRange(\DateTimeImmutable $date): ?array
|
||||
{
|
||||
$onMidnight = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date->format('Y-m-d') . ' 00:00:00');
|
||||
|
||||
switch ($dow = (int) $onMidnight->format('w')) {
|
||||
case 6: // Saturday
|
||||
case 0: // Sunday
|
||||
return ['startDate' => null, 'endDate' => null];
|
||||
return null;
|
||||
|
||||
case 1: // Monday
|
||||
// send for Tuesday and Wednesday
|
||||
|
@ -23,7 +23,7 @@ use DateTimeImmutable;
|
||||
interface RangeGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* @return array<startDate: \DateTimeImmutable, endDate: \DateTimeImmutable>
|
||||
* @return ?array{startDate: DateTimeImmutable, endDate: DateTimeImmutable} when return is null, then no ShortMessage must be send
|
||||
*/
|
||||
public function generateRange(DateTimeImmutable $date): array;
|
||||
public function generateRange(DateTimeImmutable $date): ?array;
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Repository\CalendarRepository;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\CalendarForShortMessageProvider;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\DefaultRangeGenerator;
|
||||
use Chill\CalendarBundle\Service\ShortMessageNotification\RangeGeneratorInterface;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@ -37,6 +38,32 @@ final class CalendarForShortMessageProviderTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testGenerateRangeIsNull()
|
||||
{
|
||||
$calendarRepository = $this->prophesize(CalendarRepository::class);
|
||||
$calendarRepository->findByNotificationAvailable(
|
||||
Argument::type(DateTimeImmutable::class),
|
||||
Argument::type(DateTimeImmutable::class),
|
||||
Argument::type('int'),
|
||||
Argument::exact(0)
|
||||
)->shouldBeCalledTimes(0);
|
||||
$rangeGenerator = $this->prophesize(RangeGeneratorInterface::class);
|
||||
$rangeGenerator->generateRange(Argument::type(DateTimeImmutable::class))->willReturn(null);
|
||||
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
$em->clear()->shouldNotBeCalled();
|
||||
|
||||
$provider = new CalendarForShortMessageProvider(
|
||||
$calendarRepository->reveal(),
|
||||
$em->reveal(),
|
||||
$rangeGenerator->reveal()
|
||||
);
|
||||
|
||||
$calendars = iterator_to_array($provider->getCalendars(new DateTimeImmutable('now')));
|
||||
|
||||
$this->assertEquals(0, count($calendars));
|
||||
}
|
||||
|
||||
public function testGetCalendars()
|
||||
{
|
||||
$calendarRepository = $this->prophesize(CalendarRepository::class);
|
||||
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle;
|
||||
|
||||
use Chill\MainBundle\Cron\CronJobInterface;
|
||||
use Chill\MainBundle\CRUD\CompilerPass\CRUDControllerCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\ExportsCompilerPass;
|
||||
@ -59,6 +60,8 @@ class ChillMainBundle extends Bundle
|
||||
->addTag('chill.count_notification.user');
|
||||
$container->registerForAutoconfiguration(EntityWorkflowHandlerInterface::class)
|
||||
->addTag('chill_main.workflow_handler');
|
||||
$container->registerForAutoconfiguration(CronJobInterface::class)
|
||||
->addTag('chill_main.cron_job');
|
||||
|
||||
$container->addCompilerPass(new SearchableServicesCompilerPass());
|
||||
$container->addCompilerPass(new ConfigConsistencyCompilerPass());
|
||||
|
55
src/Bundle/ChillMainBundle/Command/ExecuteCronJobCommand.php
Normal file
55
src/Bundle/ChillMainBundle/Command/ExecuteCronJobCommand.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Command;
|
||||
|
||||
use Chill\MainBundle\Cron\CronManagerInterface;
|
||||
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 ExecuteCronJobCommand extends Command
|
||||
{
|
||||
private CronManagerInterface $cronManager;
|
||||
|
||||
public function __construct(
|
||||
CronManagerInterface $cronManager
|
||||
) {
|
||||
parent::__construct('chill:cron-job:execute');
|
||||
|
||||
$this->cronManager = $cronManager;
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setDescription('Execute the cronjob(s) given as argument, or one cronjob scheduled by system.')
|
||||
->setHelp("If no job is specified, the next available cronjob will be executed by system.\nThis command should be execute every 15 minutes (more or less)")
|
||||
->addArgument('job', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'one or more job to force execute (by default, all jobs are executed)', [])
|
||||
->addUsage('');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
if ([] === $input->getArgument('job')) {
|
||||
$this->cronManager->run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach ($input->getArgument('job') as $jobName) {
|
||||
$this->cronManager->run($jobName);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
23
src/Bundle/ChillMainBundle/Cron/CronJobInterface.php
Normal file
23
src/Bundle/ChillMainBundle/Cron/CronJobInterface.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Cron;
|
||||
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
|
||||
interface CronJobInterface
|
||||
{
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool;
|
||||
|
||||
public function getKey(): string;
|
||||
|
||||
public function run(): void;
|
||||
}
|
180
src/Bundle/ChillMainBundle/Cron/CronManager.php
Normal file
180
src/Bundle/ChillMainBundle/Cron/CronManager.php
Normal file
@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Cron;
|
||||
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use Chill\MainBundle\Repository\CronJobExecutionRepositoryInterface;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use function array_key_exists;
|
||||
|
||||
/**
|
||||
* Manage cronjob and execute them.
|
||||
*
|
||||
* If any :code:`job` argument is given, the :code:`CronManager` schedule job with those steps:
|
||||
*
|
||||
* - the tasks are ordered, with:
|
||||
* - a priority is given for tasks that weren't never executed;
|
||||
* - then, the tasks are ordered, the last executed are the first in the list
|
||||
*
|
||||
* Then, for each tasks, and in the given order, the first task where :code:`canRun` return :code:`TRUE` will be executed.
|
||||
*
|
||||
* The error inside job execution are catched (with the exception of _out of memory error_).
|
||||
*
|
||||
* The manager will mark the task as executed even if an error is catched. This will lead as failed job
|
||||
* will not have priority any more on other tasks.
|
||||
*
|
||||
* If a tasks is "forced", there is no test about eligibility of the task (the `canRun` method is not called),
|
||||
* and the last task execution is not recorded.
|
||||
*/
|
||||
class CronManager implements CronManagerInterface
|
||||
{
|
||||
private const LOG_PREFIX = '[cron manager] ';
|
||||
|
||||
private const UPDATE_AFTER_EXEC = 'UPDATE ' . CronJobExecution::class . ' cr SET cr.lastEnd = :now, cr.lastStatus = :status WHERE cr.key = :key';
|
||||
|
||||
private const UPDATE_BEFORE_EXEC = 'UPDATE ' . CronJobExecution::class . ' cr SET cr.lastExecution = :now WHERE cr.key = :key';
|
||||
|
||||
private CronJobExecutionRepositoryInterface $cronJobExecutionRepository;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
/**
|
||||
* @var iterable<CronJobInterface>
|
||||
*/
|
||||
private iterable $jobs;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* @param CronJobInterface[] $jobs
|
||||
*/
|
||||
public function __construct(
|
||||
CronJobExecutionRepositoryInterface $cronJobExecutionRepository,
|
||||
EntityManagerInterface $entityManager,
|
||||
iterable $jobs,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->cronJobExecutionRepository = $cronJobExecutionRepository;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->jobs = $jobs;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function run(?string $forceJob = null): void
|
||||
{
|
||||
if (null !== $forceJob) {
|
||||
$this->runForce($forceJob);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
[$orderedJobs, $lasts] = $this->getOrderedJobs();
|
||||
|
||||
foreach ($orderedJobs as $job) {
|
||||
if ($job->canRun($lasts[$job->getKey()] ?? null)) {
|
||||
if (array_key_exists($job->getKey(), $lasts)) {
|
||||
$this->entityManager
|
||||
->createQuery(self::UPDATE_BEFORE_EXEC)
|
||||
->setParameters([
|
||||
'now' => new DateTimeImmutable('now'),
|
||||
'key' => $job->getKey(),
|
||||
]);
|
||||
} else {
|
||||
$execution = new CronJobExecution($job->getKey());
|
||||
$this->entityManager->persist($execution);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
$this->entityManager->clear();
|
||||
|
||||
try {
|
||||
$this->logger->info(sprintf('%sWill run job', self::LOG_PREFIX), ['job' => $job->getKey()]);
|
||||
$job->run();
|
||||
|
||||
$this->entityManager
|
||||
->createQuery(self::UPDATE_AFTER_EXEC)
|
||||
->setParameters([
|
||||
'now' => new DateTimeImmutable('now'),
|
||||
'status' => CronJobExecution::SUCCESS,
|
||||
'key' => $job->getKey(),
|
||||
])
|
||||
->execute();
|
||||
|
||||
$this->logger->info(sprintf('%sSuccessfully run job', self::LOG_PREFIX), ['job' => $job->getKey()]);
|
||||
|
||||
return;
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error(sprintf('%sRunning job failed', self::LOG_PREFIX), ['job' => $job->getKey()]);
|
||||
$this->entityManager
|
||||
->createQuery(self::UPDATE_AFTER_EXEC)
|
||||
->setParameters([
|
||||
'now' => new DateTimeImmutable('now'),
|
||||
'status' => CronJobExecution::FAILURE,
|
||||
'key' => $job->getKey(),
|
||||
])
|
||||
->execute();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<0: CronJobInterface[], 1: array<string, CronJobExecution>>
|
||||
*/
|
||||
private function getOrderedJobs(): array
|
||||
{
|
||||
/** @var array<string, CronJobExecution> $lasts */
|
||||
$lasts = [];
|
||||
|
||||
foreach ($this->cronJobExecutionRepository->findAll() as $execution) {
|
||||
$lasts[$execution->getKey()] = $execution;
|
||||
}
|
||||
|
||||
// order by last, NULL first
|
||||
$orderedJobs = iterator_to_array($this->jobs);
|
||||
usort(
|
||||
$orderedJobs,
|
||||
static function (CronJobInterface $a, CronJobInterface $b) use ($lasts): int {
|
||||
if (
|
||||
(!array_key_exists($a->getKey(), $lasts) && !array_key_exists($b->getKey(), $lasts))
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!array_key_exists($a->getKey(), $lasts) && array_key_exists($b->getKey(), $lasts)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!array_key_exists($b->getKey(), $lasts) && array_key_exists($a->getKey(), $lasts)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return $lasts[$a->getKey()]->getLastStart() <=> $lasts[$b->getKey()]->getLastStart();
|
||||
}
|
||||
);
|
||||
|
||||
return [$orderedJobs, $lasts];
|
||||
}
|
||||
|
||||
private function runForce(string $forceJob): void
|
||||
{
|
||||
foreach ($this->jobs as $job) {
|
||||
if ($job->getKey() === $forceJob) {
|
||||
$job->run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
src/Bundle/ChillMainBundle/Cron/CronManagerInterface.php
Normal file
20
src/Bundle/ChillMainBundle/Cron/CronManagerInterface.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Cron;
|
||||
|
||||
interface CronManagerInterface
|
||||
{
|
||||
/**
|
||||
* Execute one job, with a given priority, or the given job (identified by his key).
|
||||
*/
|
||||
public function run(?string $forceJob = null): void;
|
||||
}
|
95
src/Bundle/ChillMainBundle/Entity/CronJobExecution.php
Normal file
95
src/Bundle/ChillMainBundle/Entity/CronJobExecution.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Entity;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="chill_main_cronjob_execution")
|
||||
*/
|
||||
class CronJobExecution
|
||||
{
|
||||
public const FAILURE = 100;
|
||||
|
||||
public const SUCCESS = 1;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", nullable=false)
|
||||
* @ORM\Id
|
||||
*/
|
||||
private string $key;
|
||||
|
||||
/**
|
||||
* @var DateTimeImmutable
|
||||
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null})
|
||||
*/
|
||||
private ?DateTimeImmutable $lastEnd = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=false)
|
||||
*/
|
||||
private DateTimeImmutable $lastStart;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer", nullable=true, options={"default": null})
|
||||
*/
|
||||
private ?int $lastStatus = null;
|
||||
|
||||
public function __construct(string $key)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->lastStart = new DateTimeImmutable('now');
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function getLastEnd(): DateTimeImmutable
|
||||
{
|
||||
return $this->lastEnd;
|
||||
}
|
||||
|
||||
public function getLastStart(): DateTimeImmutable
|
||||
{
|
||||
return $this->lastStart;
|
||||
}
|
||||
|
||||
public function getLastStatus(): ?int
|
||||
{
|
||||
return $this->lastStatus;
|
||||
}
|
||||
|
||||
public function setLastEnd(?DateTimeImmutable $lastEnd): CronJobExecution
|
||||
{
|
||||
$this->lastEnd = $lastEnd;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLastStart(DateTimeImmutable $lastStart): CronJobExecution
|
||||
{
|
||||
$this->lastStart = $lastStart;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLastStatus(?int $lastStatus): CronJobExecution
|
||||
{
|
||||
$this->lastStatus = $lastStatus;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
class CronJobExecutionRepository implements CronJobExecutionRepositoryInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->repository = $entityManager->getRepository($this->getClassName());
|
||||
}
|
||||
|
||||
public function find($id): ?CronJobExecution
|
||||
{
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|CronJobExecution[]
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|CronJobExecution[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria): ?CronJobExecution
|
||||
{
|
||||
return $this->repository->findOneBy($criteria);
|
||||
}
|
||||
|
||||
public function getClassName(): string
|
||||
{
|
||||
return CronJobExecution::class;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
interface CronJobExecutionRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public function find($id): ?CronJobExecution;
|
||||
|
||||
/**
|
||||
* @return array|CronJobExecution[]
|
||||
*/
|
||||
public function findAll(): array;
|
||||
|
||||
/**
|
||||
* @return array|CronJobExecution[]
|
||||
*/
|
||||
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
public function findOneBy(array $criteria): ?CronJobExecution;
|
||||
|
||||
public function getClassName(): string;
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Service\AddressGeographicalUnit;
|
||||
|
||||
use Chill\MainBundle\Cron\CronJobInterface;
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use UnexpectedValueException;
|
||||
use function in_array;
|
||||
|
||||
class RefreshAddressToGeographicalUnitMaterializedViewCronJob implements CronJobInterface
|
||||
{
|
||||
private const ACCEPTED_HOURS = ['0', '1', '2', '3', '4', '5'];
|
||||
|
||||
private Connection $connection;
|
||||
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||
{
|
||||
if (null === $cronJobExecution) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($cronJobExecution->getKey() !== $this->getKey()) {
|
||||
throw new UnexpectedValueException();
|
||||
}
|
||||
|
||||
$now = new DateTimeImmutable('now');
|
||||
|
||||
return $cronJobExecution->getLastStart() < $now->sub(new DateInterval('P1D'))
|
||||
&& in_array($now->format('H'), self::ACCEPTED_HOURS, true)
|
||||
// introduce a random component to ensure a roll when multiple instances are hosted on same machines
|
||||
&& mt_rand(0, 5) === 0;
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return 'refresh-materialized-view-address-to-geog-units';
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->connection->executeQuery('REFRESH MATERIALIZED VIEW view_chill_main_address_geographical_unit');
|
||||
}
|
||||
}
|
201
src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php
Normal file
201
src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php
Normal file
@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Tests\Cron;
|
||||
|
||||
use ArrayObject;
|
||||
use Chill\MainBundle\Cron\CronJobInterface;
|
||||
use Chill\MainBundle\Cron\CronManager;
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use Chill\MainBundle\Repository\CronJobExecutionRepositoryInterface;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use function array_key_exists;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class CronManagerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testScheduleMostOldExecutedJob()
|
||||
{
|
||||
$jobOld1 = new JobCanRun('k');
|
||||
$jobOld2 = new JobCanRun('l');
|
||||
$jobToExecute = $this->prophesize(CronJobInterface::class);
|
||||
$jobToExecute->getKey()->willReturn('to-exec');
|
||||
$jobToExecute->canRun(Argument::type(CronJobExecution::class))->willReturn(true);
|
||||
$jobToExecute->run()->shouldBeCalled();
|
||||
|
||||
$executions = [
|
||||
['key' => $jobOld1->getKey(), 'lastStart' => new DateTimeImmutable('yesterday'), 'lastEnd' => new DateTimeImmutable('1 hours ago'), 'lastStatus' => CronJobExecution::SUCCESS],
|
||||
['key' => $jobOld2->getKey(), 'lastStart' => new DateTimeImmutable('3 days ago'), 'lastEnd' => new DateTimeImmutable('36 hours ago'), 'lastStatus' => CronJobExecution::SUCCESS],
|
||||
// this is the oldest one
|
||||
['key' => 'to-exec', 'lastStart' => new DateTimeImmutable('1 month ago'), 'lastEnd' => new DateTimeImmutable('10 days ago'), 'lastStatus' => CronJobExecution::SUCCESS],
|
||||
];
|
||||
|
||||
$cronManager = new CronManager(
|
||||
$this->buildCronJobExecutionRepository($executions),
|
||||
$this->buildEntityManager([]),
|
||||
new ArrayObject([$jobOld1, $jobToExecute->reveal(), $jobOld2]),
|
||||
new NullLogger()
|
||||
);
|
||||
|
||||
$cronManager->run();
|
||||
}
|
||||
|
||||
public function testSelectNewJobFirstAndNewJobIsFirstInList(): void
|
||||
{
|
||||
$jobAlreadyExecuted = new JobCanRun('k');
|
||||
$jobNeverExecuted = $this->prophesize(CronJobInterface::class);
|
||||
$jobNeverExecuted->getKey()->willReturn('never-executed');
|
||||
$jobNeverExecuted->run()->shouldBeCalled();
|
||||
$jobNeverExecuted->canRun(null)->willReturn(true);
|
||||
|
||||
$executions = [
|
||||
['key' => $jobAlreadyExecuted->getKey(), 'lastStart' => new DateTimeImmutable('yesterday'), 'lastEnd' => new DateTimeImmutable('1 hours ago'), 'lastStatus' => CronJobExecution::SUCCESS],
|
||||
];
|
||||
|
||||
$cronManager = new CronManager(
|
||||
$this->buildCronJobExecutionRepository($executions),
|
||||
$this->buildEntityManager([Argument::type(CronJobExecution::class)]),
|
||||
new ArrayObject([$jobNeverExecuted->reveal(), $jobAlreadyExecuted]),
|
||||
new NullLogger()
|
||||
);
|
||||
|
||||
$cronManager->run();
|
||||
}
|
||||
|
||||
public function testSelectNewJobFirstAndNewJobIsLastInList(): void
|
||||
{
|
||||
$jobAlreadyExecuted = new JobCanRun('k');
|
||||
$jobNeverExecuted = $this->prophesize(CronJobInterface::class);
|
||||
$jobNeverExecuted->getKey()->willReturn('never-executed');
|
||||
$jobNeverExecuted->run()->shouldBeCalled();
|
||||
$jobNeverExecuted->canRun(null)->willReturn(true);
|
||||
|
||||
$executions = [
|
||||
['key' => $jobAlreadyExecuted->getKey(), 'lastStart' => new DateTimeImmutable('yesterday'), 'lastEnd' => new DateTimeImmutable('1 hours ago'), 'lastStatus' => CronJobExecution::SUCCESS],
|
||||
];
|
||||
|
||||
$cronManager = new CronManager(
|
||||
$this->buildCronJobExecutionRepository($executions),
|
||||
$this->buildEntityManager([Argument::type(CronJobExecution::class)]),
|
||||
new ArrayObject([$jobAlreadyExecuted, $jobNeverExecuted->reveal()]),
|
||||
new NullLogger()
|
||||
);
|
||||
|
||||
$cronManager->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array{key: string, ?lastEnd: ?DateTimeImmutable, lastStart: DateTimeImmutable, ?lastStatus: ?int}> $executions
|
||||
*/
|
||||
private function buildCronJobExecutionRepository(array $executions): CronJobExecutionRepositoryInterface
|
||||
{
|
||||
$repository = $this->prophesize(CronJobExecutionRepositoryInterface::class);
|
||||
|
||||
$repository->findAll()->willReturn(
|
||||
array_map(
|
||||
static function (array $exec): CronJobExecution {
|
||||
$e = new CronJobExecution($exec['key']);
|
||||
$e->setLastStart($exec['lastStart']);
|
||||
|
||||
if (array_key_exists('lastEnd', $exec)) {
|
||||
$e->setLastEnd($exec['lastEnd']);
|
||||
}
|
||||
|
||||
if (array_key_exists('lastStatus', $exec)) {
|
||||
$e->setLastStatus($exec['lastStatus']);
|
||||
}
|
||||
|
||||
return $e;
|
||||
},
|
||||
$executions
|
||||
)
|
||||
);
|
||||
|
||||
return $repository->reveal();
|
||||
}
|
||||
|
||||
private function buildEntityManager(array $persistArgsShouldBeCalled = []): EntityManagerInterface
|
||||
{
|
||||
$em = $this->prophesize(EntityManagerInterface::class);
|
||||
|
||||
if ([] === $persistArgsShouldBeCalled) {
|
||||
$em->persist(Argument::any())->shouldNotBeCalled();
|
||||
} else {
|
||||
foreach ($persistArgsShouldBeCalled as $arg) {
|
||||
$em->persist($arg)->shouldBeCalled();
|
||||
}
|
||||
$em->flush()->shouldBeCalled();
|
||||
}
|
||||
|
||||
// other methods
|
||||
$em->clear()->shouldBeCalled();
|
||||
|
||||
$query = $this->prophesize(AbstractQuery::class);
|
||||
$query->setParameters(Argument::type('array'))->willReturn($query->reveal());
|
||||
$query->execute()->shouldBeCalled();
|
||||
|
||||
$em->createQuery(Argument::type('string'))->willReturn($query->reveal());
|
||||
|
||||
return $em->reveal();
|
||||
}
|
||||
}
|
||||
|
||||
class JobCanRun implements CronJobInterface
|
||||
{
|
||||
private string $key;
|
||||
|
||||
public function __construct(string $key)
|
||||
{
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class JobCannotRun implements CronJobInterface
|
||||
{
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return 'job-b';
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Tests\Services\AddressGeographicalUnit;
|
||||
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class RefreshAddressToGeographicalUnitMaterializedViewCronJobTest extends KernelTestCase
|
||||
{
|
||||
private Connection $connection;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::bootKernel();
|
||||
$this->connection = self::$container->get(Connection::class);
|
||||
}
|
||||
|
||||
public function testFullRun(): void
|
||||
{
|
||||
$job = new \Chill\MainBundle\Service\AddressGeographicalUnit\RefreshAddressToGeographicalUnitMaterializedViewCronJob(
|
||||
$this->connection
|
||||
);
|
||||
|
||||
$lastExecution = new CronJobExecution($job->getKey());
|
||||
$lastExecution->setLastStart(new DateTimeImmutable('2 days ago'));
|
||||
|
||||
$this->assertIsBool($job->canRun($lastExecution));
|
||||
|
||||
$job->run();
|
||||
}
|
||||
}
|
@ -102,12 +102,19 @@ services:
|
||||
|
||||
Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface: '@Chill\MainBundle\Security\Resolver\CenterResolverDispatcher'
|
||||
|
||||
Chill\MainBundle\Service\Import\:
|
||||
resource: '../Service/Import/'
|
||||
Chill\MainBundle\Service\:
|
||||
resource: '../Service/'
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\MainBundle\Service\RollingDate\:
|
||||
resource: '../Service/RollingDate/'
|
||||
Chill\MainBundle\Cron\:
|
||||
resource: '../Cron'
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\MainBundle\Cron\CronManager:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
lazy: true
|
||||
arguments:
|
||||
$jobs: !tagged_iterator chill_main.cron_job
|
||||
|
@ -61,3 +61,9 @@ services:
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: console.command }
|
||||
|
||||
Chill\MainBundle\Command\ExecuteCronJobCommand:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
tags:
|
||||
- {name: console.command }
|
||||
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\Migrations\Main;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20221212163734 extends AbstractMigration
|
||||
{
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE chill_main_cronjob_execution');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Table for executed jobs';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE chill_main_cronjob_execution (key TEXT NOT NULL, lastEnd TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
|
||||
lastStart TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, lastStatus INTEGER DEFAULT NULL, PRIMARY KEY(key))');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_cronjob_execution.lastEnd IS \'(DC2Type:datetime_immutable)\'');
|
||||
$this->addSql('COMMENT ON COLUMN chill_main_cronjob_execution.lastStart IS \'(DC2Type:datetime_immutable)\'');
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@
|
||||
<td>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_crud_person_household_position_edit', { 'id': entity.id }) }}" class="btn btn-edit"></a>
|
||||
<a href="{{ chill_path_add_return_path('chill_crud_person_household_composition_type_edit', { 'id': entity.id }) }}" class="btn btn-edit"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
|
Loading…
x
Reference in New Issue
Block a user