feature #36185 [Messenger] Add a \Throwable argument in RetryStrategyInterface methods (Benjamin Dos Santos)

This PR was squashed before being merged into the 5.1-dev branch.

Discussion
----------

[Messenger] Add a \Throwable argument in RetryStrategyInterface methods

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #36182
| License       | MIT

This allows to define new retry strategies based on the exceptions thrown during the last handling.

Commits
-------

5fa9d68e8b [Messenger] Add a \Throwable argument in RetryStrategyInterface methods
This commit is contained in:
Fabien Potencier 2020-04-04 09:33:17 +02:00
commit fdd8ac5f25
7 changed files with 61 additions and 6 deletions

View File

@ -73,6 +73,8 @@ Messenger
* Deprecated Doctrine transport. It has moved to a separate package. Run `composer require symfony/doctrine-messenger` to use the new classes. * Deprecated Doctrine transport. It has moved to a separate package. Run `composer require symfony/doctrine-messenger` to use the new classes.
* Deprecated RedisExt transport. It has moved to a separate package. Run `composer require symfony/redis-messenger` to use the new classes. * Deprecated RedisExt transport. It has moved to a separate package. Run `composer require symfony/redis-messenger` to use the new classes.
* Deprecated use of invalid options in Redis and AMQP connections. * Deprecated use of invalid options in Redis and AMQP connections.
* Deprecated *not* declaring a `\Throwable` argument in `RetryStrategyInterface::isRetryable()`
* Deprecated *not* declaring a `\Throwable` argument in `RetryStrategyInterface::getWaitingTime()`
Notifier Notifier
-------- --------

View File

@ -65,6 +65,8 @@ Messenger
* Removed Doctrine transport. Run `composer require symfony/doctrine-messenger` to keep the transport in your application. * Removed Doctrine transport. Run `composer require symfony/doctrine-messenger` to keep the transport in your application.
* Removed RedisExt transport. Run `composer require symfony/redis-messenger` to keep the transport in your application. * Removed RedisExt transport. Run `composer require symfony/redis-messenger` to keep the transport in your application.
* Use of invalid options in Redis and AMQP connections now throws an error. * Use of invalid options in Redis and AMQP connections now throws an error.
* The signature of method `RetryStrategyInterface::isRetryable()` has been updated to `RetryStrategyInterface::isRetryable(Envelope $message, \Throwable $throwable = null)`.
* The signature of method `RetryStrategyInterface::getWaitingTime()` has been updated to `RetryStrategyInterface::getWaitingTime(Envelope $message, \Throwable $throwable = null)`.
PhpUnitBridge PhpUnitBridge
------------- -------------

View File

@ -7,6 +7,7 @@ CHANGELOG
* Moved AmqpExt transport to package `symfony/amqp-messenger`. All classes in `Symfony\Component\Messenger\Transport\AmqpExt` have been moved to `Symfony\Component\Messenger\Bridge\Amqp\Transport` * Moved AmqpExt transport to package `symfony/amqp-messenger`. All classes in `Symfony\Component\Messenger\Transport\AmqpExt` have been moved to `Symfony\Component\Messenger\Bridge\Amqp\Transport`
* Moved Doctrine transport to package `symfony/doctrine-messenger`. All classes in `Symfony\Component\Messenger\Transport\Doctrine` have been moved to `Symfony\Component\Messenger\Bridge\Doctrine\Transport` * Moved Doctrine transport to package `symfony/doctrine-messenger`. All classes in `Symfony\Component\Messenger\Transport\Doctrine` have been moved to `Symfony\Component\Messenger\Bridge\Doctrine\Transport`
* Moved RedisExt transport to package `symfony/redis-messenger`. All classes in `Symfony\Component\Messenger\Transport\RedisExt` have been moved to `Symfony\Component\Messenger\Bridge\Redis\Transport` * Moved RedisExt transport to package `symfony/redis-messenger`. All classes in `Symfony\Component\Messenger\Transport\RedisExt` have been moved to `Symfony\Component\Messenger\Bridge\Redis\Transport`
* Added support for passing a `\Throwable` argument to `RetryStrategyInterface` methods. This allows to define strategies based on the reason of the handling failure.
5.0.0 5.0.0
----- -----

View File

@ -58,7 +58,9 @@ class SendFailedMessageForRetryListener implements EventSubscriberInterface
$event->setForRetry(); $event->setForRetry();
++$retryCount; ++$retryCount;
$delay = $retryStrategy->getWaitingTime($envelope);
$delay = $retryStrategy->getWaitingTime($envelope, $throwable);
if (null !== $this->logger) { if (null !== $this->logger) {
$this->logger->error('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]); $this->logger->error('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]);
} }
@ -103,7 +105,7 @@ class SendFailedMessageForRetryListener implements EventSubscriberInterface
return false; return false;
} }
return $retryStrategy->isRetryable($envelope); return $retryStrategy->isRetryable($envelope, $e);
} }
private function getRetryStrategyForTransport(string $alias): ?RetryStrategyInterface private function getRetryStrategyForTransport(string $alias): ?RetryStrategyInterface

View File

@ -63,14 +63,20 @@ class MultiplierRetryStrategy implements RetryStrategyInterface
$this->maxDelayMilliseconds = $maxDelayMilliseconds; $this->maxDelayMilliseconds = $maxDelayMilliseconds;
} }
public function isRetryable(Envelope $message): bool /**
* @param \Throwable|null $throwable The cause of the failed handling
*/
public function isRetryable(Envelope $message, \Throwable $throwable = null): bool
{ {
$retries = RedeliveryStamp::getRetryCountFromEnvelope($message); $retries = RedeliveryStamp::getRetryCountFromEnvelope($message);
return $retries < $this->maxRetries; return $retries < $this->maxRetries;
} }
public function getWaitingTime(Envelope $message): int /**
* @param \Throwable|null $throwable The cause of the failed handling
*/
public function getWaitingTime(Envelope $message, \Throwable $throwable = null): int
{ {
$retries = RedeliveryStamp::getRetryCountFromEnvelope($message); $retries = RedeliveryStamp::getRetryCountFromEnvelope($message);

View File

@ -20,10 +20,15 @@ use Symfony\Component\Messenger\Envelope;
*/ */
interface RetryStrategyInterface interface RetryStrategyInterface
{ {
public function isRetryable(Envelope $message): bool; /**
* @param \Throwable|null $throwable The cause of the failed handling
*/
public function isRetryable(Envelope $message/*, \Throwable $throwable = null*/): bool;
/** /**
* @param \Throwable|null $throwable The cause of the failed handling
*
* @return int The time to delay/wait in milliseconds * @return int The time to delay/wait in milliseconds
*/ */
public function getWaitingTime(Envelope $message): int; public function getWaitingTime(Envelope $message/*, \Throwable $throwable = null*/): int;
} }

View File

@ -76,4 +76,41 @@ class SendFailedMessageForRetryListenerTest extends TestCase
$listener->onMessageFailed($event); $listener->onMessageFailed($event);
} }
public function testEnvelopeIsSentToTransportOnRetryWithExceptionPassedToRetryStrategy()
{
$exception = new \Exception('no!');
$envelope = new Envelope(new \stdClass());
$sender = $this->createMock(SenderInterface::class);
$sender->expects($this->once())->method('send')->willReturnCallback(function (Envelope $envelope) {
/** @var DelayStamp $delayStamp */
$delayStamp = $envelope->last(DelayStamp::class);
/** @var RedeliveryStamp $redeliveryStamp */
$redeliveryStamp = $envelope->last(RedeliveryStamp::class);
$this->assertInstanceOf(DelayStamp::class, $delayStamp);
$this->assertSame(1000, $delayStamp->getDelay());
$this->assertInstanceOf(RedeliveryStamp::class, $redeliveryStamp);
$this->assertSame(1, $redeliveryStamp->getRetryCount());
return $envelope;
});
$senderLocator = $this->createMock(ContainerInterface::class);
$senderLocator->expects($this->once())->method('has')->willReturn(true);
$senderLocator->expects($this->once())->method('get')->willReturn($sender);
$retryStategy = $this->createMock(RetryStrategyInterface::class);
$retryStategy->expects($this->once())->method('isRetryable')->with($envelope, $exception)->willReturn(true);
$retryStategy->expects($this->once())->method('getWaitingTime')->with($envelope, $exception)->willReturn(1000);
$retryStrategyLocator = $this->createMock(ContainerInterface::class);
$retryStrategyLocator->expects($this->once())->method('has')->willReturn(true);
$retryStrategyLocator->expects($this->once())->method('get')->willReturn($retryStategy);
$listener = new SendFailedMessageForRetryListener($senderLocator, $retryStrategyLocator);
$event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception);
$listener->onMessageFailed($event);
}
} }