feature #32904 [Messenger] Added ErrorDetailsStamp (TimoBakx)

This PR was merged into the 5.2-dev branch.

Discussion
----------

[Messenger] Added ErrorDetailsStamp

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | yes
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #32311
| License       | MIT
| Doc PR        | No doc changes are needed

#SymfonyHackday

This PR is part of the work started in #32341. That PR has a workaround for showing exceptions added to a previous retry. This PR stores error messages in a separate stamp, so they're more easily accessed.

I also added the exceptionClass as a separate string (independant of FlattenException), so that information is always available, even if the trace is not (due to FlattenException not being available).

Duplicated exceptions (compared to the last one) are not stored separately.

**Questions:**
- Should we limit the total amount of exceptions (remove the oldest when adding a new one)?
  - Yes, but in a new PR
- The current implementation adds this stamp in the Worker instead of the listeners to prevent duplicate code (due to the immutability of the envelope in the event). Is this the proper way to do this?
  - No, create a new listener and a way to add stamps to the envelope inside the event.
- When adding details of a `HandlerFailedException`, should we add a stamp per wrapped `Throwable`? There can be multiple errors wrapped by a single `HandlerFailedException`.
  - Yes, but in a later PR

**Todo:**
- [x] only add new information if it differs from the previous exception
- [x] add deprecations
- [x] fall back to old stamp data if no new stamp is available
- [x] rebase and retarget to master
- [x] update CHANGELOG.md
- [x] check for docs PR

**BC Breaks:**
When this PR is merged, RedeliveryStamps will no longer be populated with exception data. Any implementations that use `RedeliveryStamp::getExceptionMessage()` or `RedeliveryStamp::getFlattenedException()` will receive an empty string or `null` respectively for stamps added after this update. They should rely on `ErrorDetailsStamp` instead.

**New stuff:**
- New stamp `ErrorDetailsStamp`.
- New event subscriber `AddErrorDetailsStampListener`.
- New method `AbstractWorkerMessageEvent::addStamps`.

Commits
-------

cd27b863f9 [Messenger] Added FailedMessageErrorDetailsStamp
This commit is contained in:
Fabien Potencier 2020-10-02 07:56:41 +02:00
commit 954009a4d7
14 changed files with 419 additions and 79 deletions

View File

@ -76,6 +76,10 @@ class AmqpExtIntegrationTest extends TestCase
$this->assertEmpty(iterator_to_array($receiver->get()));
}
/**
* @group legacy
* ^ for now, deprecation errors are thrown during serialization.
*/
public function testRetryAndDelay()
{
$serializer = $this->createSerializer();

View File

@ -4,6 +4,7 @@ CHANGELOG
5.2.0
-----
* The `RedeliveryStamp` will no longer be populated with error data. This information is now stored in the `ErrorDetailsStamp` instead.
* Added `FlattenExceptionNormalizer` to give more information about the exception on Messenger background processes. The `FlattenExceptionNormalizer` has a higher priority than `ProblemNormalizer` and it is only used when the Messenger serialization context is set.
* Added factory methods to `DelayStamp`.
* Removed the exception when dispatching a message with a `DispatchAfterCurrentBusStamp` and not in a context of another dispatch call

View File

@ -15,6 +15,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Dumper;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
@ -61,7 +62,11 @@ abstract class AbstractFailedMessagesCommand extends Command
/** @var SentToFailureTransportStamp|null $sentToFailureTransportStamp */
$sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class);
$lastRedeliveryStampWithException = $this->getLastRedeliveryStampWithException($envelope);
/** @var RedeliveryStamp|null $lastRedeliveryStamp */
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);
$lastRedeliveryStampWithException = $this->getLastRedeliveryStampWithException($envelope, true);
$rows = [
['Class', \get_class($envelope->getMessage())],
@ -71,14 +76,35 @@ abstract class AbstractFailedMessagesCommand extends Command
$rows[] = ['Message Id', $id];
}
$flattenException = null === $lastRedeliveryStampWithException ? null : $lastRedeliveryStampWithException->getFlattenException();
if (null === $sentToFailureTransportStamp) {
$io->warning('Message does not appear to have been sent to this transport after failing');
} else {
$failedAt = '';
$errorMessage = '';
$errorCode = '';
$errorClass = '(unknown)';
if (null !== $lastRedeliveryStamp) {
$failedAt = $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s');
}
if (null !== $lastErrorDetailsStamp) {
$errorMessage = $lastErrorDetailsStamp->getExceptionMessage();
$errorCode = $lastErrorDetailsStamp->getExceptionCode();
$errorClass = $lastErrorDetailsStamp->getExceptionClass();
} elseif (null !== $lastRedeliveryStampWithException) {
// Try reading the errorMessage for messages that are still in the queue without the new ErrorDetailStamps.
$errorMessage = $lastRedeliveryStampWithException->getExceptionMessage();
if (null !== $lastRedeliveryStampWithException->getFlattenException()) {
$errorClass = $lastRedeliveryStampWithException->getFlattenException()->getClass();
}
}
$rows = array_merge($rows, [
['Failed at', null === $lastRedeliveryStampWithException ? '' : $lastRedeliveryStampWithException->getRedeliveredAt()->format('Y-m-d H:i:s')],
['Error', null === $lastRedeliveryStampWithException ? '' : $lastRedeliveryStampWithException->getExceptionMessage()],
['Error Class', null === $flattenException ? '(unknown)' : $flattenException->getClass()],
['Failed at', $failedAt],
['Error', $errorMessage],
['Error Code', $errorCode],
['Error Class', $errorClass],
['Transport', $sentToFailureTransportStamp->getOriginalReceiverName()],
]);
}
@ -98,6 +124,12 @@ abstract class AbstractFailedMessagesCommand extends Command
$dump = new Dumper($io);
$io->writeln($dump($envelope->getMessage()));
$io->title('Exception:');
$flattenException = null;
if (null !== $lastErrorDetailsStamp) {
$flattenException = $lastErrorDetailsStamp->getFlattenException();
} elseif (null !== $lastRedeliveryStampWithException) {
$flattenException = $lastRedeliveryStampWithException->getFlattenException();
}
$io->writeln(null === $flattenException ? '(no data)' : $flattenException->getTraceAsString());
} else {
$io->writeln(' Re-run command with <info>-vv</info> to see more message & error details.');
@ -122,6 +154,23 @@ abstract class AbstractFailedMessagesCommand extends Command
protected function getLastRedeliveryStampWithException(Envelope $envelope): ?RedeliveryStamp
{
if (null === \func_get_args()[1]) {
trigger_deprecation(
'symfony/messenger',
'5.2',
sprintf(
'Using the "getLastRedeliveryStampWithException" method in the "%s" class is deprecated, use the "Envelope::last(%s)" instead.',
self::class,
ErrorDetailsStamp::class
)
);
}
// Use ErrorDetailsStamp instead if it is available
if (null !== $envelope->last(ErrorDetailsStamp::class)) {
return null;
}
/** @var RedeliveryStamp $stamp */
foreach (array_reverse($envelope->all(RedeliveryStamp::class)) as $stamp) {
if (null !== $stamp->getExceptionMessage()) {

View File

@ -18,6 +18,8 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
/**
@ -82,13 +84,25 @@ EOF
$rows = [];
foreach ($envelopes as $envelope) {
$lastRedeliveryStampWithException = $this->getLastRedeliveryStampWithException($envelope);
/** @var RedeliveryStamp|null $lastRedeliveryStamp */
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);
$lastRedeliveryStampWithException = $this->getLastRedeliveryStampWithException($envelope, true);
$errorMessage = '';
if (null !== $lastErrorDetailsStamp) {
$errorMessage = $lastErrorDetailsStamp->getExceptionMessage();
} elseif (null !== $lastRedeliveryStampWithException) {
// Try reading the errorMessage for messages that are still in the queue without the new ErrorDetailStamps.
$errorMessage = $lastRedeliveryStampWithException->getExceptionMessage();
}
$rows[] = [
$this->getMessageId($envelope),
\get_class($envelope->getMessage()),
null === $lastRedeliveryStampWithException ? '' : $lastRedeliveryStampWithException->getRedeliveredAt()->format('Y-m-d H:i:s'),
null === $lastRedeliveryStampWithException ? '' : $lastRedeliveryStampWithException->getExceptionMessage(),
null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'),
$errorMessage,
];
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
final class AddErrorDetailsStampListener implements EventSubscriberInterface
{
public function onMessageFailed(WorkerMessageFailedEvent $event): void
{
$stamp = new ErrorDetailsStamp($event->getThrowable());
$previousStamp = $event->getEnvelope()->last(ErrorDetailsStamp::class);
// Do not append duplicate information
if (null === $previousStamp || !$previousStamp->equals($stamp)) {
$event->addStamps($stamp);
}
}
public static function getSubscribedEvents(): array
{
return [
// must have higher priority than SendFailedMessageForRetryListener
WorkerMessageFailedEvent::class => ['onMessageFailed', 200],
];
}
}

View File

@ -11,10 +11,8 @@
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Stamp\DelayStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
@ -49,16 +47,10 @@ class SendFailedMessageToFailureTransportListener implements EventSubscriberInte
return;
}
$throwable = $event->getThrowable();
if ($throwable instanceof HandlerFailedException) {
$throwable = $throwable->getNestedExceptions()[0];
}
$flattenedException = class_exists(FlattenException::class) ? FlattenException::createFromThrowable($throwable) : null;
$envelope = $envelope->with(
new SentToFailureTransportStamp($event->getReceiverName()),
new DelayStamp(0),
new RedeliveryStamp(0, $throwable->getMessage(), $flattenedException)
new RedeliveryStamp(0)
);
if (null !== $this->logger) {

View File

@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Throwable;
/**
* Stamp applied when a messages fails due to an exception in the handler.
*/
final class ErrorDetailsStamp implements StampInterface
{
/** @var string */
private $exceptionClass;
/** @var int|mixed */
private $exceptionCode;
/** @var string */
private $exceptionMessage;
/** @var FlattenException|null */
private $flattenException;
public function __construct(Throwable $throwable)
{
if ($throwable instanceof HandlerFailedException) {
$throwable = $throwable->getPrevious();
}
$this->exceptionClass = \get_class($throwable);
$this->exceptionCode = $throwable->getCode();
$this->exceptionMessage = $throwable->getMessage();
if (class_exists(FlattenException::class)) {
$this->flattenException = FlattenException::createFromThrowable($throwable);
}
}
public function getExceptionClass(): string
{
return $this->exceptionClass;
}
public function getExceptionCode()
{
return $this->exceptionCode;
}
public function getExceptionMessage(): string
{
return $this->exceptionMessage;
}
public function getFlattenException(): ?FlattenException
{
return $this->flattenException;
}
public function equals(?self $that): bool
{
if (null === $that) {
return false;
}
if ($this->flattenException && $that->flattenException) {
return $this->flattenException->getClass() === $that->flattenException->getClass()
&& $this->flattenException->getCode() === $that->flattenException->getCode()
&& $this->flattenException->getMessage() === $that->flattenException->getMessage();
}
return $this->exceptionClass === $that->exceptionClass
&& $this->exceptionCode === $that->exceptionCode
&& $this->exceptionMessage === $that->exceptionMessage;
}
}

View File

@ -27,9 +27,33 @@ final class RedeliveryStamp implements StampInterface
public function __construct(int $retryCount, string $exceptionMessage = null, FlattenException $flattenException = null, \DateTimeInterface $redeliveredAt = null)
{
$this->retryCount = $retryCount;
$this->exceptionMessage = $exceptionMessage;
$this->flattenException = $flattenException;
$this->redeliveredAt = $redeliveredAt ?? new \DateTimeImmutable();
if (null !== $exceptionMessage) {
trigger_deprecation(
'symfony/messenger',
'5.2',
sprintf(
'Using the "$exceptionMessage" parameter in the "%s" class is deprecated, use the "%s" class instead.',
self::class,
ErrorDetailsStamp::class
)
);
}
$this->exceptionMessage = $exceptionMessage;
if (null !== $flattenException) {
trigger_deprecation(
'symfony/messenger',
'5.2',
sprintf(
'Using the "$flattenException" parameter in the "%s" class is deprecated, use the "%s" class instead.',
self::class,
ErrorDetailsStamp::class
)
);
}
$this->flattenException = $flattenException;
}
public static function getRetryCountFromEnvelope(Envelope $envelope): int
@ -45,13 +69,39 @@ final class RedeliveryStamp implements StampInterface
return $this->retryCount;
}
/**
* @deprecated since Symfony 5.2, use ErrorDetailsStamp instead.
*/
public function getExceptionMessage(): ?string
{
trigger_deprecation(
'symfony/messenger',
'5.2',
sprintf(
'Using the "getExceptionMessage()" method of the "%s" class is deprecated, use the "%s" class instead.',
self::class,
ErrorDetailsStamp::class
)
);
return $this->exceptionMessage;
}
/**
* @deprecated since Symfony 5.2, use ErrorDetailsStamp instead.
*/
public function getFlattenException(): ?FlattenException
{
trigger_deprecation(
'symfony/messenger',
'5.2',
sprintf(
'Using the "getFlattenException()" method of the "%s" class is deprecated, use the "%s" class instead.',
self::class,
ErrorDetailsStamp::class
)
);
return $this->flattenException;
}

View File

@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Messenger\Command\FailedMessagesShowCommand;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
@ -29,11 +30,13 @@ class FailedMessagesShowCommandTest extends TestCase
public function testBasicRun()
{
$sentToFailureStamp = new SentToFailureTransportStamp('async');
$redeliveryStamp = new RedeliveryStamp(0, 'Things are bad!');
$redeliveryStamp = new RedeliveryStamp(0);
$errorStamp = new ErrorDetailsStamp(new \Exception('Things are bad!', 123));
$envelope = new Envelope(new \stdClass(), [
new TransportMessageIdStamp(15),
$sentToFailureStamp,
$redeliveryStamp,
$errorStamp,
]);
$receiver = $this->createMock(ListableReceiverInterface::class);
$receiver->expects($this->once())->method('find')->with(15)->willReturn($envelope);
@ -52,8 +55,9 @@ class FailedMessagesShowCommandTest extends TestCase
Message Id 15
Failed at %s
Error Things are bad!
Error Class (unknown)
Transport async
Error Code 123
Error Class Exception
Transport async
EOF
,
$redeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s')),
@ -63,39 +67,74 @@ EOF
public function testMultipleRedeliveryFails()
{
$sentToFailureStamp = new SentToFailureTransportStamp('async');
$redeliveryStamp1 = new RedeliveryStamp(0, 'Things are bad!');
$redeliveryStamp1 = new RedeliveryStamp(0);
$errorStamp = new ErrorDetailsStamp(new \Exception('Things are bad!', 123));
$redeliveryStamp2 = new RedeliveryStamp(0);
$envelope = new Envelope(new \stdClass(), [
new TransportMessageIdStamp(15),
$sentToFailureStamp,
$redeliveryStamp1,
$errorStamp,
$redeliveryStamp2,
]);
$receiver = $this->createMock(ListableReceiverInterface::class);
$receiver->expects($this->once())->method('find')->with(15)->willReturn($envelope);
$command = new FailedMessagesShowCommand(
'failure_receiver',
$receiver
);
$tester = new CommandTester($command);
$tester->execute(['id' => 15]);
$this->assertStringContainsString(sprintf(<<<EOF
------------- ---------------------
Class stdClass
Message Id 15
Failed at %s
Error Things are bad!
Error Class (unknown)
Transport async
Error Code 123
Error Class Exception
Transport async
EOF
,
$redeliveryStamp2->getRedeliveredAt()->format('Y-m-d H:i:s')),
$tester->getDisplay(true));
}
/**
* @group legacy
*/
public function testLegacyFallback()
{
$sentToFailureStamp = new SentToFailureTransportStamp('async');
$redeliveryStamp = new RedeliveryStamp(0, 'Things are bad!');
$envelope = new Envelope(new \stdClass(), [
new TransportMessageIdStamp(15),
$sentToFailureStamp,
$redeliveryStamp,
]);
$receiver = $this->createMock(ListableReceiverInterface::class);
$receiver->expects($this->once())->method('find')->with(15)->willReturn($envelope);
$command = new FailedMessagesShowCommand(
'failure_receiver',
$receiver
);
$tester = new CommandTester($command);
$tester->execute(['id' => 15]);
$this->assertStringContainsString(sprintf(<<<EOF
------------- ---------------------
Class stdClass
Message Id 15
Failed at %s
Error Things are bad!
Error Code
Error Class (unknown)
Transport async
EOF
,
$redeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s')),
$tester->getDisplay(true));
}
public function testReceiverShouldBeListable()
{
$receiver = $this->createMock(ReceiverInterface::class);
@ -113,11 +152,13 @@ EOF
public function testListMessages()
{
$sentToFailureStamp = new SentToFailureTransportStamp('async');
$redeliveryStamp = new RedeliveryStamp(0, 'Things are bad!');
$redeliveryStamp = new RedeliveryStamp(0);
$errorStamp = new ErrorDetailsStamp(new \RuntimeException('Things are bad!'));
$envelope = new Envelope(new \stdClass(), [
new TransportMessageIdStamp(15),
$sentToFailureStamp,
$redeliveryStamp,
$errorStamp,
]);
$receiver = $this->createMock(ListableReceiverInterface::class);
$receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]);
@ -130,7 +171,7 @@ EOF
$tester = new CommandTester($command);
$tester->execute([]);
$this->assertStringContainsString(sprintf(<<<EOF
15 stdClass %s Things are bad!
15 stdClass %s Things are bad!
EOF
,
$redeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s')),
@ -158,7 +199,8 @@ EOF
$envelope = new Envelope(new \stdClass(), [
new TransportMessageIdStamp(15),
$sentToFailureStamp,
new RedeliveryStamp(0, 'Things are bad!'),
new RedeliveryStamp(0),
new ErrorDetailsStamp(new \RuntimeException('Things are bad!')),
]);
$receiver = $this->createMock(ListableReceiverInterface::class);
$receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]);

View File

@ -0,0 +1,56 @@
<?php
namespace Symfony\Component\Messenger\Tests\EventListener;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\EventListener\AddErrorDetailsStampListener;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
final class AddErrorDetailsStampListenerTest extends TestCase
{
public function testExceptionDetailsAreAdded(): void
{
$listener = new AddErrorDetailsStampListener();
$envelope = new Envelope(new \stdClass());
$exception = new \Exception('It failed!');
$event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception);
$expectedStamp = new ErrorDetailsStamp($exception);
$listener->onMessageFailed($event);
$this->assertEquals($expectedStamp, $event->getEnvelope()->last(ErrorDetailsStamp::class));
}
public function testWorkerAddsNewErrorDetailsStampOnFailure()
{
$listener = new AddErrorDetailsStampListener();
$envelope = new Envelope(new \stdClass(), [
new ErrorDetailsStamp(new \InvalidArgumentException('First error!')),
]);
$exception = new \Exception('Second error!');
$event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception);
$expectedStamp = new ErrorDetailsStamp($exception);
$listener->onMessageFailed($event);
$this->assertEquals($expectedStamp, $event->getEnvelope()->last(ErrorDetailsStamp::class));
$this->assertCount(2, $event->getEnvelope()->all(ErrorDetailsStamp::class));
}
public function testWorkerDoesNotAddDuplicateErrorDetailsStampOnFailure()
{
$listener = new AddErrorDetailsStampListener();
$envelope = new Envelope(new \stdClass(), [new \Exception('It failed!')]);
$event = new WorkerMessageFailedEvent($envelope, 'my_receiver', new \Exception('It failed!'));
$listener->onMessageFailed($event);
$this->assertCount(1, $event->getEnvelope()->all(ErrorDetailsStamp::class));
}
}

View File

@ -15,8 +15,6 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\EventListener\SendFailedMessageToFailureTransportListener;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Transport\Sender\SenderInterface;
@ -34,11 +32,6 @@ class SendFailedMessageToFailureTransportListenerTest extends TestCase
$this->assertNotNull($sentToFailureTransportStamp);
$this->assertSame('my_receiver', $sentToFailureTransportStamp->getOriginalReceiverName());
/** @var RedeliveryStamp $redeliveryStamp */
$redeliveryStamp = $envelope->last(RedeliveryStamp::class);
$this->assertSame('no!', $redeliveryStamp->getExceptionMessage());
$this->assertSame('no!', $redeliveryStamp->getFlattenException()->getMessage());
return true;
}))->willReturnArgument(0);
$listener = new SendFailedMessageToFailureTransportListener($sender);
@ -50,30 +43,6 @@ class SendFailedMessageToFailureTransportListenerTest extends TestCase
$listener->onMessageFailed($event);
}
public function testItGetsNestedHandlerFailedException()
{
$sender = $this->createMock(SenderInterface::class);
$sender->expects($this->once())->method('send')->with($this->callback(function ($envelope) {
/** @var Envelope $envelope */
/** @var RedeliveryStamp $redeliveryStamp */
$redeliveryStamp = $envelope->last(RedeliveryStamp::class);
$this->assertNotNull($redeliveryStamp);
$this->assertSame('I am inside!', $redeliveryStamp->getExceptionMessage());
$this->assertSame('Exception', $redeliveryStamp->getFlattenException()->getClass());
return true;
}))->willReturnArgument(0);
$listener = new SendFailedMessageToFailureTransportListener($sender);
$envelope = new Envelope(new \stdClass());
$exception = new \Exception('I am inside!');
$exception = new HandlerFailedException($envelope, [$exception]);
$event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception);
$listener->onMessageFailed($event);
}
public function testDoNothingOnRetry()
{
$sender = $this->createMock(SenderInterface::class);

View File

@ -16,6 +16,7 @@ use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\EventListener\AddErrorDetailsStampListener;
use Symfony\Component\Messenger\EventListener\SendFailedMessageForRetryListener;
use Symfony\Component\Messenger\EventListener\SendFailedMessageToFailureTransportListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
@ -27,7 +28,7 @@ use Symfony\Component\Messenger\Middleware\FailedMessageProcessingMiddleware;
use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
use Symfony\Component\Messenger\Middleware\SendMessageMiddleware;
use Symfony\Component\Messenger\Retry\MultiplierRetryStrategy;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
@ -37,7 +38,7 @@ use Symfony\Component\Messenger\Worker;
class FailureIntegrationTest extends TestCase
{
public function testRequeMechanism()
public function testRequeueMechanism()
{
$transport1 = new DummyFailureTestSenderAndReceiver();
$transport2 = new DummyFailureTestSenderAndReceiver();
@ -96,6 +97,7 @@ class FailureIntegrationTest extends TestCase
new SendMessageMiddleware($senderLocator),
new HandleMessageMiddleware($handlerLocator),
]);
$dispatcher->addSubscriber(new AddErrorDetailsStampListener());
$dispatcher->addSubscriber(new SendFailedMessageForRetryListener($locator, $retryStrategyLocator));
$dispatcher->addSubscriber(new SendFailedMessageToFailureTransportListener($failureTransport));
$dispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(1));
@ -156,10 +158,10 @@ class FailureIntegrationTest extends TestCase
/** @var SentToFailureTransportStamp $sentToFailureStamp */
$sentToFailureStamp = $failedEnvelope->last(SentToFailureTransportStamp::class);
$this->assertNotNull($sentToFailureStamp);
/** @var RedeliveryStamp $redeliveryStamp */
$redeliveryStamp = $failedEnvelope->last(RedeliveryStamp::class);
$this->assertNotNull($redeliveryStamp);
$this->assertSame('Failure from call 2', $redeliveryStamp->getExceptionMessage());
/** @var ErrorDetailsStamp $errorDetailsStamp */
$errorDetailsStamp = $failedEnvelope->last(ErrorDetailsStamp::class);
$this->assertNotNull($errorDetailsStamp);
$this->assertSame('Failure from call 2', $errorDetailsStamp->getExceptionMessage());
/*
* Failed message is handled, fails, and sent for a retry

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Messenger\Tests\Stamp;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
class ErrorDetailsStampTest extends TestCase
{
public function testGetters(): void
{
$exception = new \Exception('exception message');
$flattenException = FlattenException::createFromThrowable($exception);
$stamp = new ErrorDetailsStamp($exception);
$this->assertSame(\Exception::class, $stamp->getExceptionClass());
$this->assertSame('exception message', $stamp->getExceptionMessage());
$this->assertEquals($flattenException, $stamp->getFlattenException());
}
public function testUnwrappingHandlerFailedException(): void
{
$wrappedException = new \Exception('I am inside', 123);
$envelope = new Envelope(new \stdClass());
$exception = new HandlerFailedException($envelope, [$wrappedException]);
$flattenException = FlattenException::createFromThrowable($wrappedException);
$stamp = new ErrorDetailsStamp($exception);
$this->assertSame(\Exception::class, $stamp->getExceptionClass());
$this->assertSame('I am inside', $stamp->getExceptionMessage());
$this->assertSame(123, $stamp->getExceptionCode());
$this->assertEquals($flattenException, $stamp->getFlattenException());
}
}

View File

@ -12,7 +12,6 @@
namespace Symfony\Component\Messenger\Tests\Stamp;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
class RedeliveryStampTest extends TestCase
@ -22,16 +21,6 @@ class RedeliveryStampTest extends TestCase
$stamp = new RedeliveryStamp(10);
$this->assertSame(10, $stamp->getRetryCount());
$this->assertInstanceOf(\DateTimeInterface::class, $stamp->getRedeliveredAt());
$this->assertNull($stamp->getExceptionMessage());
$this->assertNull($stamp->getFlattenException());
}
public function testGettersPopulated()
{
$flattenException = new FlattenException();
$stamp = new RedeliveryStamp(10, 'exception message', $flattenException);
$this->assertSame('exception message', $stamp->getExceptionMessage());
$this->assertSame($flattenException, $stamp->getFlattenException());
}
public function testSerialization()