Suffix message class with 'Message' and add check on workflow to assert no transitions were applied since message placed in queue

This commit is contained in:
Julie Lenaerts 2024-08-28 11:52:01 +02:00 committed by Julien Fastré
parent 5d84e997c1
commit cb446edd18
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
5 changed files with 43 additions and 21 deletions

View File

@ -54,8 +54,8 @@ class CancelStaleWorkflowCronJob implements CronJobInterface
foreach ($staleWorkflowIds as $wId) { foreach ($staleWorkflowIds as $wId) {
try { try {
$this->messageBus->dispatch(new CancelStaleWorkflow($wId)); $this->messageBus->dispatch(new CancelStaleWorkflowMessage($wId));
$lastCanceled = $wId; $lastCanceled = max($wId, $lastCanceled);
++$processedCount; ++$processedCount;
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error("Failed to dispatch CancelStaleWorkflow for ID {$wId}", ['exception' => $e]); $this->logger->error("Failed to dispatch CancelStaleWorkflow for ID {$wId}", ['exception' => $e]);

View File

@ -14,6 +14,7 @@ namespace Chill\MainBundle\Service\Workflow;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Registry;
@ -21,13 +22,29 @@ use Symfony\Component\Workflow\Registry;
#[AsMessageHandler] #[AsMessageHandler]
class CancelStaleWorkflowHandler class CancelStaleWorkflowHandler
{ {
public function __construct(private readonly EntityWorkflowRepository $workflowRepository, private readonly Registry $registry, private EntityManagerInterface $em, private LoggerInterface $logger) {} public const string KEEP_INTERVAL = 'P90D';
public function __invoke(CancelStaleWorkflow $message): void public function __construct(private readonly EntityWorkflowRepository $workflowRepository, private readonly Registry $registry, private readonly EntityManagerInterface $em, private readonly LoggerInterface $logger, private readonly ClockInterface $clock) {}
public function __invoke(CancelStaleWorkflowMessage $message): void
{ {
$workflowId = $message->getWorkflowId(); $workflowId = $message->getWorkflowId();
$olderThanDate = $this->clock->now()->sub(new \DateInterval(self::KEEP_INTERVAL));
$staleWorkflows = $this->workflowRepository->findWorkflowsWithoutFinalStepAndOlderThan($olderThanDate);
$workflow = $this->workflowRepository->find($workflowId); $workflow = $this->workflowRepository->find($workflowId);
if (in_array($workflow, $staleWorkflows, true)) {
$this->logger->alert('Workflow has transitioned in the meantime.', [$workflowId]);
return;
}
if (null === $workflow) {
$this->logger->alert('Workflow was not found!', [$workflowId]);
return;
}
$workflowComponent = $this->registry->get($workflow, $workflow->getWorkflowName()); $workflowComponent = $this->registry->get($workflow, $workflow->getWorkflowName());
$metadataStore = $workflowComponent->getMetadataStore(); $metadataStore = $workflowComponent->getMetadataStore();
$transitions = $workflowComponent->getEnabledTransitions($workflow); $transitions = $workflowComponent->getEnabledTransitions($workflow);
@ -44,15 +61,15 @@ class CancelStaleWorkflowHandler
$isFinalPositive = $metadataStore->getMetadata('isFinalPositive', $transition); $isFinalPositive = $metadataStore->getMetadata('isFinalPositive', $transition);
if ($isFinal && !$isFinalPositive) { if ($isFinal && !$isFinalPositive) {
$workflowComponent->apply($workflow, 'annule'); $workflowComponent->apply($workflow, $transition->getName());
$this->logger->info(sprintf('EntityWorkflow %d has been transitioned.', $workflowId)); $this->logger->info('EntityWorkflow has been cancelled automatically.', [$workflowId]);
$transitionApplied = true; $transitionApplied = true;
break; break;
} }
} }
if (!$transitionApplied) { if (!$transitionApplied) {
$this->logger->error(sprintf('No valid transition found for EntityWorkflow %d.', $workflowId)); $this->logger->error('No valid transition found for EntityWorkflow %d.', [$workflowId]);
throw new UnrecoverableMessageHandlingException(sprintf('No valid transition found for EntityWorkflow %d.', $workflowId)); throw new UnrecoverableMessageHandlingException(sprintf('No valid transition found for EntityWorkflow %d.', $workflowId));
} }
} }

View File

@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Service\Workflow; namespace Chill\MainBundle\Service\Workflow;
class CancelStaleWorkflow class CancelStaleWorkflowMessage
{ {
public function __construct(public int $workflowId) {} public function __construct(public int $workflowId) {}

View File

@ -15,6 +15,10 @@ use Chill\MainBundle\Entity\CronJobExecution;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Service\Workflow\CancelStaleWorkflow; use Chill\MainBundle\Service\Workflow\CancelStaleWorkflow;
use Chill\MainBundle\Service\Workflow\CancelStaleWorkflowCronJob; use Chill\MainBundle\Service\Workflow\CancelStaleWorkflowCronJob;
use Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage;
use DateInvalidTimeZoneException;
use DateMalformedStringException;
use DateTimeImmutable;
use Doctrine\DBAL\Connection; use Doctrine\DBAL\Connection;
use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\MockObject\Exception;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -43,7 +47,7 @@ class CancelStaleWorkflowCronJobTest extends KernelTestCase
*/ */
public function testCanRun(?CronJobExecution $cronJobExecution, bool $expected): void public function testCanRun(?CronJobExecution $cronJobExecution, bool $expected): void
{ {
$clock = new MockClock(new \DateTimeImmutable('2024-01-01 00:00:00', new \DateTimeZone('+00:00'))); $clock = new MockClock(new DateTimeImmutable('2024-01-01 00:00:00', new \DateTimeZone('+00:00')));
$logger = $this->createMock(LoggerInterface::class); $logger = $this->createMock(LoggerInterface::class);
$cronJob = new CancelStaleWorkflowCronJob($this->createMock(EntityWorkflowRepository::class), $clock, $this->buildMessageBus(), $logger); $cronJob = new CancelStaleWorkflowCronJob($this->createMock(EntityWorkflowRepository::class), $clock, $this->buildMessageBus(), $logger);
@ -52,17 +56,17 @@ class CancelStaleWorkflowCronJobTest extends KernelTestCase
} }
/** /**
* @throws \DateMalformedStringException * @throws DateMalformedStringException
* @throws \DateInvalidTimeZoneException * @throws DateInvalidTimeZoneException
* @throws Exception * @throws \Exception|Exception
*/ */
public function testRun(): void public function testRun(): void
{ {
$clock = new MockClock((new \DateTimeImmutable('now', new \DateTimeZone('+00:00')))->add(new \DateInterval('P120D'))); $clock = new MockClock((new DateTimeImmutable('now', new \DateTimeZone('+00:00')))->add(new \DateInterval('P120D')));
$workflowRepository = $this->createMock(EntityWorkflowRepository::class); $workflowRepository = $this->createMock(EntityWorkflowRepository::class);
$logger = $this->createMock(LoggerInterface::class); $logger = $this->createMock(LoggerInterface::class);
$workflowRepository->method('findWorkflowsWithoutFinalStepAndOlderThan')->willReturn([1, 2, 3]); $workflowRepository->method('findWorkflowsWithoutFinalStepAndOlderThan')->willReturn([1, 3, 2]);
$messageBus = $this->buildMessageBus(true); $messageBus = $this->buildMessageBus(true);
$cronJob = new CancelStaleWorkflowCronJob($workflowRepository, $clock, $messageBus, $logger); $cronJob = new CancelStaleWorkflowCronJob($workflowRepository, $clock, $messageBus, $logger);
@ -80,17 +84,17 @@ class CancelStaleWorkflowCronJobTest extends KernelTestCase
public static function buildTestCanRunData(): iterable public static function buildTestCanRunData(): iterable
{ {
yield [ yield [
(new CronJobExecution('last-canceled-workflow-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:00', new \DateTimeZone('+00:00'))), (new CronJobExecution('last-canceled-workflow-id'))->setLastEnd(new DateTimeImmutable('2023-12-31 00:00:00', new \DateTimeZone('+00:00'))),
true, true,
]; ];
yield [ yield [
(new CronJobExecution('last-canceled-workflow-id'))->setLastEnd(new \DateTimeImmutable('2023-12-30 23:59:59', new \DateTimeZone('+00:00'))), (new CronJobExecution('last-canceled-workflow-id'))->setLastEnd(new DateTimeImmutable('2023-12-30 23:59:59', new \DateTimeZone('+00:00'))),
true, true,
]; ];
yield [ yield [
(new CronJobExecution('last-canceled-workflow-id'))->setLastEnd(new \DateTimeImmutable('2023-12-31 00:00:01', new \DateTimeZone('+00:00'))), (new CronJobExecution('last-canceled-workflow-id'))->setLastEnd(new DateTimeImmutable('2023-12-31 00:00:01', new \DateTimeZone('+00:00'))),
false, false,
]; ];
} }
@ -104,7 +108,7 @@ class CancelStaleWorkflowCronJobTest extends KernelTestCase
false => $messageBus->method('dispatch'), false => $messageBus->method('dispatch'),
}; };
$methodDispatch->willReturnCallback(function (CancelStaleWorkflow $message) { $methodDispatch->willReturnCallback(function (CancelStaleWorkflowMessage $message) {
return new Envelope($message); return new Envelope($message);
}); });

View File

@ -16,6 +16,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Service\Workflow\CancelStaleWorkflow; use Chill\MainBundle\Service\Workflow\CancelStaleWorkflow;
use Chill\MainBundle\Service\Workflow\CancelStaleWorkflowHandler; use Chill\MainBundle\Service\Workflow\CancelStaleWorkflowHandler;
use Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -51,7 +52,7 @@ class CancelStaleWorkflowHandlerTest extends TestCase
$handler = new CancelStaleWorkflowHandler($workflowRepository, $registry, $em, $logger); $handler = new CancelStaleWorkflowHandler($workflowRepository, $registry, $em, $logger);
$handler(new CancelStaleWorkflow(1)); $handler(new CancelStaleWorkflowMessage(1));
} }
public function testInvokeWorkflowWithMultipleStepsAndValidTransition(): void public function testInvokeWorkflowWithMultipleStepsAndValidTransition(): void
@ -92,7 +93,7 @@ class CancelStaleWorkflowHandlerTest extends TestCase
$handler = new CancelStaleWorkflowHandler($workflowRepository, $registry, $em, $logger); $handler = new CancelStaleWorkflowHandler($workflowRepository, $registry, $em, $logger);
$handler(new CancelStaleWorkflow(1)); $handler(new CancelStaleWorkflowMessage(1));
} }
public function testInvokeWorkflowWithMultipleStepsAndNoValidTransition(): void public function testInvokeWorkflowWithMultipleStepsAndNoValidTransition(): void
@ -134,6 +135,6 @@ class CancelStaleWorkflowHandlerTest extends TestCase
$handler = new CancelStaleWorkflowHandler($workflowRepository, $registry, $em, $logger); $handler = new CancelStaleWorkflowHandler($workflowRepository, $registry, $em, $logger);
$handler(new CancelStaleWorkflow(1)); $handler(new CancelStaleWorkflowMessage(1));
} }
} }