├── 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 | --------------------------------------------------------------------------------