Skip to content

Commit

Permalink
Command bus cleaner
Browse files Browse the repository at this point in the history
  • Loading branch information
sbooker committed Jun 30, 2024
1 parent a468a51 commit dc85d59
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 6 deletions.
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@
},
"require-dev": {
"doctrine/cache": "^1.12",
"doctrine/orm": "^2.9 || ^3.0",
"doctrine/dbal": "^3.8 || ^4.0",
"doctrine/orm": "^2.9 || ^3.0",
"phpunit/phpunit": "^9.0",
"psr/container": "1.0.0",
"ramsey/uuid-doctrine": "^1.6",
"sbooker/enumerable-doctrine": "^1.1",
"sbooker/doctrine-transaction-handler": "^2.2"
"sbooker/console": "^1.1",
"sbooker/doctrine-transaction-handler": "^2.2",
"sbooker/enumerable-doctrine": "^1.1"
},
"suggest": {
"doctrine/orm": "If you want use DB persistence with Doctrine",
Expand Down
9 changes: 9 additions & 0 deletions src/CleanStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Sbooker\CommandBus;
interface CleanStorage
{
public function cleanSuccessCommands(\DateTimeImmutable $before): void;

public function cleanFailedCommands(\DateTimeImmutable $before): void;
}
5 changes: 4 additions & 1 deletion src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

use Ramsey\Uuid\UuidInterface;

/* final */ class Command
/**
* @final
*/
class Command
{
private UuidInterface $id;

Expand Down
8 changes: 8 additions & 0 deletions src/CommandBusCleaner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Sbooker\CommandBus;

interface CommandBusCleaner
{
public function clean(?\DateTimeImmutable $successBefore = null, ?\DateTimeImmutable $failedBefore = null): void;
}
39 changes: 39 additions & 0 deletions src/Infrastructure/Console/Clean.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Sbooker\CommandBus\Infrastructure\Console;

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

final class Clean extends Command
{
private CommandBusCleaner $cleaner;

public function __construct(CommandBusCleaner $cleaner)
{
parent::__construct();
$this->cleaner = $cleaner;
}

protected function configure()
{
$this->setDescription('Clean used commands in bus.');
$this->addArgument('success', InputArgument::OPTIONAL, "DateTime <https://www.php.net/manual/en/datetime.formats.php> to which all SUCCESS commands will be deleted");
$this->addArgument('failed', InputArgument::OPTIONAL, "DateTime <https://www.php.net/manual/en/datetime.formats.php> to which all FAILED commands will be deleted");
}

protected function doExecute(InputInterface $input, OutputInterface $output): void
{
$this->cleaner->clean($this->resolveArgument($input, 'success'), $this->resolveArgument($input, 'failed'));
}

private function resolveArgument(InputInterface $input, string $name): ?\DateTimeImmutable
{
$argument = $input->getArgument($name);

return (null === $argument) ? null : new \DateTimeImmutable($argument);
}
}
40 changes: 39 additions & 1 deletion src/Infrastructure/Persistence/DoctrineRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Persistence\Mapping\MappingException;
use Ramsey\Uuid\UuidInterface;
use Sbooker\CommandBus\CleanStorage;
use Sbooker\CommandBus\Command;
use Sbooker\CommandBus\ReadStorage;
use Sbooker\CommandBus\Status;
use Sbooker\CommandBus\WriteStorage;

final class DoctrineRepository extends EntityRepository implements WriteStorage, ReadStorage
final class DoctrineRepository extends EntityRepository implements WriteStorage, ReadStorage, CleanStorage
{
public function get(UuidInterface $id): ?Command
{
Expand Down Expand Up @@ -147,4 +148,41 @@ private function getConnection(): Connection
{
return $this->getEntityManager()->getConnection();
}

/**
* @throws DBALException
* @throws MappingException
* @throws \ReflectionException
*/
public function cleanSuccessCommands(\DateTimeImmutable $before): void
{
$this->clean(Status::success(), $before);
}

/**
* @throws DBALException
* @throws MappingException
* @throws \ReflectionException
*/
public function cleanFailedCommands(\DateTimeImmutable $before): void
{
$this->clean(Status::fail(), $before);
}

/**
* @throws DBALException
* @throws MappingException
* @throws \ReflectionException
*/
private function clean(Status $status, \DateTimeImmutable $before): void
{
$qb = $this->getConnection()->createQueryBuilder();

$qb->delete($this->getTableName(), 'c')
->andWhere('c.status = :status')
->andWhere('c.next_attempt_at < :before')
->setParameter('status', $status->getRawValue())
->setParameter('before', $before->format($this->getPlatform()->getDateTimeTzFormatString()))
->executeStatement();
}
}
38 changes: 38 additions & 0 deletions src/PersistentCommandBusCleaner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Sbooker\CommandBus;

final class PersistentCommandBusCleaner implements CommandBusCleaner
{
private CleanStorage $storage;
private ?\DateTimeImmutable $successBefore;
private ?\DateTimeImmutable $failedBefore;

public function __construct(CleanStorage $storage, ?\DateTimeImmutable $successBefore, ?\DateTimeImmutable $failedBefore)
{
$this->storage = $storage;
$this->successBefore = $successBefore;
$this->failedBefore = $failedBefore;
}

public function clean(?\DateTimeImmutable $successBefore = null, ?\DateTimeImmutable $failedBefore = null): void
{
$successBefore = $this->selectFrom($successBefore, $this->successBefore);
if (null !== $successBefore) {
$this->storage->cleanSuccessCommands($successBefore);
}
$failedBefore = $this->selectFrom($failedBefore, $this->failedBefore);
if (null !== $failedBefore) {
$this->storage->cleanFailedCommands($failedBefore);
}
}

private function selectFrom(?\DateTimeImmutable $first, ?\DateTimeImmutable $second):?\DateTimeImmutable
{
if (null !== $first) {
return $first;
}

return $second;
}
}
31 changes: 31 additions & 0 deletions tests/Infrastructure/Persistence/DoctrineRepositoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Ramsey\Uuid\UuidInterface;
use Sbooker\CommandBus\AttemptCounter;
use Sbooker\CommandBus\Command;
use Sbooker\CommandBus\Status;
use Sbooker\CommandBus\Workflow;

class DoctrineRepositoryTest extends PersistenceTestCase
Expand Down Expand Up @@ -66,6 +67,7 @@ public function testGetFirstToProcessAndLock(string $db): void

$expectedCommandName = 'command.name';
$otherCommandName = 'other.command.name';

$expectedCommand = $this->createCommand(Uuid::uuid4(), $expectedCommandName, '-10seconds');
$secondCommand = $this->createCommand(Uuid::uuid4(), $otherCommandName, '-5seconds');
$this->makeFixtures($expectedCommand);
Expand All @@ -81,6 +83,35 @@ public function testGetFirstToProcessAndLock(string $db): void
$this->tearDownDbDeps($em);
}


/**
* @dataProvider dbs
*/
public function testCleanSuccess(string $db)
{
$em = $this->setUpDbDeps($db);

$repository = $this->getRepository($em);
$commandId = Uuid::uuid4();
$otherCommandId = Uuid::uuid4();
$commandName = 'command.name';
$command = $this->createCommandWithStatus($commandId, $commandName, Status::success(),'-2days');
$otherCommand = $this->createCommandWithStatus($otherCommandId, $commandName, Status::fail(), '-5days');
$this->makeFixtures($command);
$this->makeFixtures($otherCommand);

$repository->cleanFailedCommands(new \DateTimeImmutable('-3days'));

$this->assertNotNull($repository->get($commandId));
$this->assertNull($repository->get($otherCommandId));

$em->clear();

$repository->cleanSuccessCommands(new \DateTimeImmutable('-1day'));

$this->assertNull($repository->get($commandId));
}

private function assertCommandEquals(Command $expected, Command $given): void
{
$this->assertUuidEquals(
Expand Down
16 changes: 15 additions & 1 deletion tests/Infrastructure/Persistence/PersistenceTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\SchemaTool;
use Ramsey\Uuid\UuidInterface;
use Sbooker\CommandBus\AttemptCounter;
use Sbooker\CommandBus\Command;
use Sbooker\CommandBus\Infrastructure\Persistence\DoctrineRepository;
use Sbooker\CommandBus\NormalizedCommand;
use Sbooker\CommandBus\Normalizer;
use Sbooker\CommandBus\Status;
use Sbooker\CommandBus\Tests\TestCase;
use Sbooker\TransactionManager\DoctrineTransactionHandler;
use Sbooker\TransactionManager\TransactionManager;
use Sbooker\CommandBus\Tests\TestCase;

abstract class PersistenceTestCase extends TestCase
{
Expand Down Expand Up @@ -51,6 +53,18 @@ final protected function createCommand(UuidInterface $commandId, string $command
return $command;
}

final protected function createCommandWithStatus(UuidInterface $commandId, string $commandName, Status $status, string $nextAttemptAt = 'now'): Command
{
$command = new Command($commandId, new \stdClass(), $this->createNormalizer($commandName));
/** @var AttemptCounter $attemptCounter */
$attemptCounter = $this->getPrivatePropertyValue($command, 'attemptCounter');
$this->openProperty($attemptCounter, 'nextAttemptAt')->setValue($attemptCounter, new \DateTimeImmutable($nextAttemptAt));
$workflow = $this->getPrivatePropertyValue( $command,'workflow');
$this->openProperty($workflow, 'status')->setValue($workflow, $status);

return $command;
}

final protected function makeFixtures(object ... $objects): void
{
$this->getTransactionManager()->transactional(function () use ($objects) {
Expand Down
88 changes: 88 additions & 0 deletions tests/PersistentCommandBusCleanerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace Sbooker\CommandBus\Tests;

use Sbooker\CommandBus\CleanStorage;
use Sbooker\CommandBus\CommandBusCleaner;
use Sbooker\CommandBus\PersistentCommandBusCleaner;

final class PersistentCommandBusCleanerTest extends TestCase
{
/**
* @dataProvider examples
*/
public function test(?string $inputSuccess, ?string $configuredSuccess, ?string $inputFail, ?string $configuredFail, ?string $expectedSuccess, ?string $expectedFail)
{
$cleaner = $this->createCleaner(
$this->createStorage($expectedSuccess, $expectedFail),
$configuredSuccess,
$configuredFail
);

$cleaner->clean($this->makeDateTime($inputSuccess), $this->makeDateTime($inputFail));
}

public static function examples(): array
{
return [
'all empty' => [ null, null, null, null, null, null, ],
'input success only' => [ '2024-01-01 00:00:00', null, null, null, '2024-01-01 00:00:00', null ],
'configured success only' => [ null, '2024-01-01 00:00:00', null, null, '2024-01-01 00:00:00', null ],
'both success only' => [ '2024-01-01 00:00:00', '2023-01-01 00:00:00', null, null, '2024-01-01 00:00:00', null ],
'input failed only' => [ null, null, '2024-01-01 00:00:00', null, null, '2024-01-01 00:00:00' ],
'both input' => [ '2024-01-01 00:00:00', null, '2023-01-01 00:00:00', null, '2024-01-01 00:00:00', '2023-01-01 00:00:00' ],
'configured success input failed' =>[ null, '2024-01-01 00:00:00', '2023-01-01 00:00:00', null, '2024-01-01 00:00:00', '2023-01-01 00:00:00' ],
'both success only input failed' => [ '2024-01-01 00:00:00', '2023-01-01 00:00:00', '2022-01-01 00:00:00', null, '2024-01-01 00:00:00', '2022-01-01 00:00:00', ],
'configured failed only' => [ null, null, null, '2024-01-01 00:00:00', null, '2024-01-01 00:00:00' ],
'input success configured failed' => [ '2024-01-01 00:00:00', null, null, '2021-01-01 00:00:00', '2024-01-01 00:00:00', '2021-01-01 00:00:00' ],
'configured success configured failed' => [ null, '2024-01-01 00:00:00', null, '2021-01-01 00:00:00', '2024-01-01 00:00:00', '2021-01-01 00:00:00' ],
'both success configured failed' => [ '2024-01-01 00:00:00', '2023-01-01 00:00:00', null, '2021-01-01 00:00:00', '2024-01-01 00:00:00', '2021-01-01 00:00:00' ],
'both failed only' => [ null, null, '2024-01-01 00:00:00', '2023-01-01 00:00:00', null, '2024-01-01 00:00:00', ],
'input success both failed' => [ '2025-01-01 00:00:00', null, '2024-01-01 00:00:00', '2023-01-01 00:00:00', '2025-01-01 00:00:00', '2024-01-01 00:00:00', ],
'configured success both failed' => [ null, '2025-01-01 00:00:00', '2024-01-01 00:00:00', '2023-01-01 00:00:00', '2025-01-01 00:00:00', '2024-01-01 00:00:00', ],
'both success & failed' => [ '2026-01-01 00:00:00', '2025-01-01 00:00:00', '2024-01-01 00:00:00', '2023-01-01 00:00:00', '2026-01-01 00:00:00', '2024-01-01 00:00:00', ],
];
}

private function createCleaner(CleanStorage $storage, ?string $configuredSuccess, ?string $configuredFail): CommandBusCleaner
{
return new PersistentCommandBusCleaner(
$storage,
$this->makeDateTime($configuredSuccess),
$this->makeDateTime($configuredFail),
);
}

private function createStorage(?string $expectedSuccess, ?string $expectedFail): CleanStorage
{
$mock = $this->createMock(CleanStorage::class);
$mock
->expects($this->exactly($this->resolveCount($expectedSuccess)))
->method('cleanSuccessCommands')
->with($this->makeDateTime($expectedSuccess));
$mock
->expects($this->exactly($this->resolveCount($expectedFail)))
->method('cleanFailedCommands')
->with($this->makeDateTime($expectedFail));

return $mock;
}

private function makeDateTime(?string $dateTime): ?\DateTimeImmutable
{
if (null === $dateTime) {
return null;
}

return new \DateTimeImmutable($dateTime);
}

private function resolveCount(?string $dateTime): int
{
if (null === $dateTime) {
return 0;
}

return 1;
}
}

0 comments on commit dc85d59

Please sign in to comment.