From 68a096cdb2f68b9d5f248e8dcd00cfbe320bad8d Mon Sep 17 00:00:00 2001 From: valtzu Date: Sat, 3 Aug 2024 16:12:30 +0300 Subject: [PATCH] Allow setting retry delay by RecoverableExceptionInterface --- UPGRADE-7.2.md | 5 +++ src/Symfony/Component/Messenger/CHANGELOG.md | 1 + .../SendFailedMessageForRetryListener.php | 7 +++- .../RecoverableExceptionInterface.php | 2 ++ .../RecoverableMessageHandlingException.php | 9 ++++++ .../SendFailedMessageForRetryListenerTest.php | 32 +++++++++++++++++++ 6 files changed, 55 insertions(+), 1 deletion(-) diff --git a/UPGRADE-7.2.md b/UPGRADE-7.2.md index 4ef726b8d8338..d4e0e156cdb81 100644 --- a/UPGRADE-7.2.md +++ b/UPGRADE-7.2.md @@ -23,6 +23,11 @@ FrameworkBundle * [BC BREAK] The `secrets:decrypt-to-local` command terminates with a non-zero exit code when a secret could not be read +Messenger +--------- + + * Add `getRetryDelay()` method to `RecoverableExceptionInterface` + Security -------- diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index e26ef63d0ab17..6af188cbb100b 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * `WrappedExceptionsInterface` now extends PHP's `Throwable` interface * Add `#[AsMessage]` attribute with `$transport` parameter for message routing * Add `--format` option to the `messenger:stats` command + * Add `getRetryDelay()` method to `RecoverableExceptionInterface` 7.1 --- diff --git a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php index 6be9b9eba4da8..f6334173972af 100644 --- a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php +++ b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php @@ -63,7 +63,12 @@ public function onMessageFailed(WorkerMessageFailedEvent $event): void ++$retryCount; - $delay = $retryStrategy->getWaitingTime($envelope, $throwable); + $delay = null; + if ($throwable instanceof RecoverableExceptionInterface && method_exists($throwable, 'getRetryDelay')) { + $delay = $throwable->getRetryDelay(); + } + + $delay ??= $retryStrategy->getWaitingTime($envelope, $throwable); $this->logger?->warning('Error thrown while handling message {class}. Sending for retry #{retryCount} using {delay} ms delay. Error: "{error}"', $context + ['retryCount' => $retryCount, 'delay' => $delay, 'error' => $throwable->getMessage(), 'exception' => $throwable]); diff --git a/src/Symfony/Component/Messenger/Exception/RecoverableExceptionInterface.php b/src/Symfony/Component/Messenger/Exception/RecoverableExceptionInterface.php index b49ddbfc2b24f..9863a01e8e1a1 100644 --- a/src/Symfony/Component/Messenger/Exception/RecoverableExceptionInterface.php +++ b/src/Symfony/Component/Messenger/Exception/RecoverableExceptionInterface.php @@ -18,6 +18,8 @@ * and the message should be retried, a handler can throw such an exception. * * @author Jérémy Derussé + * + * @method int|null getRetryDelay() The time to wait in milliseconds */ interface RecoverableExceptionInterface extends \Throwable { diff --git a/src/Symfony/Component/Messenger/Exception/RecoverableMessageHandlingException.php b/src/Symfony/Component/Messenger/Exception/RecoverableMessageHandlingException.php index 6514573411c7d..98c8391f47b7d 100644 --- a/src/Symfony/Component/Messenger/Exception/RecoverableMessageHandlingException.php +++ b/src/Symfony/Component/Messenger/Exception/RecoverableMessageHandlingException.php @@ -18,4 +18,13 @@ */ class RecoverableMessageHandlingException extends RuntimeException implements RecoverableExceptionInterface { + public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, private readonly ?int $retryDelay = null) + { + parent::__construct($message, $code, $previous); + } + + public function getRetryDelay(): ?int + { + return $this->retryDelay; + } } diff --git a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php index d45155beef159..cf3c86d7f4ffb 100644 --- a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php +++ b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php @@ -76,6 +76,38 @@ public function testRecoverableStrategyCausesRetry() $listener->onMessageFailed($event); } + public function testRecoverableExceptionRetryDelayOverridesStrategy() + { + $sender = $this->createMock(SenderInterface::class); + $sender->expects($this->once())->method('send')->willReturnCallback(function (Envelope $envelope) { + $delayStamp = $envelope->last(DelayStamp::class); + $redeliveryStamp = $envelope->last(RedeliveryStamp::class); + + $this->assertInstanceOf(DelayStamp::class, $delayStamp); + $this->assertSame(1234, $delayStamp->getDelay()); + + $this->assertInstanceOf(RedeliveryStamp::class, $redeliveryStamp); + $this->assertSame(1, $redeliveryStamp->getRetryCount()); + + return $envelope; + }); + $senderLocator = new Container(); + $senderLocator->set('my_receiver', $sender); + $retryStrategy = $this->createMock(RetryStrategyInterface::class); + $retryStrategy->expects($this->never())->method('isRetryable'); + $retryStrategy->expects($this->never())->method('getWaitingTime'); + $retryStrategyLocator = new Container(); + $retryStrategyLocator->set('my_receiver', $retryStrategy); + + $listener = new SendFailedMessageForRetryListener($senderLocator, $retryStrategyLocator); + + $exception = new RecoverableMessageHandlingException('retry', retryDelay: 1234); + $envelope = new Envelope(new \stdClass()); + $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception); + + $listener->onMessageFailed($event); + } + public function testEnvelopeIsSentToTransportOnRetry() { $exception = new \Exception('no!');