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 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 *not* declaring a `\Throwable` argument in `RetryStrategyInterface::isRetryable()`
* Deprecated *not* declaring a `\Throwable` argument in `RetryStrategyInterface::getWaitingTime()`
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 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.
* 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
-------------

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 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`
* 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
-----

View File

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

View File

@ -63,14 +63,20 @@ class MultiplierRetryStrategy implements RetryStrategyInterface
$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);
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);

View File

@ -20,10 +20,15 @@ use Symfony\Component\Messenger\Envelope;
*/
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
*/
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);
}
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);
}
}