Skip to content

Commit

Permalink
[CronJob/SonataIntegrationBundle] Add cron job execution state (#250)
Browse files Browse the repository at this point in the history
  • Loading branch information
DumitracheAdrian authored Apr 23, 2024
1 parent d490220 commit 89f7f07
Show file tree
Hide file tree
Showing 18 changed files with 237 additions and 43 deletions.
33 changes: 33 additions & 0 deletions app/migrations/Version20240423100707.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240423100707 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE cron_job__cron_job_execution ADD state VARCHAR(20) DEFAULT \'requested\' NOT NULL');
$this->addSql('CREATE INDEX state ON cron_job__cron_job_execution (state)');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP INDEX state ON cron_job__cron_job_execution');
$this->addSql('ALTER TABLE cron_job__cron_job_execution DROP state');
}
}
14 changes: 12 additions & 2 deletions packages/cron-job/CronJobProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,24 @@ public function queue(CronJob $cronJob, bool $force): void

public function process(CronJobExecution $execution): void
{
$manager = $this->managerRegistry->getManagerForClass(CronJobExecution::class);

if (!$execution->isExecutable(new \DateTimeImmutable())) {
$execution->skip();
$manager->flush();

return;
}

$event = $this->eventDispatcher->dispatch(new PreCronJobExecutionEvent($execution));

if ($event->isExecutionCancelled()) {
$execution->skip();
$manager->flush();

return;
}

$manager = $this->managerRegistry->getManagerForClass(CronJobExecution::class);

$execution->start();
$manager->flush();

Expand Down
50 changes: 50 additions & 0 deletions packages/cron-job/Entity/CronJobExecution.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,26 @@
#[
ORM\Entity,
ORM\Table(name: 'cron_job__cron_job_execution'),
ORM\Index(fields: ['state'], name: 'state'),
]
class CronJobExecution implements \Stringable
{
public const STATE_REQUESTED = 'requested';
public const STATE_RUNNING = 'running';
public const STATE_TERMINATED = 'terminated';
public const STATE_ERRORED = 'errored';
public const STATE_SKIPPED = 'skipped';
public const STATE_ACKNOWLEDGED = 'acknowledged';

public const STATES = [
self::STATE_REQUESTED,
self::STATE_RUNNING,
self::STATE_TERMINATED,
self::STATE_ERRORED,
self::STATE_SKIPPED,
self::STATE_ACKNOWLEDGED,
];

#[
ORM\Id,
ORM\GeneratedValue,
Expand All @@ -22,6 +39,9 @@ class CronJobExecution implements \Stringable
#[ORM\Column(name: 'requested_at', type: 'datetime_immutable', nullable: false)]
private \DateTimeImmutable $requestedAt;

#[ORM\Column(name: 'state', type: 'string', length: 20, nullable: false, options: ['default' => self::STATE_REQUESTED])]
private string $state = self::STATE_REQUESTED;

#[ORM\Column(name: '`force`', type: 'boolean', nullable: false, options: ['default' => false])]
private bool $force;

Expand Down Expand Up @@ -74,6 +94,18 @@ public function getRequestedAt(): ?\DateTimeImmutable
return $this->requestedAt;
}

public function getState(): string
{
return $this->state;
}

private function setState(string $state): self
{
$this->state = $state;

return $this;
}

public function isForce(): bool
{
return $this->force;
Expand Down Expand Up @@ -166,13 +198,15 @@ public function isExecutable(\DateTimeImmutable $dateTime): bool
public function start(): void
{
$this
->setState(self::STATE_RUNNING)
->setExecutionStartedAt(new \DateTimeImmutable())
->setExecutionEndedAt(null);
}

public function end(): static
{
$this
->setState(self::STATE_TERMINATED)
->setExitCode(0)
->setExecutionEndedAt($executionEndedAt = new \DateTimeImmutable())
->setExecutionDelay(
Expand All @@ -186,10 +220,26 @@ public function fail(?int $exitCode, ?array $error): void
{
$this
->end()
->setState(self::STATE_ERRORED)
->setExitCode($exitCode)
->setError($error);
}

public function acknowledge(): void
{
$this->setState(self::STATE_ACKNOWLEDGED);
}

public function skip(): void
{
$this->setState(self::STATE_SKIPPED);
}

public function canBeAcknowledged(): bool
{
return self::STATE_ERRORED === $this->getState();
}

public function __toString(): string
{
return implode(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ public function __construct(
#[AsMessageHandler]
public function handleExecuteCronJobMessage(ExecuteCronJobMessage $message): void
{
if (!($execution = $message->getExecution())->isExecutable(new \DateTimeImmutable())) {
return;
}

$this->cronJobProcessor->process($execution);
$this->cronJobProcessor->process($message->getExecution());
}
}
2 changes: 1 addition & 1 deletion packages/cron-job/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ draw_sonata_integration:
cron_job_execution:
group: Cron Job
entity_class: Draw\Component\CronJob\Entity\CronJobExecution
controller_class: sonata.admin.controller.crud
controller_class: Draw\Bundle\SonataIntegrationBundle\CronJob\Controller\CronJobExecutionController
icon: null
label: Cron Job Execution
pager_type: simple
Expand Down
33 changes: 31 additions & 2 deletions packages/cron-job/Tests/CronJobProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public function testProcess(

$this->cronJobProcessor->process($execution);

static::assertEquals(CronJobExecution::STATE_TERMINATED, $execution->getState());
static::assertNotNull($execution->getExecutionStartedAt());
static::assertNotNull($execution->getExecutionEndedAt());
static::assertEquals(
Expand Down Expand Up @@ -225,13 +226,37 @@ public function testProcessWithError(): void

$this->cronJobProcessor->process($execution);

static::assertEquals(CronJobExecution::STATE_ERRORED, $execution->getState());
static::assertNotNull($execution->getExecutionStartedAt());
static::assertNotNull($execution->getExecutionEndedAt());
static::assertNotNull($execution->getExecutionDelay());
static::assertEquals($exitCode, $execution->getExitCode());
static::assertNotNull($execution->getError());
}

public function testProcessWithInactiveCronJob(): void
{
$this->eventDispatcher
->expects(static::never())
->method('dispatch');

$this->entityManager
->expects(static::once())
->method('flush');

$this->processFactory
->expects(static::never())
->method('createFromShellCommandLine');

$this->cronJobProcessor->process(
$execution = (new CronJob())
->setActive(false)
->newExecution()
);

static::assertEquals(CronJobExecution::STATE_SKIPPED, $execution->getState());
}

public function testProcessWithCancelledExecution(): void
{
$this->eventDispatcher
Expand All @@ -245,20 +270,24 @@ public function testProcessWithCancelledExecution(): void
);

$this->entityManager
->expects(static::never())
->expects(static::once())
->method('flush');

$this->processFactory
->expects(static::never())
->method('createFromShellCommandLine');

$this->cronJobProcessor->process($execution);

static::assertEquals(CronJobExecution::STATE_SKIPPED, $execution->getState());
}

private function createCronJobExecution(string $command = 'bin/console draw:test:execute'): CronJobExecution
{
return new CronJobExecution(
(new CronJob())->setCommand($command),
(new CronJob())
->setActive(true)
->setCommand($command),
new \DateTimeImmutable(),
false
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Draw\Component\CronJob\Tests\MessageHandler;

use Draw\Component\CronJob\CronJobProcessor;
use Draw\Component\CronJob\Entity\CronJob;
use Draw\Component\CronJob\Entity\CronJobExecution;
use Draw\Component\CronJob\Message\ExecuteCronJobMessage;
use Draw\Component\CronJob\MessageHandler\ExecuteCronJobMessageHandler;
Expand Down Expand Up @@ -33,32 +34,13 @@ public function testHandleExecuteCronJobMessage(): void
$this->cronJobProcessor
->expects(static::once())
->method('process')
->with($execution = $this->createCronJobExecution());
->with($execution = (new CronJob())->newExecution());

$this->handler->handleExecuteCronJobMessage(
new ExecuteCronJobMessage($execution)
);
}

public function testHandleExecuteCronJobMessageWithNotExecutableExecution(): void
{
$this->cronJobProcessor
->expects(static::never())
->method('process');

$this->handler->handleExecuteCronJobMessage(
new ExecuteCronJobMessage($this->createCronJobExecution(false))
);
}

private function createCronJobExecution(bool $executable = true): CronJobExecution&MockObject
{
$execution = $this->createMock(CronJobExecution::class);
$execution
->expects(static::any())
->method('isExecutable')
->willReturn($executable);

return $execution;
static::assertEquals(CronJobExecution::STATE_REQUESTED, $execution->getState());
static::assertNotNull($execution->getRequestedAt());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

namespace Draw\Bundle\SonataIntegrationBundle\CronJob\Admin;

use Draw\Component\CronJob\Entity\CronJobExecution;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\Type\ModelAutocompleteType;
use Sonata\AdminBundle\Route\RouteCollectionInterface;
use Sonata\AdminBundle\Show\ShowMapper;
use Sonata\DoctrineORMAdminBundle\Filter\ChoiceFilter;
use Sonata\DoctrineORMAdminBundle\Filter\ModelFilter;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

class CronJobExecutionAdmin extends AbstractAdmin
{
Expand All @@ -29,6 +32,21 @@ protected function configureDatagridFilters(DatagridMapper $filter): void
]
)
->add('requestedAt')
->add(
'state',
ChoiceFilter::class,
[
'field_type' => ChoiceType::class,
'field_options' => [
'multiple' => true,
'choices' => array_combine(
CronJobExecution::STATES,
CronJobExecution::STATES
),
],
'show_filter' => true,
]
)
->add('force')
->add('executionStartedAt')
->add('executionEndedAt')
Expand All @@ -53,6 +71,7 @@ protected function configureListFields(ListMapper $list): void
]
)
->add('requestedAt')
->add('state')
->add('force')
->add('executionStartedAt')
->add('executionEndedAt')
Expand All @@ -64,6 +83,9 @@ protected function configureListFields(ListMapper $list): void
[
'actions' => [
'show' => [],
'acknowledge' => [
'template' => '@DrawSonataIntegration/CronJob/CronJobExecution/list__action_acknowledge.html.twig',
],
'delete' => [],
],
]
Expand All @@ -74,6 +96,7 @@ protected function configureShowFields(ShowMapper $show): void
{
$show
->add('requestedAt')
->add('state')
->add('force')
->add('executionStartedAt')
->add('executionEndedAt')
Expand All @@ -84,7 +107,20 @@ protected function configureShowFields(ShowMapper $show): void

protected function configureRoutes(RouteCollectionInterface $collection): void
{
$collection->clearExcept(['list', 'show', 'delete']);
$collection->add('acknowledge', sprintf('%s/acknowledge', $this->getRouterIdParameter()));
$collection->remove('create');
$collection->remove('edit');
}

protected function configureActionButtons(array $buttonList, string $action, ?object $object = null): array
{
if ('show' === $action && $object?->canBeAcknowledged()) {
$buttonList['acknowledge'] = [
'template' => '@DrawSonataIntegration/CronJob/CronJobExecution/show__action_acknowledge.html.twig',
];
}

return $buttonList;
}

public function configureGridFields(array $fields): array
Expand All @@ -93,6 +129,7 @@ public function configureGridFields(array $fields): array
$fields,
[
'requestedAt' => [],
'state' => [],
'force' => [],
'executionStartedAt' => [],
'executionEndedAt' => [],
Expand Down
Loading

0 comments on commit 89f7f07

Please sign in to comment.