Skip to content

Commit be78b47

Browse files
authored
Merge pull request #251 from patchlevel/retry-strategy-repository
add retry strategy repository
2 parents 12b2ea5 + 0c35c32 commit be78b47

5 files changed

Lines changed: 226 additions & 31 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
],
2020
"require": {
2121
"php": "~8.2.0 || ~8.3.0",
22-
"patchlevel/event-sourcing": "^3.9.0",
22+
"patchlevel/event-sourcing": "^3.10.0",
2323
"symfony/cache": "^6.4.0|^7.0.0",
2424
"symfony/config": "^6.4.0|^7.0.0",
2525
"symfony/console": "^6.4.1|^7.0.1",

composer.lock

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/DependencyInjection/Configuration.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
* command_bus: array{enabled: bool, service: string},
1414
* subscription: array{
1515
* store: array{type: string, service: string|null},
16-
* retry_strategy: array{base_delay: int, delay_factor: int, max_attempts: int},
16+
* retry_strategy?: array{base_delay: int, delay_factor: int, max_attempts: int},
17+
* retry_strategies: array<string, array{type: string, service: string, options: array<string, mixed>}>,
18+
* default_retry_strategy: string,
1719
* catch_up: array{enabled: bool, limit: positive-int|null},
1820
* throw_on_error: array{enabled: bool},
1921
* run_after_aggregate_save: array{
@@ -182,14 +184,44 @@ public function getConfigTreeBuilder(): TreeBuilder
182184
->end()
183185

184186
->arrayNode('retry_strategy')
185-
->addDefaultsIfNotSet()
187+
->setDeprecated(
188+
'patchlevel/event-sourcing-bundle',
189+
'3.10',
190+
'The "%node%" option is deprecated and will be removed in 4.0. Use "patchlevel_event_sourcing.subscription.retry_strategies" instead.'
191+
)
186192
->children()
187193
->integerNode('base_delay')->defaultValue(5)->end()
188194
->integerNode('delay_factor')->defaultValue(2)->end()
189195
->integerNode('max_attempts')->defaultValue(5)->end()
190196
->end()
191197
->end()
192198

199+
->arrayNode('retry_strategies')
200+
->useAttributeAsKey('name')
201+
->arrayPrototype()
202+
->children()
203+
->enumNode('type')->values(['clock_based', 'no_retry', 'custom'])->end()
204+
->scalarNode('service')->end()
205+
->arrayNode('options')->variablePrototype()->end()->end()
206+
->end()
207+
->end()
208+
->defaultValue([
209+
'default' => [
210+
'type' => 'clock_based',
211+
'options' => [
212+
'base_delay' => 5,
213+
'delay_factor' => 2,
214+
'max_attempts' => 5,
215+
],
216+
],
217+
'no_retry' => [
218+
'type' => 'no_retry',
219+
],
220+
])
221+
->end()
222+
223+
->scalarNode('default_retry_strategy')->defaultValue('default')->end()
224+
193225
->arrayNode('catch_up')
194226
->canBeEnabled()
195227
->addDefaultsIfNotSet()

src/DependencyInjection/PatchlevelEventSourcingExtension.php

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@
9797
use Patchlevel\EventSourcing\Subscription\Engine\ThrowOnErrorSubscriptionEngine;
9898
use Patchlevel\EventSourcing\Subscription\Repository\RunSubscriptionEngineRepositoryManager;
9999
use Patchlevel\EventSourcing\Subscription\RetryStrategy\ClockBasedRetryStrategy;
100+
use Patchlevel\EventSourcing\Subscription\RetryStrategy\NoRetryStrategy;
100101
use Patchlevel\EventSourcing\Subscription\RetryStrategy\RetryStrategy;
102+
use Patchlevel\EventSourcing\Subscription\RetryStrategy\RetryStrategyRepository;
101103
use Patchlevel\EventSourcing\Subscription\Store\DoctrineSubscriptionStore;
102104
use Patchlevel\EventSourcing\Subscription\Store\InMemorySubscriptionStore;
103105
use Patchlevel\EventSourcing\Subscription\Store\SubscriptionStore;
@@ -335,16 +337,73 @@ static function (ChildDefinition $definition): void {
335337
$container->register(AttributeSubscriberMetadataFactory::class);
336338
$container->setAlias(SubscriberMetadataFactory::class, AttributeSubscriberMetadataFactory::class);
337339

338-
$container->register(ClockBasedRetryStrategy::class)
340+
$strategies = [];
341+
342+
$retryStrategy = $config['subscription']['retry_strategy'] ?? null;
343+
344+
if ($retryStrategy) {
345+
$container->register(ClockBasedRetryStrategy::class)
346+
->setArguments([
347+
new Reference('event_sourcing.clock'),
348+
$retryStrategy['base_delay'],
349+
$retryStrategy['delay_factor'],
350+
$retryStrategy['max_attempts'],
351+
]);
352+
353+
$container->register(NoRetryStrategy::class);
354+
355+
$container
356+
->setAlias(RetryStrategy::class, ClockBasedRetryStrategy::class)
357+
->setDeprecated(
358+
'patchlevel/event-sourcing-bundle',
359+
'3.10',
360+
'The "%alias_id%" alias is deprecated, use "RetryStrategyRepository" instead.',
361+
);
362+
363+
$strategies['default'] = new Reference(RetryStrategy::class);
364+
$strategies['no_retry'] = new Reference(NoRetryStrategy::class);
365+
} else {
366+
foreach ($config['subscription']['retry_strategies'] as $name => $strategyConfig) {
367+
if ($strategyConfig['type'] === 'custom') {
368+
$strategies[$name] = new Reference($strategyConfig['service']);
369+
370+
continue;
371+
}
372+
373+
$id = 'event_sourcing.subscription.retry_strategy.' . $name;
374+
375+
if ($strategyConfig['type'] === 'clock_based') {
376+
$container->register($id, ClockBasedRetryStrategy::class)
377+
->setArguments([
378+
new Reference('event_sourcing.clock'),
379+
$strategyConfig['options']['base_delay'] ?? 5,
380+
$strategyConfig['options']['delay_factor'] ?? 2,
381+
$strategyConfig['options']['max_attempts'] ?? 5,
382+
]);
383+
384+
$strategies[$name] = new Reference($id);
385+
386+
continue;
387+
}
388+
389+
if ($strategyConfig['type'] === 'no_retry') {
390+
$container->register($id, NoRetryStrategy::class);
391+
392+
$strategies[$name] = new Reference($id);
393+
394+
continue;
395+
}
396+
397+
throw new InvalidArgumentException(sprintf('Unknown retry strategy type "%s"', $strategyConfig['type']));
398+
}
399+
}
400+
401+
$container->register(RetryStrategyRepository::class)
339402
->setArguments([
340-
new Reference('event_sourcing.clock'),
341-
$config['subscription']['retry_strategy']['base_delay'],
342-
$config['subscription']['retry_strategy']['delay_factor'],
343-
$config['subscription']['retry_strategy']['max_attempts'],
403+
$strategies,
404+
$config['subscription']['default_retry_strategy'],
344405
]);
345406

346-
$container->setAlias(RetryStrategy::class, ClockBasedRetryStrategy::class);
347-
348407
$container->register(SubscriberHelper::class)
349408
->setArguments([new Reference(SubscriberMetadataFactory::class)]);
350409

@@ -395,7 +454,7 @@ static function (ChildDefinition $definition): void {
395454
new Reference(Store::class),
396455
new Reference(SubscriptionStore::class),
397456
new Reference(SubscriberAccessorRepository::class),
398-
new Reference(RetryStrategy::class),
457+
new Reference(RetryStrategyRepository::class),
399458
new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE),
400459
])
401460
->addTag('monolog.logger', ['channel' => 'event_sourcing']);

tests/Unit/PatchlevelEventSourcingBundleTest.php

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine;
6767
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
6868
use Patchlevel\EventSourcing\Subscription\Repository\RunSubscriptionEngineRepositoryManager;
69+
use Patchlevel\EventSourcing\Subscription\RetryStrategy\ClockBasedRetryStrategy;
70+
use Patchlevel\EventSourcing\Subscription\RetryStrategy\NoRetryStrategy;
71+
use Patchlevel\EventSourcing\Subscription\RetryStrategy\RetryStrategy;
72+
use Patchlevel\EventSourcing\Subscription\RetryStrategy\RetryStrategyRepository;
6973
use Patchlevel\EventSourcing\Subscription\Store\DoctrineSubscriptionStore;
7074
use Patchlevel\EventSourcing\Subscription\Store\InMemorySubscriptionStore;
7175
use Patchlevel\EventSourcing\Subscription\Store\SubscriptionStore;
@@ -96,7 +100,6 @@
96100
use Psr\EventDispatcher\EventDispatcherInterface;
97101
use Psr\Log\LoggerInterface;
98102
use Psr\SimpleCache\CacheInterface;
99-
use stdClass;
100103
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
101104
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
102105
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -1129,8 +1132,7 @@ public function testAutoconfigureArgumentResolver(): void
11291132
$container = new ContainerBuilder();
11301133

11311134
$container->setDefinition(DummyArgumentResolver::class, new Definition(DummyArgumentResolver::class))
1132-
->setAutoconfigured(true)
1133-
;
1135+
->setAutoconfigured(true);
11341136

11351137
$this->compileContainer(
11361138
$container,
@@ -1144,8 +1146,110 @@ public function testAutoconfigureArgumentResolver(): void
11441146
);
11451147

11461148
self::assertTrue($container->getDefinition(DummyArgumentResolver::class)->hasTag('event_sourcing.argument_resolver'));
1147-
self::assertInstanceOf(TaggedIteratorArgument::class, $container->getDefinition(MetadataSubscriberAccessorRepository::class)->getArgument(2));
1148-
self::assertEquals('event_sourcing.argument_resolver', $container->getDefinition(MetadataSubscriberAccessorRepository::class)->getArgument(2)->getTag());
1149+
self::assertInstanceOf(TaggedIteratorArgument::class,
1150+
$container->getDefinition(MetadataSubscriberAccessorRepository::class)->getArgument(2));
1151+
self::assertEquals('event_sourcing.argument_resolver',
1152+
$container->getDefinition(MetadataSubscriberAccessorRepository::class)->getArgument(2)->getTag());
1153+
}
1154+
1155+
public function testLegacyRetryStrategy(): void
1156+
{
1157+
$container = new ContainerBuilder();
1158+
1159+
$this->compileContainer(
1160+
$container,
1161+
[
1162+
'patchlevel_event_sourcing' => [
1163+
'connection' => [
1164+
'service' => 'doctrine.dbal.eventstore_connection',
1165+
],
1166+
'subscription' => [
1167+
'retry_strategy' => [
1168+
'base_delay' => 10,
1169+
'delay_factor' => 11,
1170+
'max_attempts' => 12,
1171+
],
1172+
],
1173+
],
1174+
]
1175+
);
1176+
1177+
$repository = $container->get(RetryStrategyRepository::class);
1178+
1179+
self::assertInstanceOf(RetryStrategyRepository::class, $repository);
1180+
self::assertInstanceOf(ClockBasedRetryStrategy::class, $repository->getDefaultRetryStrategy());
1181+
self::assertInstanceOf(ClockBasedRetryStrategy::class, $repository->get('default'));
1182+
self::assertInstanceOf(NoRetryStrategy::class, $repository->get('no_retry'));
1183+
}
1184+
1185+
public function testRetryStrategy(): void
1186+
{
1187+
$container = new ContainerBuilder();
1188+
1189+
$this->compileContainer(
1190+
$container,
1191+
[
1192+
'patchlevel_event_sourcing' => [
1193+
'connection' => [
1194+
'service' => 'doctrine.dbal.eventstore_connection',
1195+
],
1196+
'subscription' => [
1197+
'retry_strategies' => [
1198+
'default' => [
1199+
'type' => 'clock_based',
1200+
'options' => [
1201+
'base_delay' => 10,
1202+
'delay_factor' => 11,
1203+
'max_attempts' => 12,
1204+
],
1205+
],
1206+
'no_retry' => [
1207+
'type' => 'no_retry',
1208+
]
1209+
]
1210+
],
1211+
],
1212+
]
1213+
);
1214+
1215+
$repository = $container->get(RetryStrategyRepository::class);
1216+
1217+
self::assertInstanceOf(RetryStrategyRepository::class, $repository);
1218+
self::assertInstanceOf(ClockBasedRetryStrategy::class, $repository->getDefaultRetryStrategy());
1219+
self::assertInstanceOf(ClockBasedRetryStrategy::class, $repository->get('default'));
1220+
self::assertInstanceOf(NoRetryStrategy::class, $repository->get('no_retry'));
1221+
}
1222+
1223+
public function testRetryStrategyCustom(): void
1224+
{
1225+
$retryStrategy = $this->prophesize(RetryStrategy::class)->reveal();
1226+
1227+
$container = new ContainerBuilder();
1228+
$container->set('my_retry_strategy', $retryStrategy);
1229+
1230+
$this->compileContainer(
1231+
$container,
1232+
[
1233+
'patchlevel_event_sourcing' => [
1234+
'connection' => [
1235+
'service' => 'doctrine.dbal.eventstore_connection',
1236+
],
1237+
'subscription' => [
1238+
'retry_strategies' => [
1239+
'default' => [
1240+
'type' => 'custom',
1241+
'service' => 'my_retry_strategy'
1242+
],
1243+
]
1244+
],
1245+
],
1246+
]
1247+
);
1248+
1249+
$repository = $container->get(RetryStrategyRepository::class);
1250+
1251+
self::assertInstanceOf(RetryStrategyRepository::class, $repository);
1252+
self::assertEquals($retryStrategy, $repository->getDefaultRetryStrategy());
11491253
}
11501254

11511255
public function testSchemaMerge(): void

0 commit comments

Comments
 (0)