Skip to content

[Messenger] Add WrappedExceptionsInterface for nested exceptions #51653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions UPGRADE-6.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Messenger
---------

* Deprecate `StopWorkerOnSignalsListener` in favor of using the `SignalableCommandInterface`
* Deprecate `HandlerFailedException::getNestedExceptions()`, `HandlerFailedException::getNestedExceptionsOfClass()` and `DelayedMessageHandlingException::getExceptions()` which are replaced by a new `getWrappedExceptions()` method

MonologBridge
-------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ protected function handleForManager(EntityManagerInterface $entityManager, Envel
if ($exception instanceof HandlerFailedException) {
// Remove all HandledStamp from the envelope so the retry will execute all handlers again.
// When a handler fails, the queries of allegedly successful previous handlers just got rolled back.
throw new HandlerFailedException($exception->getEnvelope()->withoutAll(HandledStamp::class), $exception->getNestedExceptions());
throw new HandlerFailedException($exception->getEnvelope()->withoutAll(HandledStamp::class), $exception->getWrappedExceptions());
}

throw $exception;
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Mailer/Mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function send(RawMessage $message, Envelope $envelope = null): void
try {
$this->bus->dispatch(new SendEmailMessage($message, $envelope), $stamps);
} catch (HandlerFailedException $e) {
foreach ($e->getNestedExceptions() as $nested) {
foreach ($e->getWrappedExceptions() as $nested) {
if ($nested instanceof TransportExceptionInterface) {
throw $nested;
}
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Component/Messenger/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ CHANGELOG
* Add support for multiple Redis Sentinel hosts
* Add `--all` option to the `messenger:failed:remove` command
* `RejectRedeliveredMessageException` implements `UnrecoverableExceptionInterface` in order to not be retried
* Add `WrappedExceptionsInterface` interface for exceptions that hold multiple individual exceptions
* Deprecate `HandlerFailedException::getNestedExceptions()`, `HandlerFailedException::getNestedExceptionsOfClass()`
and `DelayedMessageHandlingException::getExceptions()` which are replaced by a new `getWrappedExceptions()` method

6.3
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInt
// if ALL nested Exceptions are an instance of UnrecoverableExceptionInterface we should not retry
if ($e instanceof HandlerFailedException) {
$shouldNotRetry = true;
foreach ($e->getNestedExceptions() as $nestedException) {
foreach ($e->getWrappedExceptions() as $nestedException) {
if ($nestedException instanceof RecoverableExceptionInterface) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function onMessageFailed(WorkerMessageFailedEvent $event): void
$this->stop = true;
}
if ($th instanceof HandlerFailedException) {
foreach ($th->getNestedExceptions() as $e) {
foreach ($th->getWrappedExceptions() as $e) {
if ($e instanceof StopWorkerExceptionInterface) {
$this->stop = true;
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class DelayedMessageHandlingException extends RuntimeException
class DelayedMessageHandlingException extends RuntimeException implements WrappedExceptionsInterface
{
use WrappedExceptionsTrait;

private array $exceptions;
private Envelope $envelope;

Expand All @@ -41,11 +43,16 @@ public function __construct(array $exceptions, Envelope $envelope)

$this->exceptions = $exceptions;

parent::__construct($message, 0, $exceptions[0]);
parent::__construct($message, 0, $exceptions[array_key_first($exceptions)]);
}

/**
* @deprecated since Symfony 6.4, use {@see self::getWrappedExceptions()} instead
*/
public function getExceptions(): array
{
trigger_deprecation('symfony/messenger', '6.4', 'The "%s()" method is deprecated, use "%s::getWrappedExceptions" instead.', __METHOD__, self::class);

return $this->exceptions;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@

use Symfony\Component\Messenger\Envelope;

class HandlerFailedException extends RuntimeException
class HandlerFailedException extends RuntimeException implements WrappedExceptionsInterface
{
use WrappedExceptionsTrait;

private array $exceptions;
private Envelope $envelope;

Expand Down Expand Up @@ -46,15 +48,24 @@ public function getEnvelope(): Envelope
}

/**
* @deprecated since Symfony 6.4, use {@see self::getWrappedExceptions()} instead
*
* @return \Throwable[]
*/
public function getNestedExceptions(): array
{
trigger_deprecation('symfony/messenger', '6.4', 'The "%s()" method is deprecated, use "%s::getWrappedExceptions" instead.', __METHOD__, self::class);

return $this->exceptions;
}

/**
* @deprecated since Symfony 6.4, use {@see self::getWrappedExceptions()} instead
*/
public function getNestedExceptionOfClass(string $exceptionClassName): array
{
trigger_deprecation('symfony/messenger', '6.4', 'The "%s()" method is deprecated, use "%s::getWrappedExceptions" instead.', __METHOD__, self::class);

return array_values(
array_filter(
$this->exceptions,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?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\Exception;

/**
* Exception that holds multiple exceptions thrown by one or more handlers and/or messages.
*
* @author Jeroen <https://github.com/Jeroeny>
*/
interface WrappedExceptionsInterface
{
/**
* @return \Throwable[]
*/
public function getWrappedExceptions(string $class = null, bool $recursive = false): array;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?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\Exception;

/**
* @author Jeroen <https://github.com/Jeroeny>
*
* @internal
*/
trait WrappedExceptionsTrait
{
/**
* @return \Throwable[]
*/
public function getWrappedExceptions(string $class = null, bool $recursive = false): array
{
return $this->getWrappedExceptionsRecursively($class, $recursive, $this->exceptions);
}

/**
* @param class-string<\Throwable>|null $class
* @param iterable<\Throwable> $exceptions
*
* @return \Throwable[]
*/
private function getWrappedExceptionsRecursively(?string $class, bool $recursive, iterable $exceptions): array
{
$unwrapped = [];
foreach ($exceptions as $key => $exception) {
if ($recursive && $exception instanceof WrappedExceptionsInterface) {
$unwrapped[] = $this->getWrappedExceptionsRecursively($class, $recursive, $exception->getWrappedExceptions());

continue;
}

if ($class && !is_a($exception, $class)) {
continue;
}

$unwrapped[] = [$key => $exception];
}

return array_merge(...$unwrapped);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Tests\Fixtures\MyOwnChildException;
use Symfony\Component\Messenger\Tests\Fixtures\MyOwnException;
Expand All @@ -32,7 +33,7 @@ public function __construct()
};

$handlerException = new HandlerFailedException($envelope, [$exception]);
$originalException = $handlerException->getNestedExceptions()[0];
$originalException = $handlerException->getWrappedExceptions()[0];

$this->assertIsInt($handlerException->getCode(), 'Exception codes must converts to int');
$this->assertSame(0, $handlerException->getCode(), 'String code (HY000) converted to int must be 0');
Expand All @@ -46,7 +47,7 @@ public function testThatNestedExceptionClassAreFound()
$exception = new MyOwnException();

$handlerException = new HandlerFailedException($envelope, [new \LogicException(), $exception]);
$this->assertSame([$exception], $handlerException->getNestedExceptionOfClass(MyOwnException::class));
$this->assertSame([$exception], $handlerException->getWrappedExceptions(MyOwnException::class));
}

public function testThatNestedExceptionClassAreFoundWhenUsingChildException()
Expand All @@ -55,7 +56,7 @@ public function testThatNestedExceptionClassAreFoundWhenUsingChildException()
$exception = new MyOwnChildException();

$handlerException = new HandlerFailedException($envelope, [$exception]);
$this->assertSame([$exception], $handlerException->getNestedExceptionOfClass(MyOwnException::class));
$this->assertSame([$exception], $handlerException->getWrappedExceptions(MyOwnException::class));
}

public function testThatNestedExceptionClassAreNotFoundIfNotPresent()
Expand All @@ -64,6 +65,39 @@ public function testThatNestedExceptionClassAreNotFoundIfNotPresent()
$exception = new \LogicException();

$handlerException = new HandlerFailedException($envelope, [$exception]);
$this->assertCount(0, $handlerException->getNestedExceptionOfClass(MyOwnException::class));
$this->assertCount(0, $handlerException->getWrappedExceptions(MyOwnException::class));
}

public function testThatWrappedExceptionsRecursive()
{
$envelope = new Envelope(new \stdClass());
$exception1 = new \LogicException();
$exception2 = new MyOwnException('second');
$exception3 = new MyOwnException('third');

$handlerException = new HandlerFailedException($envelope, [$exception1, $exception2, new DelayedMessageHandlingException([$exception3])]);
$this->assertSame([$exception1, $exception2, $exception3], $handlerException->getWrappedExceptions(recursive: true));
}

public function testThatWrappedExceptionsRecursiveStringKeys()
{
$envelope = new Envelope(new \stdClass());
$exception1 = new \LogicException();
$exception2 = new MyOwnException('second');
$exception3 = new MyOwnException('third');

$handlerException = new HandlerFailedException($envelope, ['first' => $exception1, 'second' => $exception2, new DelayedMessageHandlingException(['third' => $exception3])]);
$this->assertSame(['first' => $exception1, 'second' => $exception2, 'third' => $exception3], $handlerException->getWrappedExceptions(recursive: true));
}

public function testThatWrappedExceptionsByClassRecursive()
{
$envelope = new Envelope(new \stdClass());
$exception1 = new \LogicException();
$exception2 = new MyOwnException('second');
$exception3 = new MyOwnException('third');

$handlerException = new HandlerFailedException($envelope, [$exception1, $exception2, new DelayedMessageHandlingException([$exception3])]);
$this->assertSame([$exception2, $exception3], $handlerException->getWrappedExceptions(class: MyOwnException::class, recursive: true));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public function onMessageFailed(WorkerMessageFailedEvent $event)

$throwable = $event->getThrowable();
if ($throwable instanceof HandlerFailedException) {
$throwable = $throwable->getNestedExceptions()[0];
$exceptions = $throwable->getWrappedExceptions();
$throwable = $exceptions[array_key_first($exceptions)];
}
$envelope = $event->getEnvelope();
$notification = Notification::fromThrowable($throwable)->importance(Notification::IMPORTANCE_HIGH);
Expand Down