diff --git a/docs/aggregate.md b/docs/aggregate.md index e93e2697c..9e9371bc7 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -944,6 +944,11 @@ use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegist $aggregateRegistry = (new AttributeAggregateRootRegistryFactory())->create([/* paths... */]); ``` +:::tip +Scanning the paths on every request costs time. In production you can wrap the factory +in a [metadata cache](metadata-cache.md). +::: + ## Learn more * [How to create own aggregate id](aggregate-id.md) @@ -951,3 +956,4 @@ $aggregateRegistry = (new AttributeAggregateRootRegistryFactory())->create([/* p * [How to snapshot aggregates](snapshots.md) * [How to create Projections](subscription.md) * [How to split streams](split-stream.md) +* [How to cache metadata](metadata-cache.md) diff --git a/docs/cli.md b/docs/cli.md index 832f4c671..43ba954c7 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -7,6 +7,8 @@ You can: * Create and delete `databases` * Create, update and delete `schemas` * Manage `subscriptions` +* Inspect your `aggregates`, `events` and `subscribers` +* Migrate events from one `store` to another ## Database commands @@ -53,6 +55,86 @@ The inspector is a tool to inspect the event streams. * ShowAggregateCommand: `event-sourcing:show-aggregate` * WatchCommand: `event-sourcing:watch` +## Debug command + +The debug command prints everything the library knows about your application: +all registered aggregates, all registered events and all subscribers with their subscribe methods. +It is the fastest way to check if a class was picked up by the attribute scanning. + +* DebugCommand: `event-sourcing:debug` (alias `debug:event-sourcing`) + +```php +use Patchlevel\EventSourcing\Console\Command\DebugCommand; +use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; +use Patchlevel\EventSourcing\Metadata\Event\EventRegistry; +use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberAccessorRepository; +use Symfony\Component\Console\Application; + +/** + * @var Application $cli + * @var AggregateRootRegistry $aggregateRootRegistry + * @var EventRegistry $eventRegistry + * @var SubscriberAccessorRepository $subscriberAccessorRepository + */ +$cli->add( + new DebugCommand( + $aggregateRootRegistry, + $eventRegistry, + $subscriberAccessorRepository, + ), +); +``` +:::note +The subscriber repository is optional. If you don't pass it, the subscriber section is skipped. +::: + +## Store migration command + +The store migration command copies all events from one store into another one. +You need it when you switch the store implementation, +for example from the `DoctrineDbalStore` to the [StreamDoctrineDbalStore](store.md#streamdoctrinedbalstore). + +* StoreMigrateCommand: `event-sourcing:store:migrate` + +```php +use Patchlevel\EventSourcing\Console\Command\StoreMigrateCommand; +use Patchlevel\EventSourcing\Message\Translator\AggregateToStreamHeaderTranslator; +use Patchlevel\EventSourcing\Store\Store; +use Symfony\Component\Console\Application; + +/** + * @var Application $cli + * @var Store $oldStore + * @var Store $newStore + */ +$cli->add( + new StoreMigrateCommand( + $oldStore, + $newStore, + [new AggregateToStreamHeaderTranslator()], + ), +); +``` +The third constructor argument is a list of [translators](message.md#translator) +that are applied to every message before it is written into the new store. +The `AggregateToStreamHeaderTranslator` converts the `AggregateHeader` into the stream based headers +and is what you need for a migration to the `StreamDoctrineDbalStore`. + +Events are written in batches. You can control the batch size with the `buffer` option: + +```bash +bin/console event-sourcing:store:migrate --buffer=5000 +``` +:::danger +The command writes into the target store, it does not clean it up first. +Make sure the target store is empty and create a backup before you run the migration. +::: + +:::note +The schema of the new store has to exist before you run the command. +You can create it with the [schema commands](#schema-commands). +::: + ## CLI example A cli php file can look like this: diff --git a/docs/events.md b/docs/events.md index 6979adbeb..49784c9fa 100644 --- a/docs/events.md +++ b/docs/events.md @@ -82,6 +82,68 @@ The serializer needs the path information where the event classes are located so that it can instantiate the correct classes. Internally, an EventRegistry is used, which will be described later. +## Encoder + +The serializer turns an event into an array first and then encodes that array into a string. +The encoding is done by an `Encoder`. By default the `JsonEncoder` is used, which encodes the payload as JSON. + +If you want to change how the payload is encoded, you can pass your own encoder to the serializer. + +```php +use Patchlevel\EventSourcing\Metadata\Event\AttributeEventRegistryFactory; +use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; +use Patchlevel\EventSourcing\Serializer\Encoder\JsonEncoder; + +$serializer = new DefaultEventSerializer( + (new AttributeEventRegistryFactory())->create(['src/Domain']), + encoder: new JsonEncoder(), +); +``` +The `Encoder` interface has two methods: + +```php +use Patchlevel\EventSourcing\Serializer\Encoder\Encoder; + +final class MyEncoder implements Encoder +{ + /** + * @param array $data + * @param array $options + */ + public function encode(array $data, array $options = []): string + { + // your encoding + } + + /** + * @param array $options + * + * @return array + */ + public function decode(string $data, array $options = []): array + { + // your decoding + } +} +``` +An `encode` call that fails must throw an `EncodeNotPossible` exception, +a failing `decode` call a `DecodeNotPossible` exception. + +The options are passed through from the serializer. The `JsonEncoder` understands +`Encoder::OPTION_PRETTY_PRINT`, which is used by the [cli](cli.md) to print readable payloads. + +```php +use Patchlevel\EventSourcing\Serializer\Encoder\Encoder; +use Patchlevel\EventSourcing\Serializer\EventSerializer; + +/** @var EventSerializer $serializer */ +$data = $serializer->serialize($event, [Encoder::OPTION_PRETTY_PRINT => true]); +``` +:::warning +The encoder decides the format of everything that is already in your store. +If you change it, old events can no longer be decoded. +::: + ## Normalizer Sometimes you also want to add more complex data as a payload. For example DateTime or value objects. @@ -140,6 +202,11 @@ use Patchlevel\EventSourcing\Metadata\Event\AttributeEventRegistryFactory; $eventRegistry = (new AttributeEventRegistryFactory())->create([/* paths... */]); ``` +:::tip +Scanning the paths on every request costs time. In production you can wrap the factory +in a [metadata cache](metadata-cache.md). +::: + ## Learn more * [How to normalize events](normalizer.md) @@ -147,3 +214,4 @@ $eventRegistry = (new AttributeEventRegistryFactory())->create([/* paths... */]) * [How to store events](store.md) * [How to upcast events](upcasting.md) * [How to use messages](message.md) +* [How to cache metadata](metadata-cache.md) diff --git a/docs/message.md b/docs/message.md index 71338672c..fbd0f5324 100644 --- a/docs/message.md +++ b/docs/message.md @@ -50,11 +50,53 @@ $message->headers(); // [AggregateHeader object] ## Built-in headers The message object has some built-in headers which are used internally. +Which of them you get depends on the [store](store.md) you use. -* `AggregateHeader` - Contains the aggregate name, aggregate id, playhead and recorded on. +These headers are set by every store: + +* `IndexHeader` - The global position of the message in the store. * `ArchivedHeader` - Flag if the message is archived. + +The `DoctrineDbalStore` is aggregate based and adds: + +* `AggregateHeader` - Contains the aggregate name, aggregate id, playhead and recorded on. * `StreamStartHeader` - Flag if the message is the first message in a new stream. +The `StreamDoctrineDbalStore` is stream based and splits the same information into single headers: + +* `StreamNameHeader` - The name of the stream, for example `profile-e3e3e3e3-3e3e-3e3e-3e3e-3e3e3e3e3e3e`. +* `PlayheadHeader` - The position of the message inside its stream. +* `RecordedOnHeader` - The point in time when the message was saved. +* `EventIdHeader` - The unique id of the event. + +```php +use Patchlevel\EventSourcing\Message\Message; +use Patchlevel\EventSourcing\Store\Header\EventIdHeader; +use Patchlevel\EventSourcing\Store\Header\IndexHeader; +use Patchlevel\EventSourcing\Store\Header\PlayheadHeader; +use Patchlevel\EventSourcing\Store\Header\RecordedOnHeader; +use Patchlevel\EventSourcing\Store\Header\StreamNameHeader; + +/** @var Message $message */ +$message->header(IndexHeader::class)->index; // 42 +$message->header(StreamNameHeader::class)->streamName; // 'profile-e3e3e3e3-...' +$message->header(PlayheadHeader::class)->playhead; // 2 +$message->header(RecordedOnHeader::class)->recordedOn; // DateTimeImmutable +$message->header(EventIdHeader::class)->eventId; // 'a4a4a4a4-4a4a-...' +``` +:::warning +The `PlayheadHeader` is only added if the stream is playhead based. +Streams that are written without a playhead, for example custom streams, +do not have this header. Use `hasHeader` before you access it. +::: + +:::note +The `AggregateHeader` and the stream based headers never appear on the same message. +If you migrate from the `DoctrineDbalStore` to the `StreamDoctrineDbalStore`, you can convert +them with the `AggregateToStreamHeaderTranslator`, see the +[store migration command](cli.md#store-migration-command). +::: + ## Custom headers You can also add custom headers to the message object. For example, you can add an application id. diff --git a/docs/metadata-cache.md b/docs/metadata-cache.md new file mode 100644 index 000000000..cd77bcbff --- /dev/null +++ b/docs/metadata-cache.md @@ -0,0 +1,185 @@ +# Metadata Cache + +The library reads a lot of information from attributes: which classes are aggregates and events, +which apply method belongs to which event, which subscriber listens to what. +This information is called metadata, and collecting it means scanning directories +and reflecting over classes. + +In development that is exactly what you want, because every change is picked up immediately. +In production the classes never change while the process runs, so the same work is repeated on every request. +For this the library ships decorators that cache the metadata in a +[PSR-6](https://www.php-fig.org/psr/psr-6/) or [PSR-16](https://www.php-fig.org/psr/psr-16/) cache. + +:::note +The library only provides the decorators, not the cache implementation itself. +You can use [symfony cache](https://symfony.com/doc/current/components/cache.html) or any other +PSR-6 or PSR-16 compatible cache. +::: + +## Registry cache + +Registries are the hashmaps between names and classes. +Building them means scanning all configured paths, which is the most expensive part of the metadata handling. + +The `AggregateRootRegistryFactory` and the `EventRegistryFactory` both have a caching decorator. +You wrap the attribute based factory and pass a cache to it. + +```php +use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegistryFactory; +use Patchlevel\EventSourcing\Metadata\AggregateRoot\Psr6AggregateRootRegistryFactory; +use Psr\Cache\CacheItemPoolInterface; + +/** @var CacheItemPoolInterface $cache */ +$aggregateRegistryFactory = new Psr6AggregateRootRegistryFactory( + new AttributeAggregateRootRegistryFactory(), + $cache, +); + +$aggregateRegistry = $aggregateRegistryFactory->create(['src/Domain']); +``` +```php +use Patchlevel\EventSourcing\Metadata\Event\AttributeEventRegistryFactory; +use Patchlevel\EventSourcing\Metadata\Event\Psr6EventRegistryFactory; +use Psr\Cache\CacheItemPoolInterface; + +/** @var CacheItemPoolInterface $cache */ +$eventRegistryFactory = new Psr6EventRegistryFactory( + new AttributeEventRegistryFactory(), + $cache, +); + +$eventRegistry = $eventRegistryFactory->create(['src/Domain']); +``` +If you use a PSR-16 cache, take the `Psr16` variants instead. They work the same way. + +```php +use Patchlevel\EventSourcing\Metadata\Event\AttributeEventRegistryFactory; +use Patchlevel\EventSourcing\Metadata\Event\Psr16EventRegistryFactory; +use Psr\SimpleCache\CacheInterface; + +/** @var CacheInterface $cache */ +$eventRegistryFactory = new Psr16EventRegistryFactory( + new AttributeEventRegistryFactory(), + $cache, +); +``` +:::warning +The registry factories cache under a fixed key, `aggregate_root_registry` and `event_registry`. +The paths you pass to `create` are not part of that key. +If you call the same factory with different paths, you get the result of the first call back. +Use a separate factory instance with its own cache for each set of paths. +::: + +## Metadata cache + +Next to the registries, there is the metadata of a single class. +These factories are called with a class name and return the metadata for it, +so they cache per class name. + +```php +use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootMetadataFactory; +use Patchlevel\EventSourcing\Metadata\AggregateRoot\Psr6AggregateRootMetadataFactory; +use Psr\Cache\CacheItemPoolInterface; + +/** @var CacheItemPoolInterface $cache */ +$aggregateMetadataFactory = new Psr6AggregateRootMetadataFactory( + new AttributeAggregateRootMetadataFactory(), + $cache, +); +``` +```php +use Patchlevel\EventSourcing\Metadata\Event\AttributeEventMetadataFactory; +use Patchlevel\EventSourcing\Metadata\Event\Psr6EventMetadataFactory; +use Psr\Cache\CacheItemPoolInterface; + +/** @var CacheItemPoolInterface $cache */ +$eventMetadataFactory = new Psr6EventMetadataFactory( + new AttributeEventMetadataFactory(), + $cache, +); +``` +```php +use Patchlevel\EventSourcing\Metadata\Subscriber\AttributeSubscriberMetadataFactory; +use Patchlevel\EventSourcing\Metadata\Subscriber\Psr6SubscriberMetadataFactory; +use Psr\Cache\CacheItemPoolInterface; + +/** @var CacheItemPoolInterface $cache */ +$subscriberMetadataFactory = new Psr6SubscriberMetadataFactory( + new AttributeSubscriberMetadataFactory(), + $cache, +); +``` +For all three there is a `Psr16` variant that takes a PSR-16 cache instead. + +:::note +The attribute based metadata factories already keep an in memory cache for the current process. +The PSR decorators add a cache that survives the process. +::: + +## Usage + +The cached factories are drop-in replacements, so you pass them wherever the library asks +for a factory or a registry. + +The registries go into the [repository manager](repository.md) and the serializer, +the aggregate metadata factory is the seventh argument of the `DefaultRepositoryManager`. + +```php +use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; +use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamDecorator; +use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; +use Patchlevel\EventSourcing\Store\Store; + +/** @var Store $store */ +$repositoryManager = new DefaultRepositoryManager( + $aggregateRegistryFactory->create(['src/Domain']), + $store, + null, + null, + new SplitStreamDecorator($eventMetadataFactory), + null, + $aggregateMetadataFactory, +); + +$serializer = new DefaultEventSerializer( + $eventRegistryFactory->create(['src/Domain']), +); +``` +The subscriber metadata factory goes into the `MetadataSubscriberAccessorRepository`. + +```php +use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository; + +$subscriberRepository = new MetadataSubscriberAccessorRepository( + [/* subscribers... */], + $subscriberMetadataFactory, +); +``` +:::note +Without an explicit factory, the `DefaultRepositoryManager` asks the aggregate class itself +for its metadata. Pass the cached factory as shown above if you want the repository manager to use it. +::: + +## Deployment + +The cache is keyed by class name and by a fixed registry key, not by the content of your classes. +Nothing invalidates it when you change an attribute. + +:::danger +You have to clear the cache on every deployment. +A stale registry makes the library load the wrong class for an event name, +a stale metadata entry makes it call the wrong apply method. +::: + +:::tip +Use a separate cache pool for the metadata and clear it as part of your deployment, +in the same step where you warm up your other caches. +Do not enable the cache in development, otherwise new events and subscribers are not picked up. +::: + +## Learn more + +* [How to register aggregates](aggregate.md) +* [How to register events](events.md) +* [How to create subscribers](subscription.md) +* [How to store events](store.md) diff --git a/docs/project.json b/docs/project.json index f6cc27897..bb4dd46c6 100644 --- a/docs/project.json +++ b/docs/project.json @@ -84,6 +84,10 @@ "title": "Time / Clock", "file": "clock.md" }, + { + "title": "Metadata Cache", + "file": "metadata-cache.md" + }, { "title": "Testing", "file": "testing.md" diff --git a/docs/repository.md b/docs/repository.md index 79deb558b..734abce63 100644 --- a/docs/repository.md +++ b/docs/repository.md @@ -176,7 +176,7 @@ An `AggregateOutdated` exception is thrown if a conflict occurs. ::: :::tip -If you use the Command Bus, you can use the [RetryOutdatedAggregateCommandBus](command-bus.md#retry-outdated-aggregate-command-bus) +If you use the Command Bus, you can use the [InstantRetryCommandBus](command-bus.md#instant-retry) to retry the command when an `AggregateOutdated` exception occurs automatically. ::: diff --git a/docs/store.md b/docs/store.md index da6f37a58..e59a6eced 100644 --- a/docs/store.md +++ b/docs/store.md @@ -340,12 +340,14 @@ use Patchlevel\EventSourcing\Store\Criteria\Criteria; use Patchlevel\EventSourcing\Store\Criteria\EventsCriterion; use Patchlevel\EventSourcing\Store\Criteria\FromIndexCriterion; use Patchlevel\EventSourcing\Store\Criteria\FromPlayheadCriterion; +use Patchlevel\EventSourcing\Store\Criteria\ToIndexCriterion; $criteria = new Criteria( new AggregateNameCriterion('profile'), new AggregateIdCriterion('e3e3e3e3-3e3e-3e3e-3e3e-3e3e3e3e3e3e'), new FromPlayheadCriterion(2), new FromIndexCriterion(100), + new ToIndexCriterion(200), new ArchivedCriterion(true), new EventsCriterion(['profile.created', 'profile.name_changed']), ); @@ -364,6 +366,56 @@ $criteria = (new CriteriaBuilder()) ->events(['profile.created', 'profile.name_changed']) ->build(); ``` +#### Criteria for Stream Stores + +The `StreamDoctrineDbalStore` does not know about aggregates, it works with stream names. +Instead of the aggregate criteria you use the `StreamCriterion`, which also supports a `*` wildcard. +On top of that it offers the `ToPlayheadCriterion` and the `EventIdCriterion`. + +```php +use Patchlevel\EventSourcing\Store\Criteria\Criteria; +use Patchlevel\EventSourcing\Store\Criteria\EventIdCriterion; +use Patchlevel\EventSourcing\Store\Criteria\FromPlayheadCriterion; +use Patchlevel\EventSourcing\Store\Criteria\StreamCriterion; +use Patchlevel\EventSourcing\Store\Criteria\ToPlayheadCriterion; + +$criteria = new Criteria( + new StreamCriterion('profile-e3e3e3e3-3e3e-3e3e-3e3e-3e3e3e3e3e3e'), + new FromPlayheadCriterion(2), + new ToPlayheadCriterion(10), +); + +$criteria = new Criteria( + new EventIdCriterion('a4a4a4a4-4a4a-4a4a-4a4a-4a4a4a4a4a4a'), +); +``` +The `StreamCriterion` is variadic, so you can pass multiple stream names. +There is also a `startWith` named constructor that appends the wildcard for you. + +```php +use Patchlevel\EventSourcing\Store\Criteria\StreamCriterion; + +$criterion = new StreamCriterion('profile-*', 'hotel-*'); +$criterion = StreamCriterion::startWith('profile-'); +``` +The criteria builder covers this as well: + +```php +use Patchlevel\EventSourcing\Store\Criteria\CriteriaBuilder; + +$criteria = (new CriteriaBuilder()) + ->streamName('profile-*') + ->fromPlayhead(2) + ->toPlayhead(10) + ->build(); +``` +:::warning +Not every store supports every criterion. If a store gets a criterion it cannot handle, +it throws an `UnsupportedCriterion` exception. `StreamCriterion`, `ToPlayheadCriterion` and +`EventIdCriterion` only work with the `StreamDoctrineDbalStore`, +`AggregateNameCriterion` and `AggregateIdCriterion` only with the `DoctrineDbalStore`. +::: + #### Stream The load method returns a `Stream` object and is a generator. @@ -449,18 +501,66 @@ In event sourcing, the events are immutable. ### Remove -You can remove a stream with the `remove` method. +You can remove events with the `remove` method. It takes the same criteria as the `load` method. ```php +use Patchlevel\EventSourcing\Store\Criteria\Criteria; +use Patchlevel\EventSourcing\Store\Criteria\StreamCriterion; use Patchlevel\EventSourcing\Store\StreamStore; /** @var StreamStore $store */ -$store->remove('profile-*'); +$store->remove(new Criteria(StreamCriterion::startWith('profile-'))); ``` +:::danger +Without criteria the method removes every event in the store. +Deleted events cannot be restored, all subscriptions built from them become inconsistent. +::: + :::note The method is only available in the `StreamStore` like `StreamDoctrineDbalStore`. ::: +### Archive + +You can archive events with the `archive` method. +Archived events are still in the store, but they are skipped when an aggregate is loaded, +which keeps the loading of long living aggregates fast. + +```php +use Patchlevel\EventSourcing\Store\Criteria\Criteria; +use Patchlevel\EventSourcing\Store\Criteria\StreamCriterion; +use Patchlevel\EventSourcing\Store\Criteria\ToPlayheadCriterion; +use Patchlevel\EventSourcing\Store\StreamStore; + +/** @var StreamStore $store */ +$store->archive( + new Criteria( + new StreamCriterion('profile-e3e3e3e3-3e3e-3e3e-3e3e-3e3e3e3e3e3e'), + new ToPlayheadCriterion(100), + ), +); +``` +Archived events get the `ArchivedHeader` when they are loaded again. +You can include or exclude them explicitly with the `ArchivedCriterion`. + +```php +use Patchlevel\EventSourcing\Store\Criteria\ArchivedCriterion; +use Patchlevel\EventSourcing\Store\Criteria\Criteria; +use Patchlevel\EventSourcing\Store\Store; + +/** @var Store $store */ +$stream = $store->load(new Criteria(new ArchivedCriterion(false))); +``` +:::note +The method is only available in the `StreamStore` like `StreamDoctrineDbalStore`. +The `DoctrineDbalStore` archives events automatically when a [split stream](split-stream.md) event is saved. +::: + +:::tip +Archiving is the non destructive alternative to `remove`. The events stay readable, +so you can still replay them by passing an `ArchivedCriterion(true)`. +::: + ### List Streams You can list all streams with the `streams` method. diff --git a/docs/subscription.md b/docs/subscription.md index 4fea2afbe..e52651e5c 100644 --- a/docs/subscription.md +++ b/docs/subscription.md @@ -988,6 +988,58 @@ use Patchlevel\EventSourcing\Subscription\RetryStrategy\NoRetryStrategy; $retryStrategy = new NoRetryStrategy(); ``` +#### Custom Retry Strategy + +You can write your own strategy by implementing the `RetryStrategy` interface. +The `shouldRetry` method is asked on every run whether the errored subscription should be picked up again. + +```php +use Patchlevel\EventSourcing\Subscription\RetryStrategy\RetryStrategy; +use Patchlevel\EventSourcing\Subscription\Subscription; + +final class AlwaysRetryStrategy implements RetryStrategy +{ + public function shouldRetry(Subscription $subscription): bool + { + return true; + } +} +``` +A plain `RetryStrategy` can never mark a subscription as failed. +The subscription stays in the error status and is offered to `shouldRetry` again on every run. +To give up at some point, implement `ConditionalRetryStrategy` instead. +It adds a `canRetry` method that answers whether a retry is possible at all. +As soon as `canRetry` returns `false`, the subscription is set to failed and is skipped in all future runs. + +```php +use Patchlevel\EventSourcing\Subscription\RetryStrategy\ConditionalRetryStrategy; +use Patchlevel\EventSourcing\Subscription\Subscription; + +final class TenAttemptsRetryStrategy implements ConditionalRetryStrategy +{ + public function canRetry(Subscription $subscription): bool + { + return $subscription->retryAttempt() < 10; + } + + public function shouldRetry(Subscription $subscription): bool + { + return $this->canRetry($subscription); + } +} +``` +:::note +Both built-in strategies implement `ConditionalRetryStrategy`. +The `ClockBasedRetryStrategy` returns `false` in `canRetry` once `maxAttempts` is reached, +the `NoRetryStrategy` always returns `false`. +::: + +:::warning +`shouldRetry` is called on every run of the subscription engine, so keep it cheap. +Use `canRetry` for the "is there any point in trying again" decision and `shouldRetry` +for the "is it time to try again" decision. +::: + #### Retry Strategy Repository You can define multiple retry strategies and select them by name in the subscriber.