From c582ede003cffdac2f6db1535887df4ab58929c1 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 22 Feb 2024 16:53:19 +0100 Subject: [PATCH] improve api --- docs/pages/projection.md | 4 +- src/Projection/Projection/NoErrorToRetry.php | 15 ++++ src/Projection/Projection/Projection.php | 29 ++++--- .../Projection/Store/DoctrineStore.php | 38 ++++---- .../Projectionist/DefaultProjectionist.php | 36 ++++---- ...rategy.php => ClockBasedRetryStrategy.php} | 41 +++------ .../RetryStrategy/NoRetryStrategy.php | 9 +- src/Projection/RetryStrategy/Retry.php | 31 ------- .../RetryStrategy/RetryStrategy.php | 6 +- .../RetryStrategy/UnexpectedError.php | 11 +++ .../Projectionist/ProjectionistTest.php | 54 +++++++----- .../Projection/Projection/ProjectionTest.php | 29 +++++-- .../DefaultProjectionistTest.php | 70 +++++---------- .../ClockBasedRetryStrategyTest.php | 69 +++++++++++++++ .../DefaultRetryStrategyTest.php | 87 ------------------- .../RetryStrategy/NoRetryStrategyTest.php | 40 +-------- 16 files changed, 249 insertions(+), 320 deletions(-) create mode 100644 src/Projection/Projection/NoErrorToRetry.php rename src/Projection/RetryStrategy/{DefaultRetryStrategy.php => ClockBasedRetryStrategy.php} (53%) delete mode 100644 src/Projection/RetryStrategy/Retry.php create mode 100644 src/Projection/RetryStrategy/UnexpectedError.php create mode 100644 tests/Unit/Projection/RetryStrategy/ClockBasedRetryStrategyTest.php delete mode 100644 tests/Unit/Projection/RetryStrategy/DefaultRetryStrategyTest.php diff --git a/docs/pages/projection.md b/docs/pages/projection.md index 56023afe2..10adde9b9 100644 --- a/docs/pages/projection.md +++ b/docs/pages/projection.md @@ -442,9 +442,9 @@ Our default strategy can be configured with the following parameters: * `maxAttempts` - The maximum number of attempts. ```php -use Patchlevel\EventSourcing\Projection\RetryStrategy\DefaultRetryStrategy; +use Patchlevel\EventSourcing\Projection\RetryStrategy\ClockBasedRetryStrategy; -$retryStrategy = new DefaultRetryStrategy( +$retryStrategy = new ClockBasedRetryStrategy( baseDelay: 5, delayFactor: 2, maxAttempts: 5, diff --git a/src/Projection/Projection/NoErrorToRetry.php b/src/Projection/Projection/NoErrorToRetry.php new file mode 100644 index 000000000..2ff10b192 --- /dev/null +++ b/src/Projection/Projection/NoErrorToRetry.php @@ -0,0 +1,15 @@ +status === ProjectionStatus::Error; } - public function updateRetry(Retry|null $retry): void + public function retryAttempt(): int { - $this->retry = $retry; - } - - public function retry(): Retry|null - { - return $this->retry; + return $this->retryAttempt; } public function doRetry(): void { if ($this->error === null) { - return; + throw new NoErrorToRetry(); } + $this->retryAttempt++; $this->status = $this->error->previousStatus; $this->error = null; } public function resetRetry(): void { - $this->retry = null; + $this->retryAttempt = 0; + } + + public function lastSavedAt(): DateTimeImmutable|null + { + return $this->lastSavedAt; + } + + public function updateLastSavedAt(DateTimeImmutable $lastSavedAt): void + { + $this->lastSavedAt = $lastSavedAt; } } diff --git a/src/Projection/Projection/Store/DoctrineStore.php b/src/Projection/Projection/Store/DoctrineStore.php index a20538133..7e437c474 100644 --- a/src/Projection/Projection/Store/DoctrineStore.php +++ b/src/Projection/Projection/Store/DoctrineStore.php @@ -14,14 +14,15 @@ use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; +use Patchlevel\EventSourcing\Clock\SystemClock; use Patchlevel\EventSourcing\Projection\Projection\Projection; use Patchlevel\EventSourcing\Projection\Projection\ProjectionCriteria; use Patchlevel\EventSourcing\Projection\Projection\ProjectionError; use Patchlevel\EventSourcing\Projection\Projection\ProjectionNotFound; use Patchlevel\EventSourcing\Projection\Projection\ProjectionStatus; use Patchlevel\EventSourcing\Projection\Projection\RunMode; -use Patchlevel\EventSourcing\Projection\RetryStrategy\Retry; use Patchlevel\EventSourcing\Schema\SchemaConfigurator; +use Psr\Clock\ClockInterface; use function array_map; use function assert; @@ -39,14 +40,15 @@ * error_message: string|null, * error_previous_status: string|null, * error_context: string|null, - * retry_attempt: int|null, - * retry_next: string|null, + * retry_attempt: int, + * last_saved_at: string, * } */ final class DoctrineStore implements LockableProjectionStore, SchemaConfigurator { public function __construct( private readonly Connection $connection, + private readonly ClockInterface $clock = new SystemClock(), private readonly string $projectionTable = 'projections', ) { } @@ -121,7 +123,8 @@ public function find(ProjectionCriteria|null $criteria = null): array public function add(Projection $projection): void { $projectionError = $projection->projectionError(); - $projectionRetry = $projection->retry(); + + $projection->updateLastSavedAt($this->clock->now()); $this->connection->insert( $this->projectionTable, @@ -134,11 +137,11 @@ public function add(Projection $projection): void 'error_message' => $projectionError?->errorMessage, 'error_previous_status' => $projectionError?->previousStatus?->value, 'error_context' => $projectionError?->errorContext !== null ? json_encode($projectionError->errorContext, JSON_THROW_ON_ERROR) : null, - 'retry_attempt' => $projectionRetry?->attempt, - 'retry_next' => $projectionRetry?->nextRetry, + 'retry_attempt' => $projection->retryAttempt(), + 'last_saved_at' => $projection->lastSavedAt(), ], [ - 'retry_next' => Types::DATETIME_IMMUTABLE, + 'last_saved_at' => Types::DATETIME_IMMUTABLE, ], ); } @@ -146,7 +149,8 @@ public function add(Projection $projection): void public function update(Projection $projection): void { $projectionError = $projection->projectionError(); - $projectionRetry = $projection->retry(); + + $projection->updateLastSavedAt($this->clock->now()); $effectedRows = $this->connection->update( $this->projectionTable, @@ -158,14 +162,14 @@ public function update(Projection $projection): void 'error_message' => $projectionError?->errorMessage, 'error_previous_status' => $projectionError?->previousStatus?->value, 'error_context' => $projectionError?->errorContext !== null ? json_encode($projectionError->errorContext, JSON_THROW_ON_ERROR) : null, - 'retry_attempt' => $projectionRetry?->attempt, - 'retry_next' => $projectionRetry?->nextRetry, + 'retry_attempt' => $projection->retryAttempt(), + 'last_saved_at' => $projection->lastSavedAt(), ], [ 'id' => $projection->id(), ], [ - 'retry_next' => Types::DATETIME_IMMUTABLE, + 'last_saved_at' => Types::DATETIME_IMMUTABLE, ], ); @@ -221,9 +225,9 @@ public function configureSchema(Schema $schema, Connection $connection): void $table->addColumn('error_context', Types::JSON) ->setNotnull(false); $table->addColumn('retry_attempt', Types::INTEGER) - ->setNotnull(false); - $table->addColumn('retry_next', Types::DATETIMETZ_IMMUTABLE) - ->setNotnull(false); + ->setNotnull(true); + $table->addColumn('last_saved_at', Types::DATETIMETZ_IMMUTABLE) + ->setNotnull(true); $table->setPrimaryKey(['id']); $table->addIndex(['group_name']); @@ -247,10 +251,8 @@ private function createProjection(array $row): Projection $row['error_previous_status'] !== null ? ProjectionStatus::from($row['error_previous_status']) : ProjectionStatus::New, $context, ) : null, - $row['retry_attempt'] !== null ? new Retry( - $row['retry_attempt'], - self::normalizeDateTime($row['retry_next'], $this->connection->getDatabasePlatform()), - ) : null, + $row['retry_attempt'], + self::normalizeDateTime($row['last_saved_at'], $this->connection->getDatabasePlatform()), ); } diff --git a/src/Projection/Projectionist/DefaultProjectionist.php b/src/Projection/Projectionist/DefaultProjectionist.php index 75ec4f2e8..c12c412e5 100644 --- a/src/Projection/Projectionist/DefaultProjectionist.php +++ b/src/Projection/Projectionist/DefaultProjectionist.php @@ -16,7 +16,7 @@ use Patchlevel\EventSourcing\Projection\Projection\RunMode; use Patchlevel\EventSourcing\Projection\Projection\Store\LockableProjectionStore; use Patchlevel\EventSourcing\Projection\Projection\Store\ProjectionStore; -use Patchlevel\EventSourcing\Projection\RetryStrategy\DefaultRetryStrategy; +use Patchlevel\EventSourcing\Projection\RetryStrategy\ClockBasedRetryStrategy; use Patchlevel\EventSourcing\Projection\RetryStrategy\RetryStrategy; use Patchlevel\EventSourcing\Store\Criteria; use Patchlevel\EventSourcing\Store\Store; @@ -39,7 +39,7 @@ public function __construct( private readonly Store $streamableMessageStore, private readonly ProjectionStore $projectionStore, private readonly iterable $projectors, - private readonly RetryStrategy $retryStrategy = new DefaultRetryStrategy(), + private readonly RetryStrategy $retryStrategy = new ClockBasedRetryStrategy(), private readonly ProjectorMetadataFactory $metadataFactory = new AttributeProjectorMetadataFactory(), private readonly LoggerInterface|null $logger = null, ) { @@ -604,25 +604,34 @@ private function handleRetryProjections(ProjectionistCriteria $criteria): void function (array $projections): void { /** @var Projection $projection */ foreach ($projections as $projection) { - $retry = $projection->retry(); + $error = $projection->projectionError(); - if (!$retry) { + if ($error === null) { continue; } - if (!$this->retryStrategy->shouldRetry($retry)) { + $retryable = in_array( + $error->previousStatus, + [ProjectionStatus::New, ProjectionStatus::Booting, ProjectionStatus::Active], + true, + ); + + if (!$retryable) { continue; } - $projection->doRetry(); + if (!$this->retryStrategy->shouldRetry($projection)) { + continue; + } + $projection->doRetry(); $this->projectionStore->update($projection); $this->logger?->info( sprintf( 'Projectionist: Retry projection "%s" (%d) and set back to %s.', $projection->id(), - $retry->attempt, + $projection->retryAttempt(), $projection->status()->value, ), ); @@ -855,20 +864,7 @@ private function findForUpdate(ProjectionCriteria $criteria, Closure $closure): private function handleError(Projection $projection, Throwable $throwable): void { - $retryable = in_array( - $projection->status(), - [ProjectionStatus::New, ProjectionStatus::Booting, ProjectionStatus::Active], - true, - ); - $projection->error($throwable); - - if ($retryable) { - $projection->updateRetry( - $this->retryStrategy->nextAttempt($projection->retry()), - ); - } - $this->projectionStore->update($projection); } } diff --git a/src/Projection/RetryStrategy/DefaultRetryStrategy.php b/src/Projection/RetryStrategy/ClockBasedRetryStrategy.php similarity index 53% rename from src/Projection/RetryStrategy/DefaultRetryStrategy.php rename to src/Projection/RetryStrategy/ClockBasedRetryStrategy.php index 28fe62f4c..cd4ea1ac1 100644 --- a/src/Projection/RetryStrategy/DefaultRetryStrategy.php +++ b/src/Projection/RetryStrategy/ClockBasedRetryStrategy.php @@ -6,13 +6,13 @@ use DateTimeImmutable; use Patchlevel\EventSourcing\Clock\SystemClock; +use Patchlevel\EventSourcing\Projection\Projection\Projection; use Psr\Clock\ClockInterface; -use RuntimeException; use function round; use function sprintf; -final class DefaultRetryStrategy implements RetryStrategy +final class ClockBasedRetryStrategy implements RetryStrategy { public const DEFAULT_BASE_DELAY = 5; public const DEFAULT_DELAY_FACTOR = 2; @@ -30,46 +30,29 @@ public function __construct( ) { } - public function nextAttempt(Retry|null $retry): Retry|null + public function shouldRetry(Projection $projection): bool { - if ($retry === null) { - return new Retry( - 1, - $this->calculateNextDate(1), - ); - } - - if ($retry->attempt() >= $this->maxAttempts) { - return null; - } - - return new Retry( - $retry->attempt() + 1, - $this->calculateNextDate($retry->attempt()), - ); - } - - public function shouldRetry(Retry|null $retry): bool - { - if ($retry === null) { + if ($projection->retryAttempt() >= $this->maxAttempts) { return false; } - $nextRetry = $retry->nextRetry(); + $lastSavedAt = $projection->lastSavedAt(); - if ($nextRetry === null) { + if ($lastSavedAt === null) { return false; } - return $nextRetry <= $this->clock->now(); + $nextRetryDate = $this->calculateNextRetryDate($lastSavedAt, $projection->retryAttempt()); + + return $nextRetryDate <= $this->clock->now(); } - private function calculateNextDate(int $attempt): DateTimeImmutable + private function calculateNextRetryDate(DateTimeImmutable $lastDate, int $attempt): DateTimeImmutable { - $nextDate = $this->clock->now()->modify(sprintf('+%d seconds', $this->calculateDelay($attempt))); + $nextDate = $lastDate->modify(sprintf('+%d seconds', $this->calculateDelay($attempt))); if ($nextDate === false) { - throw new RuntimeException('Could not calculate next date'); + throw new UnexpectedError('Could not calculate next date'); } return $nextDate; diff --git a/src/Projection/RetryStrategy/NoRetryStrategy.php b/src/Projection/RetryStrategy/NoRetryStrategy.php index 0f617c166..a9baea3ac 100644 --- a/src/Projection/RetryStrategy/NoRetryStrategy.php +++ b/src/Projection/RetryStrategy/NoRetryStrategy.php @@ -4,14 +4,11 @@ namespace Patchlevel\EventSourcing\Projection\RetryStrategy; +use Patchlevel\EventSourcing\Projection\Projection\Projection; + final class NoRetryStrategy implements RetryStrategy { - public function nextAttempt(Retry|null $retry): Retry|null - { - return null; - } - - public function shouldRetry(Retry|null $retry): bool + public function shouldRetry(Projection $projection): bool { return false; } diff --git a/src/Projection/RetryStrategy/Retry.php b/src/Projection/RetryStrategy/Retry.php deleted file mode 100644 index 1b2de99e2..000000000 --- a/src/Projection/RetryStrategy/Retry.php +++ /dev/null @@ -1,31 +0,0 @@ -attempt; - } - - public function nextRetry(): DateTimeImmutable|null - { - return $this->nextRetry; - } - - public function canRetry(): bool - { - return $this->nextRetry !== null; - } -} diff --git a/src/Projection/RetryStrategy/RetryStrategy.php b/src/Projection/RetryStrategy/RetryStrategy.php index 5f21f29d4..ff4a66f22 100644 --- a/src/Projection/RetryStrategy/RetryStrategy.php +++ b/src/Projection/RetryStrategy/RetryStrategy.php @@ -4,9 +4,9 @@ namespace Patchlevel\EventSourcing\Projection\RetryStrategy; +use Patchlevel\EventSourcing\Projection\Projection\Projection; + interface RetryStrategy { - public function nextAttempt(Retry|null $retry): Retry|null; - - public function shouldRetry(Retry|null $retry): bool; + public function shouldRetry(Projection $projection): bool; } diff --git a/src/Projection/RetryStrategy/UnexpectedError.php b/src/Projection/RetryStrategy/UnexpectedError.php new file mode 100644 index 000000000..da8d1a371 --- /dev/null +++ b/src/Projection/RetryStrategy/UnexpectedError.php @@ -0,0 +1,11 @@ +connection); + $clock = new FrozenClock(new DateTimeImmutable('2021-01-01T00:00:00')); + + $projectionStore = new DoctrineStore( + $this->connection, + $clock, + ); $manager = new DefaultRepositoryManager( new AggregateRootRegistry(['profile' => Profile::class]), @@ -80,7 +85,7 @@ public function testHappyPath(): void ); self::assertEquals( - [new Projection('profile_1')], + [new Projection('profile_1', lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'))], $projectionist->projections(), ); @@ -93,6 +98,7 @@ public function testHappyPath(): void Projection::DEFAULT_GROUP, RunMode::FromBeginning, ProjectionStatus::Active, + lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), ), ], $projectionist->projections(), @@ -111,6 +117,7 @@ public function testHappyPath(): void RunMode::FromBeginning, ProjectionStatus::Active, 1, + lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), ), ], $projectionist->projections(), @@ -132,6 +139,7 @@ public function testHappyPath(): void Projection::DEFAULT_GROUP, RunMode::FromBeginning, ProjectionStatus::New, + lastSavedAt: new DateTimeImmutable('2021-01-01T00:00:00'), ), ], $projectionist->projections(), @@ -144,13 +152,18 @@ public function testHappyPath(): void public function testErrorHandling(): void { + $clock = new FrozenClock(new DateTimeImmutable('2021-01-01T00:00:00')); + $store = new DoctrineDbalStore( $this->connection, DefaultEventSerializer::createFromPaths([__DIR__ . '/Events']), 'eventstore', ); - $projectionStore = new DoctrineStore($this->connection); + $projectionStore = new DoctrineStore( + $this->connection, + $clock, + ); $schemaDirector = new DoctrineSchemaDirector( $this->connection, @@ -169,16 +182,15 @@ public function testErrorHandling(): void ); $projector = new ErrorProducerProjector(); - $clock = new FrozenClock(new DateTimeImmutable('2021-01-01T00:00:00')); $projectionist = new DefaultProjectionist( $store, $projectionStore, [$projector], - new DefaultRetryStrategy( + new ClockBasedRetryStrategy( $clock, - DefaultRetryStrategy::DEFAULT_BASE_DELAY, - DefaultRetryStrategy::DEFAULT_DELAY_FACTOR, + ClockBasedRetryStrategy::DEFAULT_BASE_DELAY, + ClockBasedRetryStrategy::DEFAULT_DELAY_FACTOR, 2, ), ); @@ -189,7 +201,7 @@ public function testErrorHandling(): void self::assertEquals(ProjectionStatus::Active, $projection->status()); self::assertEquals(null, $projection->projectionError()); - self::assertEquals(null, $projection->retry()); + self::assertEquals(0, $projection->retryAttempt()); $repository = $manager->get(Profile::class); @@ -204,8 +216,7 @@ public function testErrorHandling(): void self::assertEquals(ProjectionStatus::Error, $projection->status()); self::assertEquals('subscribe error', $projection->projectionError()?->errorMessage); self::assertEquals(ProjectionStatus::Active, $projection->projectionError()?->previousStatus); - self::assertEquals(1, $projection->retry()?->attempt); - self::assertEquals(new DateTimeImmutable('2021-01-01T00:00:10'), $projection->retry()?->nextRetry); + self::assertEquals(0, $projection->retryAttempt()); $projectionist->run(); @@ -214,10 +225,9 @@ public function testErrorHandling(): void self::assertEquals(ProjectionStatus::Error, $projection->status()); self::assertEquals('subscribe error', $projection->projectionError()?->errorMessage); self::assertEquals(ProjectionStatus::Active, $projection->projectionError()?->previousStatus); - self::assertEquals(1, $projection->retry()?->attempt); - self::assertEquals(new DateTimeImmutable('2021-01-01T00:00:10'), $projection->retry()?->nextRetry); + self::assertEquals(0, $projection->retryAttempt()); - $clock->sleep(10); + $clock->sleep(5); $projectionist->run(); @@ -226,10 +236,9 @@ public function testErrorHandling(): void self::assertEquals(ProjectionStatus::Error, $projection->status()); self::assertEquals('subscribe error', $projection->projectionError()?->errorMessage); self::assertEquals(ProjectionStatus::Active, $projection->projectionError()?->previousStatus); - self::assertEquals(2, $projection->retry()?->attempt); - self::assertEquals(new DateTimeImmutable('2021-01-01T00:00:20'), $projection->retry()?->nextRetry); + self::assertEquals(1, $projection->retryAttempt()); - $clock->sleep(20); + $clock->sleep(10); $projectionist->run(); @@ -238,7 +247,7 @@ public function testErrorHandling(): void self::assertEquals(ProjectionStatus::Error, $projection->status()); self::assertEquals('subscribe error', $projection->projectionError()?->errorMessage); self::assertEquals(ProjectionStatus::Active, $projection->projectionError()?->previousStatus); - self::assertEquals(null, $projection->retry()); + self::assertEquals(2, $projection->retryAttempt()); $projectionist->reactivate(new ProjectionistCriteria( ids: ['error_producer'], @@ -248,7 +257,7 @@ public function testErrorHandling(): void self::assertEquals(ProjectionStatus::Active, $projection->status()); self::assertEquals(null, $projection->projectionError()); - self::assertEquals(null, $projection->retry()); + self::assertEquals(0, $projection->retryAttempt()); $projectionist->run(); @@ -257,10 +266,9 @@ public function testErrorHandling(): void self::assertEquals(ProjectionStatus::Error, $projection->status()); self::assertEquals('subscribe error', $projection->projectionError()?->errorMessage); self::assertEquals(ProjectionStatus::Active, $projection->projectionError()?->previousStatus); - self::assertEquals(1, $projection->retry()?->attempt); - self::assertEquals(new DateTimeImmutable('2021-01-01T00:00:40'), $projection->retry()?->nextRetry); + self::assertEquals(0, $projection->retryAttempt()); - $clock->sleep(10); + $clock->sleep(5); $projector->subscribeError = false; $projectionist->run(); @@ -269,7 +277,7 @@ public function testErrorHandling(): void self::assertEquals(ProjectionStatus::Active, $projection->status()); self::assertEquals(null, $projection->projectionError()); - self::assertEquals(null, $projection->retry()); + self::assertEquals(0, $projection->retryAttempt()); } /** @param list $projections */ diff --git a/tests/Unit/Projection/Projection/ProjectionTest.php b/tests/Unit/Projection/Projection/ProjectionTest.php index c8be5f2c5..6c23af7b6 100644 --- a/tests/Unit/Projection/Projection/ProjectionTest.php +++ b/tests/Unit/Projection/Projection/ProjectionTest.php @@ -4,11 +4,12 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Projection\Projection; +use Patchlevel\EventSourcing\Projection\Projection\NoErrorToRetry; use Patchlevel\EventSourcing\Projection\Projection\Projection; use Patchlevel\EventSourcing\Projection\Projection\ProjectionError; use Patchlevel\EventSourcing\Projection\Projection\ProjectionStatus; +use Patchlevel\EventSourcing\Projection\Projection\RunMode; use Patchlevel\EventSourcing\Projection\Projection\ThrowableToErrorContextTransformer; -use Patchlevel\EventSourcing\Projection\RetryStrategy\Retry; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -115,22 +116,34 @@ public function testChangePosition(): void self::assertEquals(10, $projection->position()); } - public function testRetry(): void + public function testCanNotRetry(): void { + $this->expectException(NoErrorToRetry::class); + $projection = new Projection( 'test', ); - self::assertEquals(null, $projection->retry()); - - $retry = new Retry(1, null); + $projection->doRetry(); + } - $projection->updateRetry($retry); + public function testDoRetry(): void + { + $projection = new Projection( + 'test', + 'default', + RunMode::FromBeginning, + ProjectionStatus::Error, + 0, + new ProjectionError('test', ProjectionStatus::New, []), + ); - self::assertEquals($retry, $projection->retry()); + self::assertEquals(null, $projection->retryAttempt()); + $projection->doRetry(); + self::assertEquals(1, $projection->retryAttempt()); $projection->resetRetry(); - self::assertEquals(null, $projection->retry()); + self::assertEquals(null, $projection->retryAttempt()); } } diff --git a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php b/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php index 67c0f6490..e51a63b4c 100644 --- a/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php +++ b/tests/Unit/Projection/Projectionist/DefaultProjectionistTest.php @@ -5,7 +5,6 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Projection\Projectionist; use Closure; -use DateTimeImmutable; use Generator; use Patchlevel\EventSourcing\Attribute\Projector as ProjectionAttribute; use Patchlevel\EventSourcing\Attribute\Setup; @@ -22,7 +21,6 @@ use Patchlevel\EventSourcing\Projection\Projection\ThrowableToErrorContextTransformer; use Patchlevel\EventSourcing\Projection\Projectionist\DefaultProjectionist; use Patchlevel\EventSourcing\Projection\Projectionist\ProjectionistCriteria; -use Patchlevel\EventSourcing\Projection\RetryStrategy\Retry; use Patchlevel\EventSourcing\Projection\RetryStrategy\RetryStrategy; use Patchlevel\EventSourcing\Store\ArrayStream; use Patchlevel\EventSourcing\Store\Criteria; @@ -397,15 +395,10 @@ public function create(): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->shouldNotBeCalled(); - $retry = new Retry(1, new DateTimeImmutable()); - $retryStrategy = $this->prophesize(RetryStrategy::class); - $retryStrategy->nextAttempt(null)->willReturn($retry)->shouldBeCalledOnce(); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, [$projector], - $retryStrategy->reveal(), ); $projectionist->boot(); @@ -423,7 +416,6 @@ public function create(): void ProjectionStatus::New, ThrowableToErrorContextTransformer::transform($projector->exception), ), - $retry, ), ], $projectionStore->updatedProjections, @@ -830,15 +822,10 @@ public function handle(Message $message): void $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - $retry = new Retry(1, new DateTimeImmutable()); - $retryStrategy = $this->prophesize(RetryStrategy::class); - $retryStrategy->nextAttempt(null)->willReturn($retry)->shouldBeCalledOnce(); - $projectionist = new DefaultProjectionist( $streamableStore->reveal(), $projectionStore, [$projector], - $retryStrategy->reveal(), ); $projectionist->run(); @@ -856,7 +843,6 @@ public function handle(Message $message): void ProjectionStatus::Active, ThrowableToErrorContextTransformer::transform($projector->exception), ), - $retry, ), ], $projectionStore->updatedProjections, @@ -1489,29 +1475,24 @@ public function subscribe(): void } }; - $retry1 = new Retry(1, new DateTimeImmutable()); - $retry2 = new Retry(2, new DateTimeImmutable()); - $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); $streamableStore = $this->prophesize(Store::class); $streamableStore->load($this->criteria())->willReturn(new ArrayStream([$message]))->shouldBeCalledOnce(); - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Error, - 0, - new ProjectionError('ERROR', ProjectionStatus::Active), - $retry1, - ), - ]); + $projection = new Projection( + $projectionId, + Projection::DEFAULT_GROUP, + RunMode::FromBeginning, + ProjectionStatus::Error, + 0, + new ProjectionError('ERROR', ProjectionStatus::Active), + ); + + $projectionStore = new DummyStore([$projection]); $retryStrategy = $this->prophesize(RetryStrategy::class); - $retryStrategy->shouldRetry($retry1)->willReturn(true); - $retryStrategy->nextAttempt($retry1)->willReturn($retry2); + $retryStrategy->shouldRetry($projection)->willReturn(true); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), @@ -1533,13 +1514,13 @@ public function subscribe(): void ProjectionStatus::Active, 0, null, - $retry1, + 1, )); self::assertEquals(ProjectionStatus::Error, $update2->status()); self::assertEquals(ProjectionStatus::Active, $update2->projectionError()?->previousStatus); self::assertEquals('ERROR2', $update2->projectionError()?->errorMessage); - self::assertEquals($retry2, $update2->retry()); + self::assertEquals(1, $update2->retryAttempt()); } public function testShouldNotRetry(): void @@ -1549,24 +1530,21 @@ public function testShouldNotRetry(): void class { }; - $retry = new Retry(1, new DateTimeImmutable()); - $streamableStore = $this->prophesize(Store::class); - $projectionStore = new DummyStore([ - new Projection( - $projectionId, - Projection::DEFAULT_GROUP, - RunMode::FromBeginning, - ProjectionStatus::Error, - 0, - new ProjectionError('ERROR', ProjectionStatus::Active), - $retry, - ), - ]); + $projection = new Projection( + $projectionId, + Projection::DEFAULT_GROUP, + RunMode::FromBeginning, + ProjectionStatus::Error, + 0, + new ProjectionError('ERROR', ProjectionStatus::Active), + ); + + $projectionStore = new DummyStore([$projection]); $retryStrategy = $this->prophesize(RetryStrategy::class); - $retryStrategy->shouldRetry($retry)->willReturn(false); + $retryStrategy->shouldRetry($projection)->willReturn(false); $projectionist = new DefaultProjectionist( $streamableStore->reveal(), diff --git a/tests/Unit/Projection/RetryStrategy/ClockBasedRetryStrategyTest.php b/tests/Unit/Projection/RetryStrategy/ClockBasedRetryStrategyTest.php new file mode 100644 index 000000000..420486dc4 --- /dev/null +++ b/tests/Unit/Projection/RetryStrategy/ClockBasedRetryStrategyTest.php @@ -0,0 +1,69 @@ +clock = new FrozenClock(new DateTimeImmutable('2021-01-01T00:00:00+00:00')); + $this->strategy = new ClockBasedRetryStrategy($this->clock); + } + + /** @param positive-int $seconds */ + #[DataProvider('attemptProvider')] + public function testShouldRetry(int $attempt, int $seconds, bool $expected): void + { + $projection = new Projection( + 'test', + 'default', + RunMode::FromBeginning, + ProjectionStatus::Error, + 0, + null, + $attempt, + $this->clock->now(), + ); + + $this->clock->sleep($seconds); + + self::assertEquals( + $expected, + $this->strategy->shouldRetry($projection), + ); + } + + public static function attemptProvider(): Generator + { + yield [0, 0, false]; + yield [0, 5, true]; + yield [1, 5, false]; + yield [1, 10, true]; + yield [2, 10, false]; + yield [2, 20, true]; + yield [3, 20, false]; + yield [3, 40, true]; + yield [4, 40, false]; + yield [4, 80, true]; + yield [5, 80, false]; + yield [5, 160, false]; + yield [5, 320, false]; + } +} diff --git a/tests/Unit/Projection/RetryStrategy/DefaultRetryStrategyTest.php b/tests/Unit/Projection/RetryStrategy/DefaultRetryStrategyTest.php deleted file mode 100644 index fb6564166..000000000 --- a/tests/Unit/Projection/RetryStrategy/DefaultRetryStrategyTest.php +++ /dev/null @@ -1,87 +0,0 @@ -clock = new FrozenClock(new DateTimeImmutable('2021-01-01T00:00:00+00:00')); - $this->strategy = new DefaultRetryStrategy($this->clock); - } - - public function testShouldRetryWithNull(): void - { - self::assertFalse($this->strategy->shouldRetry(null)); - } - - public function testShouldRetryWithoutTime(): void - { - self::assertFalse($this->strategy->shouldRetry(new Retry(1, null))); - } - - public function testShouldRetryWithTime(): void - { - self::assertFalse($this->strategy->shouldRetry(new Retry(1, new DateTimeImmutable()))); - } - - public function testNextAttemptWithNull(): void - { - $expected = new Retry(1, new DateTimeImmutable('2021-01-01T00:00:10+00:00')); - - self::assertEquals($expected, $this->strategy->nextAttempt(null)); - } - - #[DataProvider('attemptProvider')] - public function testNextAttempt(int $attempt, string $dateString): void - { - $expected = new Retry($attempt, new DateTimeImmutable($dateString)); - - self::assertEquals( - $expected, - $this->strategy->nextAttempt( - new Retry( - $attempt - 1, - null, - ), - ), - ); - } - - public static function attemptProvider(): Generator - { - yield 'first attempt' => [1, '2021-01-01T00:00:5+00:00']; - yield 'second attempt' => [2, '2021-01-01T00:00:10+00:00']; - yield 'third attempt' => [3, '2021-01-01T00:00:20+00:00']; - yield 'fourth attempt' => [4, '2021-01-01T00:00:40+00:00']; - yield 'fifth attempt' => [5, '2021-01-01T00:01:20+00:00']; - } - - public function testMaxAttempt(): void - { - self::assertNull( - $this->strategy->nextAttempt( - new Retry( - 6, - null, - ), - ), - ); - } -} diff --git a/tests/Unit/Projection/RetryStrategy/NoRetryStrategyTest.php b/tests/Unit/Projection/RetryStrategy/NoRetryStrategyTest.php index c669cbdfa..dc76fabe8 100644 --- a/tests/Unit/Projection/RetryStrategy/NoRetryStrategyTest.php +++ b/tests/Unit/Projection/RetryStrategy/NoRetryStrategyTest.php @@ -4,51 +4,19 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Projection\RetryStrategy; -use DateTimeImmutable; +use Patchlevel\EventSourcing\Projection\Projection\Projection; use Patchlevel\EventSourcing\Projection\RetryStrategy\NoRetryStrategy; -use Patchlevel\EventSourcing\Projection\RetryStrategy\Retry; use PHPUnit\Framework\TestCase; /** @covers \Patchlevel\EventSourcing\Projection\RetryStrategy\NoRetryStrategy */ final class NoRetryStrategyTest extends TestCase { - public function testShouldRetryWithNull(): void + public function testNull(): void { $strategy = new NoRetryStrategy(); - self::assertFalse($strategy->shouldRetry(null)); - } - - public function testShouldRetryWithoutTime(): void - { - $strategy = new NoRetryStrategy(); - self::assertFalse($strategy->shouldRetry(new Retry(1, null))); - } - - public function testShouldRetryWithTime(): void - { - $strategy = new NoRetryStrategy(); - self::assertFalse($strategy->shouldRetry(new Retry(1, new DateTimeImmutable()))); - } - - public function testNextAttemptWithNull(): void - { - $strategy = new NoRetryStrategy(); - self::assertNull($strategy->nextAttempt(null)); - } - - public function testNextAttemptWithoutTime(): void - { - $strategy = new NoRetryStrategy(); - self::assertNull($strategy->nextAttempt( - new Retry(1, null), - )); - } - public function testNextAttemptWithTime(): void - { - $strategy = new NoRetryStrategy(); - self::assertNull($strategy->nextAttempt( - new Retry(1, new DateTimeImmutable()), + self::assertFalse($strategy->shouldRetry( + new Projection('test'), )); } }