├── phpstan.neon.dist
├── .php-cs-fixer.dist.php
├── psalm.xml
├── phpunit.xml.dist
├── composer.json
├── LICENSE
├── src
├── SymfonyBusDriver.php
└── ExceptionLogger.php
└── Readme.md
/phpstan.neon.dist:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 5
3 | paths:
4 | - ./src
5 | excludePaths:
6 | - ./vendor/
7 | - ./tests/
8 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | in(__DIR__.'/src')
5 | ->in(__DIR__.'/tests')
6 | ;
7 |
8 | return (new PhpCsFixer\Config())
9 | ->setRules([
10 | '@Symfony' => true,
11 | ])
12 | ->setFinder($finder)
13 | ;
14 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ./tests
21 |
22 |
23 |
24 |
25 |
26 | ./
27 |
28 | vendor
29 | tests
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "happyr/bref-messenger-failure-strategies",
3 | "description": "Make sure you can use Bref Symfony Messenger failure strategies",
4 | "type": "library",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Tobias Nyholm",
9 | "email": "tobias.nyholm@gmail.com"
10 | }
11 | ],
12 | "require": {
13 | "php": ">=7.3",
14 | "bref/symfony-messenger": "^0.3 || ^0.4 || ^0.5 || ^1.0",
15 | "symfony/messenger": "^4.4 || ^5.0 || ^6.0 || ^7.0"
16 | },
17 | "require-dev": {
18 | "phpunit/phpunit": "^8.5.31",
19 | "symfony/validator": "^5.0 || ^6.0 || ^7.0",
20 | "symfony/polyfill-php80": "^1.17",
21 | "symfony/event-dispatcher-contracts": "^2.0"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "Happyr\\BrefMessenger\\": "src/"
26 | }
27 | },
28 | "autoload-dev": {
29 | "psr-4": {
30 | "Happyr\\BrefMessenger\\Test\\": "tests/"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Tobias Nyholm
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/SymfonyBusDriver.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
29 | $this->eventDispatcher = $eventDispatcher;
30 | }
31 |
32 | public function putEnvelopeOnBus(MessageBusInterface $bus, Envelope $envelope, string $transportName): void
33 | {
34 | $event = new WorkerMessageReceivedEvent($envelope, $transportName);
35 | $this->eventDispatcher->dispatch($event);
36 |
37 | if (!$event->shouldHandle()) {
38 | return;
39 | }
40 |
41 | try {
42 | $envelope = $bus->dispatch($envelope->with(new ReceivedStamp($transportName), new ConsumedByWorkerStamp()));
43 | } catch (\Throwable $throwable) {
44 | $this->eventDispatcher->dispatch(new WorkerMessageFailedEvent($envelope, $transportName, $throwable));
45 |
46 | return;
47 | }
48 |
49 | $this->eventDispatcher->dispatch(new WorkerMessageHandledEvent($envelope, $transportName));
50 |
51 | $message = $envelope->getMessage();
52 | $this->logger->info('{class} was handled successfully (acknowledging to transport).', [
53 | 'message' => $message,
54 | 'transport' => $transportName,
55 | 'class' => \get_class($message),
56 | ]);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/ExceptionLogger.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
20 | }
21 |
22 | public static function getSubscribedEvents()
23 | {
24 | return [
25 | WorkerMessageFailedEvent::class => ['onException', 20],
26 | ];
27 | }
28 |
29 | public function onException(WorkerMessageFailedEvent $event): void
30 | {
31 | $envelope = $event->getEnvelope();
32 | $throwable = $event->getThrowable();
33 | $firstNestedException = null;
34 | if ($throwable instanceof HandlerFailedException) {
35 | $envelope = $throwable->getEnvelope();
36 | $nestedExceptions = method_exists($throwable, 'getNestedExceptions') ? $throwable->getNestedExceptions() : $throwable->getWrappedExceptions();
37 | $firstNestedException = $nestedExceptions[array_key_first($nestedExceptions)];
38 | }
39 |
40 | if ($throwable instanceof ValidationFailedException) {
41 | $this->logValidationException($throwable);
42 | } else {
43 | $this->logException($envelope, $throwable, $event->getReceiverName(), $firstNestedException);
44 | }
45 | }
46 |
47 | private function logValidationException(ValidationFailedException $exception): void
48 | {
49 | $violations = $exception->getViolations();
50 | $violationMessages = [];
51 | /** @var ConstraintViolationInterface $v */
52 | foreach ($violations as $v) {
53 | $violationMessages[] = \sprintf('%s: %s', $v->getPropertyPath(), (string) $v->getMessage());
54 | }
55 |
56 | $this->logger->error('{class} did failed validation.', [
57 | 'class' => get_class($exception->getViolatingMessage()),
58 | 'violations' => \json_encode($violationMessages),
59 | ]);
60 | }
61 |
62 | private function logException(Envelope $envelope, \Throwable $throwable, string $transportName, ?\Throwable $firstNestedException): void
63 | {
64 | $message = $envelope->getMessage();
65 | $context = [
66 | 'exception' => $throwable,
67 | 'message' => $message,
68 | 'transport' => $transportName,
69 | 'class' => \get_class($message),
70 | ];
71 |
72 | if (null === $firstNestedException) {
73 | $logMessage = 'Dispatching {class} caused an exception: '.$throwable->getMessage();
74 | } else {
75 | $logMessage = 'Handling {class} caused an HandlerFailedException: '.$throwable->getMessage();
76 | $context['first_exception'] = $firstNestedException;
77 | }
78 |
79 | $this->logger->error($logMessage, $context);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Bref Messenger failure strategies
2 |
3 | So you have fallen in love with [Bref](https://bref.sh) and you really want to use
4 | Symfony's excellent Messenger component. You've probably also installed the
5 | [Bref Symfony Messenger bundle](https://github.com/brefphp/symfony-messenger)
6 | that allows you to publish messages on SQS and SNS etc. But you are missing something...
7 | You want to be able to use Symfony Messenger retry strategies, right?
8 |
9 | This is the package for you!
10 |
11 | ## Install
12 |
13 | ```cli
14 | composer require happyr/bref-messenger-failure-strategies
15 | ```
16 |
17 | Now you have a class called `Happyr\BrefMessenger\SymfonyBusDriver` that implements
18 | `Bref\Symfony\Messenger\Service\BusDriver`. Feel free to configure your consumers with this
19 | new class.
20 |
21 | ## Example
22 |
23 | On each consumer you can choose to let Symfony handle failures as described in
24 | [the documentation](https://symfony.com/doc/current/messenger.html#retries-failures).
25 |
26 |
27 | ```yaml
28 | # config/packages/messenger.yaml
29 |
30 | framework:
31 | messenger:
32 | failure_transport: failed
33 | transports:
34 | failed: 'doctrine://default?queue_name=failed'
35 | workqueue:
36 | dsn: 'https://sqs.us-east-1.amazonaws.com/123456789/my-queue'
37 | retry_strategy:
38 | max_retries: 3
39 | # milliseconds delay
40 | delay: 1000
41 | multiplier: 2
42 | max_delay: 60
43 |
44 | services:
45 | Happyr\BrefMessenger\ExceptionLogger:
46 | autowire: true
47 | autoconfigure: true
48 |
49 | Happyr\BrefMessenger\SymfonyBusDriver:
50 | autowire: true
51 |
52 | Bref\Symfony\Messenger\Service\Sqs\SqsConsumer:
53 | arguments:
54 | - '@Happyr\BrefMessenger\SymfonyBusDriver'
55 | - '@messenger.routable_message_bus'
56 | - '@Symfony\Component\Messenger\Transport\Serialization\SerializerInterface'
57 | - 'my_sqs' # Same as transport name
58 |
59 | # ...
60 |
61 | ```
62 |
63 | The delay is only supported on SQS "normal queue". If you are using SNS or SQS FIFO
64 | you should use the failure queue directly.
65 |
66 | ```yaml
67 | # config/packages/messenger.yaml
68 |
69 | framework:
70 | messenger:
71 | failure_transport: failed
72 | transports:
73 | failed: 'doctrine://default?queue_name=failed'
74 | workqueue:
75 | dsn: 'sns://arn:aws:sns:us-east-1:1234567890:foobar'
76 | retry_strategy:
77 | max_retries: 0
78 | services:
79 | # ...
80 |
81 | ```
82 |
83 | Make sure you re-run the failure queue time to time. The following config will
84 | run a script for 5 seconds every 30 minutes. It will run for 5 seconds even though
85 | no messages has failed.
86 |
87 | ```yaml
88 | # serverless.yml
89 |
90 | functions:
91 | website:
92 | # ...
93 | consumer:
94 | # ...
95 |
96 | console:
97 | handler: bin/console
98 | Timeout: 120 # in seconds
99 | layers:
100 | - ${bref:layer.php-74}
101 | - ${bref:layer.console}
102 | events:
103 | - schedule:
104 | rate: rate(30 minutes)
105 | input:
106 | cli: messenger:consume failed --time-limit=5 --limit=50
107 |
108 | ```
109 |
--------------------------------------------------------------------------------