Skip to content

Commit 543bd2c

Browse files
authored
Merge pull request #848 from patchlevel/refactor-subscription-engine
Refactor subscription engine
2 parents 466f278 + a9eb109 commit 543bd2c

81 files changed

Lines changed: 6625 additions & 6835 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docker-compose.yaml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1+
# docker run --rm -it --volume $PWD:/app --net="host" -w /app ghcr.io/patchlevel/php:8.5
12
services:
3+
php:
4+
image: ghcr.io/patchlevel/php:8.5
5+
volumes:
6+
- .:/app
7+
working_dir: /app
8+
network_mode: host
9+
tty: true
10+
stdin_open: true
11+
command: sleep infinity
12+
213
postgres:
314
image: postgres:alpine
415
environment:
@@ -13,4 +24,4 @@ services:
1324
- MYSQL_ALLOW_EMPTY_PASSWORD="yes"
1425
- MYSQL_DATABASE=eventstore
1526
ports:
16-
- 3306:3306
27+
- 3306:3306

docs/UPGRADE-4.0.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,60 @@ $subscriptionEngine = new DefaultSubscriptionEngine(
8686
RetryStrategyRepository::withDefault($retryStrategy),
8787
);
8888
```
89+
### Subscription Engine Commands
90+
91+
The `SubscriptionEngine` interface has been changed.
92+
The methods `setup`, `boot`, `run`, `teardown`, `remove`, `reactivate`, `pause` and `refresh` have been replaced
93+
by a single `execute` method that takes a command object.
94+
The `ids` and `groups` filters, previously passed via `SubscriptionEngineCriteria`,
95+
are now constructor parameters of the command objects.
96+
The `SubscriptionEngineCriteria` is now only used for the `subscriptions` method.
97+
98+
before:
99+
100+
```php
101+
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
102+
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria;
103+
104+
/** @var SubscriptionEngine $subscriptionEngine */
105+
$subscriptionEngine->setup(new SubscriptionEngineCriteria(ids: ['profile_1']), skipBooting: true);
106+
$subscriptionEngine->boot(new SubscriptionEngineCriteria(ids: ['profile_1']), limit: 100);
107+
$subscriptionEngine->run(new SubscriptionEngineCriteria(ids: ['profile_1']), limit: 100);
108+
$subscriptionEngine->teardown(new SubscriptionEngineCriteria(ids: ['profile_1']));
109+
$subscriptionEngine->remove(new SubscriptionEngineCriteria(ids: ['profile_1']));
110+
$subscriptionEngine->reactivate(new SubscriptionEngineCriteria(ids: ['profile_1']));
111+
$subscriptionEngine->pause(new SubscriptionEngineCriteria(ids: ['profile_1']));
112+
$subscriptionEngine->refresh(new SubscriptionEngineCriteria(ids: ['profile_1']));
113+
```
114+
after:
115+
116+
```php
117+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Boot;
118+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Pause;
119+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Reactivate;
120+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Refresh;
121+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Remove;
122+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Run;
123+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Setup;
124+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Teardown;
125+
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
126+
127+
/** @var SubscriptionEngine $subscriptionEngine */
128+
$subscriptionEngine->execute(new Setup(ids: ['profile_1'], skipBooting: true));
129+
$subscriptionEngine->execute(new Boot(ids: ['profile_1'], limit: 100));
130+
$subscriptionEngine->execute(new Run(ids: ['profile_1'], limit: 100));
131+
$subscriptionEngine->execute(new Teardown(ids: ['profile_1']));
132+
$subscriptionEngine->execute(new Remove(ids: ['profile_1']));
133+
$subscriptionEngine->execute(new Reactivate(ids: ['profile_1']));
134+
$subscriptionEngine->execute(new Pause(ids: ['profile_1']));
135+
$subscriptionEngine->execute(new Refresh(ids: ['profile_1']));
136+
```
137+
Further changes:
138+
139+
* The `CanRefreshSubscriptions` interface has been removed. Refresh is now part of the `SubscriptionEngine` interface via the `Refresh` command.
140+
* `ProcessedResult` now extends `Result`, so the `execute` method always returns a `Result`. The `Boot` and `Run` commands return a `ProcessedResult`.
141+
* The `DefaultSubscriptionEngine` accepts an optional `EventDispatcherInterface` as last constructor argument to hook into the engine with own listeners.
142+
89143
## Store
90144

91145
### StreamStore

docs/cli.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ To manage your subscriptions there are the following cli commands.
3434
* SubscriptionBootCommand: `event-sourcing:subscription:boot`
3535
* SubscriptionPauseCommand: `event-sourcing:subscription:pause`
3636
* SubscriptionReactiveCommand: `event-sourcing:subscription:reactive`
37+
* SubscriptionRefreshCommand: `event-sourcing:subscription:refresh`
3738
* SubscriptionRemoveCommand: `event-sourcing:subscription:remove`
3839
* SubscriptionRunCommand: `event-sourcing:subscription:run`
3940
* SubscriptionSetupCommand: `event-sourcing:subscription:setup`
@@ -86,6 +87,7 @@ $cli->addCommands([
8687
new Command\SubscriptionTeardownCommand($subscriptionEngine),
8788
new Command\SubscriptionRemoveCommand($subscriptionEngine),
8889
new Command\SubscriptionReactivateCommand($subscriptionEngine),
90+
new Command\SubscriptionRefreshCommand($subscriptionEngine),
8991
new Command\SubscriptionSetupCommand($subscriptionEngine),
9092
new Command\SubscriptionStatusCommand($subscriptionEngine),
9193
new Command\SchemaCreateCommand($schemaDirector),

docs/getting-started.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ use Doctrine\DBAL\Connection;
339339
use Patchlevel\EventSourcing\Schema\ChainDoctrineSchemaConfigurator;
340340
use Patchlevel\EventSourcing\Schema\DoctrineSchemaDirector;
341341
use Patchlevel\EventSourcing\Store\Store;
342+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Setup;
342343
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
343344
use Patchlevel\EventSourcing\Subscription\Store\SubscriptionStore;
344345

@@ -358,7 +359,7 @@ $schemaDirector = new DoctrineSchemaDirector(
358359
$schemaDirector->create();
359360

360361
/** @var SubscriptionEngine $engine */
361-
$engine->setup(skipBooting: true);
362+
$engine->execute(new Setup(skipBooting: true));
362363
```
363364

364365
:::note

docs/subscription.md

Lines changed: 81 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,8 @@ $subscriberAccessorRepository = new MetadataSubscriberAccessorRepository([
11421142
Now we can create the subscription engine and plug together the necessary services.
11431143
The message loader is needed to load the messages, the Subscription Store to store the subscription state
11441144
and we need the subscriber accessor repository. Optionally, we can also pass a retry strategy.
1145-
Finally, if we want to use the cleanup feature, we need to pass the cleanup handlers.
1145+
If we want to use the cleanup feature, we need to pass the cleanup handlers.
1146+
Finally, we can pass an event dispatcher to hook into the engine with own listeners.
11461147

11471148
```php
11481149
use Doctrine\DBAL\Connection;
@@ -1153,6 +1154,7 @@ use Patchlevel\EventSourcing\Subscription\Engine\MessageLoader;
11531154
use Patchlevel\EventSourcing\Subscription\RetryStrategy\RetryStrategyRepository;
11541155
use Patchlevel\EventSourcing\Subscription\Store\DoctrineSubscriptionStore;
11551156
use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository;
1157+
use Symfony\Component\EventDispatcher\EventDispatcher;
11561158

11571159
/**
11581160
* @var MessageLoader $messageLoader
@@ -1169,6 +1171,34 @@ $subscriptionEngine = new DefaultSubscriptionEngine(
11691171
$retryStrategyRepository, // optional, if not set the default retry strategy is used
11701172
$logger, // optional
11711173
new DefaultCleaner([new DbalCleanupTaskHandler($projectionConnection)]), // optional but required if you want to use the cleanup feature
1174+
new EventDispatcher(), // optional, to hook into the engine with own listeners
1175+
);
1176+
```
1177+
### Engine Events
1178+
1179+
The `DefaultSubscriptionEngine` dispatches events during processing on the passed event dispatcher.
1180+
You can register your own listeners to hook into the engine, for example for logging, metrics or batching.
1181+
1182+
| Event | Description |
1183+
|--------------------------|----------------------------------------------------------------------|
1184+
| `OnCommand` | A command was passed to the engine for execution |
1185+
| `OnSubscriptions` | The engine determined the subscriptions for the current command |
1186+
| `OnHandleMessage` | A message is about to be passed to a subscriber |
1187+
| `OnHandleMessageSuccess` | A message was successfully handled by a subscriber |
1188+
| `OnHandleMessageError` | An error occurred while a subscriber was handling a message |
1189+
| `OnProcessingFinished` | The engine finished processing the stream (ended or limit reached) |
1190+
| `OnResult` | The engine finished the command and returns the result |
1191+
1192+
```php
1193+
use Patchlevel\EventSourcing\Subscription\Engine\Event\OnHandleMessageError;
1194+
use Symfony\Component\EventDispatcher\EventDispatcher;
1195+
1196+
$eventDispatcher = new EventDispatcher();
1197+
$eventDispatcher->addListener(
1198+
OnHandleMessageError::class,
1199+
static function (OnHandleMessageError $event): void {
1200+
// own error handling like logging or metrics
1201+
},
11721202
);
11731203
```
11741204
### Catch up Subscription Engine
@@ -1249,15 +1279,20 @@ Especially in combination with the `CatchUpSubscriptionEngine` and `ThrowOnError
12491279

12501280
## Usage
12511281

1252-
The Subscription Engine has a few methods needed to use it effectively.
1253-
A `SubscriptionEngineCriteria` can be passed to all of these methods to filter the respective subscriptions.
1282+
The Subscription Engine is controlled with command objects.
1283+
Each command is passed to the `execute` method, which returns a `Result` with the errors that occurred.
1284+
Every command accepts `ids` and `groups` parameters to filter the subscriptions the command should be applied to.
12541285

12551286
```php
1256-
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria;
1287+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Run;
1288+
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
12571289

1258-
$criteria = new SubscriptionEngineCriteria(
1259-
ids: ['profile_1', 'welcome_email'],
1260-
groups: ['default'],
1290+
/** @var SubscriptionEngine $subscriptionEngine */
1291+
$subscriptionEngine->execute(
1292+
new Run(
1293+
ids: ['profile_1', 'welcome_email'],
1294+
groups: ['default'],
1295+
),
12611296
);
12621297
```
12631298

@@ -1272,52 +1307,62 @@ In this step, the subscription engine also tries to call the `setup` method if a
12721307
After the setup process, the subscription is set to booting or active.
12731308

12741309
```php
1310+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Setup;
12751311
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
1276-
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria;
12771312

12781313
/** @var SubscriptionEngine $subscriptionEngine */
1279-
$subscriptionEngine->setup(new SubscriptionEngineCriteria());
1314+
$subscriptionEngine->execute(new Setup());
12801315
```
12811316

12821317
:::tip
1283-
You can skip the booting step with the second boolean parameter named `skipBooting`.
1318+
You can skip the booting step with the `skipBooting` parameter: `new Setup(skipBooting: true)`.
12841319
:::
12851320

12861321
### Boot
12871322

1288-
You can boot the subscriptions with the `boot` method.
1323+
You can boot the subscriptions with the `Boot` command.
12891324
All booting subscriptions will catch up to the current event stream.
12901325
After the boot process, the subscription is set to active or finished.
12911326

12921327
```php
1328+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Boot;
12931329
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
1294-
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria;
12951330

12961331
/** @var SubscriptionEngine $subscriptionEngine */
1297-
$subscriptionEngine->boot(new SubscriptionEngineCriteria());
1332+
$subscriptionEngine->execute(new Boot());
12981333
```
1334+
1335+
:::tip
1336+
You can limit the number of processed messages with the `limit` parameter: `new Boot(limit: 100)`.
1337+
:::
1338+
12991339
### Run
13001340

13011341
All active subscriptions are continued and updated here.
13021342

13031343
```php
1344+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Run;
13041345
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
1305-
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria;
13061346

13071347
/** @var SubscriptionEngine $subscriptionEngine */
1308-
$subscriptionEngine->run(new SubscriptionEngineCriteria());
1348+
$subscriptionEngine->execute(new Run());
13091349
```
1350+
1351+
:::tip
1352+
You can limit the number of processed messages with the `limit` parameter: `new Run(limit: 100)`.
1353+
:::
1354+
13101355
### Teardown
13111356

13121357
If subscriptions are detached, they can be cleaned up here.
13131358
The subscription engine also tries to call the `teardown` method if available.
13141359

13151360
```php
1361+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Teardown;
13161362
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
1317-
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria;
13181363

13191364
/** @var SubscriptionEngine $subscriptionEngine */
1320-
$subscriptionEngine->teardown(new SubscriptionEngineCriteria());
1365+
$subscriptionEngine->execute(new Teardown());
13211366
```
13221367
### Remove
13231368

@@ -1326,23 +1371,23 @@ An attempt is made to call the `teardown` method if available.
13261371
But the entry will still be removed if it doesn't work.
13271372

13281373
```php
1374+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Remove;
13291375
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
1330-
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria;
13311376

13321377
/** @var SubscriptionEngine $subscriptionEngine */
1333-
$subscriptionEngine->remove(new SubscriptionEngineCriteria());
1378+
$subscriptionEngine->execute(new Remove());
13341379
```
13351380
### Reactivate
13361381

13371382
If a subscription had an error or is outdated, you can reactivate it.
13381383
As a result, the subscription gets in the last status again.
13391384

13401385
```php
1386+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Reactivate;
13411387
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
1342-
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria;
13431388

13441389
/** @var SubscriptionEngine $subscriptionEngine */
1345-
$subscriptionEngine->reactivate(new SubscriptionEngineCriteria());
1390+
$subscriptionEngine->execute(new Reactivate());
13461391
```
13471392
### Pause
13481393

@@ -1351,38 +1396,39 @@ The subscription will then no longer be managed by the subscription engine.
13511396
You can reactivate the subscription if you want so that it continues.
13521397

13531398
```php
1399+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Pause;
13541400
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
1355-
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria;
13561401

13571402
/** @var SubscriptionEngine $subscriptionEngine */
1358-
$subscriptionEngine->pause(new SubscriptionEngineCriteria());
1403+
$subscriptionEngine->execute(new Pause());
13591404
```
1360-
### Status
1405+
### Refresh
13611406

1362-
To get the current status of all subscriptions, you can get them using the `subscriptions` method.
1407+
If you change the metadata of a subscriber in the code (e.g. `runMode`, `group` or `cleanupTasks`),
1408+
you can use the `Refresh` command to update the existing subscriptions in the store.
13631409

13641410
```php
1411+
use Patchlevel\EventSourcing\Subscription\Engine\Command\Refresh;
13651412
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
1366-
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria;
13671413

13681414
/** @var SubscriptionEngine $subscriptionEngine */
1369-
$subscriptions = $subscriptionEngine->subscriptions(new SubscriptionEngineCriteria());
1370-
1371-
foreach ($subscriptions as $subscription) {
1372-
echo $subscription->status()->value;
1373-
}
1415+
$subscriptionEngine->execute(new Refresh());
13741416
```
1375-
### Refresh
1417+
### Status
13761418

1377-
If you change the metadata of a subscriber in the code (e.g. `runMode`, `group` or `cleanupTasks`),
1378-
you can use the `refresh` method to update the existing subscriptions in the store.
1419+
To get the current status of all subscriptions, you can get them using the `subscriptions` method.
1420+
A `SubscriptionEngineCriteria` can be passed to filter the subscriptions.
13791421

13801422
```php
13811423
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
13821424
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria;
13831425

13841426
/** @var SubscriptionEngine $subscriptionEngine */
1385-
$subscriptionEngine->refresh(new SubscriptionEngineCriteria());
1427+
$subscriptions = $subscriptionEngine->subscriptions(new SubscriptionEngineCriteria());
1428+
1429+
foreach ($subscriptions as $subscription) {
1430+
echo $subscription->status()->value;
1431+
}
13861432
```
13871433
## Learn more
13881434

phpstan-baseline.neon

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ parameters:
102102
count: 1
103103
path: src/Store/TaggableDoctrineDbalStore.php
104104

105+
-
106+
message: '#^Parameter \#1 \$command of callable Patchlevel\\EventSourcing\\Subscription\\Engine\\Handler\\BootHandler\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Handler\\PauseHandler\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Handler\\ReactivateHandler\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Handler\\RefreshHandler\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Handler\\RemoveHandler\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Handler\\RunHandler\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Handler\\SetupHandler\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Handler\\TeardownHandler expects Patchlevel\\EventSourcing\\Subscription\\Engine\\Command\\Boot\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Command\\Pause\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Command\\Reactivate\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Command\\Refresh\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Command\\Remove\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Command\\Run\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Command\\Setup\|Patchlevel\\EventSourcing\\Subscription\\Engine\\Command\\Teardown, Patchlevel\\EventSourcing\\Subscription\\Engine\\Command\\Command given\.$#'
107+
identifier: argument.type
108+
count: 1
109+
path: src/Subscription/Engine/DefaultSubscriptionEngine.php
110+
105111
-
106112
message: '#^Parameter \#1 \$eventClass of method Patchlevel\\EventSourcing\\Metadata\\Event\\EventRegistry\:\:eventName\(\) expects class\-string, string given\.$#'
107113
identifier: argument.type
@@ -396,18 +402,6 @@ parameters:
396402
count: 2
397403
path: tests/Unit/QueryBus/ServiceHandlerProviderTest.php
398404

399-
-
400-
message: '#^Match expression does not handle remaining value\: string$#'
401-
identifier: match.unhandled
402-
count: 1
403-
path: tests/Unit/Subscription/Engine/DefaultSubscriptionEngineTest.php
404-
405-
-
406-
message: '#^Parameter \#1 \$error of static method Patchlevel\\EventSourcing\\Subscription\\ThrowableToErrorContextTransformer\:\:transform\(\) expects Throwable, Throwable\|null given\.$#'
407-
identifier: argument.type
408-
count: 8
409-
path: tests/Unit/Subscription/Engine/DefaultSubscriptionEngineTest.php
410-
411405
-
412406
message: '#^Offset ''args'' on array\{file\: literal\-string&non\-falsy\-string, line\: int, function\: ''createException'', class\: ''Patchlevel\\\\EventSourcing\\\\Tests\\\\Unit\\\\Subscription\\\\ErrorContextTest'', type\: ''\-\>'', args\: array\<mixed\>\} on left side of \?\? always exists and is not nullable\.$#'
413407
identifier: nullCoalesce.offset

0 commit comments

Comments
 (0)