Skip to content

Commit d6d37dc

Browse files
authored
Allow Error Handler to execute asynchronously (#490)
* Make error message serializable * allow to handle error messages asynchronously * fixes * fixes * fixes
1 parent 6046242 commit d6d37dc

File tree

11 files changed

+192
-51
lines changed

11 files changed

+192
-51
lines changed

packages/Dbal/tests/Integration/DbalErrorChannelCommandBusTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Ecotone\Messaging\Config\ModulePackageList;
1313
use Ecotone\Messaging\Config\ServiceConfiguration;
1414
use Ecotone\Messaging\Endpoint\ExecutionPollingMetadata;
15+
use Ecotone\Messaging\Handler\MessageHandlingException;
1516
use Ecotone\Messaging\Handler\Recoverability\ErrorContext;
1617
use Ecotone\Modelling\Attribute\InstantRetry;
1718
use Ecotone\Test\LicenceTesting;
@@ -189,7 +190,7 @@ public function test_it_fails_on_using_asynchronous_retry_on_synchronous_dead_le
189190
$exception = false;
190191
try {
191192
$commandBus->sendWithRouting('order.place', 'coffee');
192-
} catch (InvalidArgumentException) {
193+
} catch (MessageHandlingException) {
193194
$exception = true;
194195
}
195196

packages/Ecotone/src/Messaging/Config/Annotation/ModuleConfiguration/ErrorHandlerModule.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use Ecotone\Messaging\Handler\InterfaceToCallRegistry;
1717
use Ecotone\Messaging\Handler\Logger\LoggingGateway;
1818
use Ecotone\Messaging\Handler\Processor\MethodInvoker\Converter\ReferenceBuilder;
19-
use Ecotone\Messaging\Handler\Recoverability\ErrorHandler;
19+
use Ecotone\Messaging\Handler\Recoverability\DelayedRetryErrorHandler;
2020
use Ecotone\Messaging\Handler\Recoverability\ErrorHandlerConfiguration;
2121
use Ecotone\Messaging\Handler\Router\HeaderRouter;
2222
use Ecotone\Messaging\Handler\Router\RouterBuilder;
@@ -57,7 +57,7 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO
5757
}
5858

5959
$errorHandler = ServiceActivatorBuilder::createWithDefinition(
60-
new Definition(ErrorHandler::class, [
60+
new Definition(DelayedRetryErrorHandler::class, [
6161
$extensionObject->getDelayedRetryTemplate(),
6262
(bool)$extensionObject->getDeadLetterQueueChannel(),
6363
Reference::to(LoggingGateway::class),

packages/Ecotone/src/Messaging/Handler/Gateway/GatewayInternalProcessor.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Ecotone\Messaging\Channel\QueueChannel;
66
use Ecotone\Messaging\Future;
7+
use Ecotone\Messaging\Handler\MessageHandlingException;
78
use Ecotone\Messaging\Handler\MessageProcessor;
89
use Ecotone\Messaging\Handler\Processor\MethodInvoker\AroundInterceptable;
910
use Ecotone\Messaging\Handler\Type;
@@ -104,7 +105,10 @@ private function getReply(PollableChannel $replyChannel): callable
104105
throw InvalidArgumentException::create("{$this->interfaceToCallName} expects value, but null was returned. Have you consider changing return value to nullable?");
105106
}
106107
if ($replyMessage instanceof ErrorMessage) {
107-
throw $replyMessage->getException();
108+
throw MessageHandlingException::create(
109+
$replyMessage->getExceptionMessage(),
110+
$replyMessage->getExceptionCode(),
111+
);
108112
}
109113

110114
return $replyMessage;

packages/Ecotone/src/Messaging/Handler/Processor/MethodInvoker/Converter/MessageConverter.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
namespace Ecotone\Messaging\Handler\Processor\MethodInvoker\Converter;
66

77
use Ecotone\Messaging\Handler\ParameterConverter;
8+
use Ecotone\Messaging\Handler\Recoverability\ErrorContext;
89
use Ecotone\Messaging\Message;
10+
use Ecotone\Messaging\Support\ErrorMessage;
911

1012
/**
1113
* Class MessageArgument
@@ -28,6 +30,10 @@ public static function create(): self
2830
*/
2931
public function getArgumentFrom(Message $message): Message
3032
{
33+
if (ErrorMessage::isErrorMessage($message)) {
34+
return ErrorMessage::createFromMessage($message);
35+
}
36+
3137
return $message;
3238
}
3339
}

packages/Ecotone/src/Messaging/Handler/Recoverability/ErrorHandler.php renamed to packages/Ecotone/src/Messaging/Handler/Recoverability/DelayedRetryErrorHandler.php

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,19 @@
77
use Ecotone\Messaging\Attribute\Parameter\Reference;
88
use Ecotone\Messaging\Handler\ChannelResolver;
99
use Ecotone\Messaging\Handler\Logger\LoggingGateway;
10+
use Ecotone\Messaging\Handler\MessageHandlingException;
1011
use Ecotone\Messaging\Message;
1112
use Ecotone\Messaging\MessageChannel;
1213
use Ecotone\Messaging\MessageHeaders;
14+
use Ecotone\Messaging\Support\ErrorMessage;
1315
use Ecotone\Messaging\Support\MessageBuilder;
1416

1517
/**
1618
* licence Apache-2.0
1719
*/
18-
class ErrorHandler
20+
class DelayedRetryErrorHandler
1921
{
2022
public const ECOTONE_RETRY_HEADER = 'ecotone_retry_number';
21-
public const EXCEPTION_STACKTRACE = 'exception-stacktrace';
22-
public const EXCEPTION_FILE = 'exception-file';
23-
public const EXCEPTION_LINE = 'exception-line';
24-
public const EXCEPTION_CODE = 'exception-code';
25-
public const EXCEPTION_MESSAGE = 'exception-message';
2623

2724
public function __construct(
2825
private RetryTemplate $delayedRetryTemplate,
@@ -32,23 +29,21 @@ public function __construct(
3229
}
3330

3431
public function handle(
35-
Message $errorMessage,
32+
ErrorMessage $errorMessage,
3633
ChannelResolver $channelResolver,
3734
#[Reference] LoggingGateway $logger
3835
): ?Message {
3936
$failedMessage = $errorMessage;
40-
$cause = $errorMessage->getHeaders()->get(ErrorContext::EXCEPTION);
4137
$retryNumber = $failedMessage->getHeaders()->containsKey(self::ECOTONE_RETRY_HEADER) ? $failedMessage->getHeaders()->get(self::ECOTONE_RETRY_HEADER) + 1 : 1;
4238

4339
if (! $failedMessage->getHeaders()->containsKey(MessageHeaders::POLLED_CHANNEL_NAME)) {
4440
$this->loggingGateway->error(
4541
'Failed to handle Error Message via your Retry Configuration, as it does not contain information about origination channel from which it was polled.
4642
This means that most likely Synchronous Dead Letter is configured with Retry Configuration which works only for Asynchronous configuration.',
4743
$failedMessage,
48-
['exception' => $cause],
4944
);
5045

51-
throw $cause;
46+
throw MessageHandlingException::create('Failed to handle Error Message via Retry Configuration, as it does not contain information about origination channel from which it was polled. Original error message: ' . $failedMessage->getExceptionMessage());
5247
}
5348
/** @var MessageChannel $messageChannel */
5449
$messageChannel = $channelResolver->resolve($failedMessage->getHeaders()->get(MessageHeaders::POLLED_CHANNEL_NAME));
@@ -62,7 +57,6 @@ public function handle(
6257
MessageHeaders::DELIVERY_DELAY,
6358
MessageHeaders::TIME_TO_LIVE,
6459
MessageHeaders::CONSUMER_ACK_HEADER_LOCATION,
65-
ErrorContext::EXCEPTION,
6660
self::ECOTONE_RETRY_HEADER,
6761
]);
6862

@@ -73,10 +67,10 @@ public function handle(
7367
'Discarding message %s as no dead letter channel was defined. Retried maximum number of `%s` times. Due to: %s',
7468
$failedMessage->getHeaders()->getMessageId(),
7569
$retryNumber,
76-
$cause->getMessage()
70+
$errorMessage->getExceptionMessage()
7771
),
7872
$failedMessage,
79-
['exception' => $cause],
73+
$errorMessage->getErrorContext()->toArray(),
8074
);
8175

8276
return null;
@@ -87,10 +81,10 @@ public function handle(
8781
'Sending message `%s` to dead letter channel, as retried maximum number of `%s` times. Due to: %s',
8882
$failedMessage->getHeaders()->getMessageId(),
8983
$retryNumber,
90-
$cause->getMessage()
84+
$errorMessage->getExceptionMessage()
9185
),
9286
$failedMessage,
93-
['exception' => $cause],
87+
[ErrorContext::EXCEPTION_CLASS => $errorMessage->getExceptionClass()],
9488
);
9589

9690
return $messageBuilder->build();
@@ -105,10 +99,10 @@ public function handle(
10599
$this->delayedRetryTemplate->getMaxAttempts()
106100
? sprintf('Try %d out of %s', $retryNumber, $this->delayedRetryTemplate->getMaxAttempts())
107101
: '',
108-
$cause->getMessage()
102+
$errorMessage->getExceptionMessage()
109103
),
110104
$failedMessage,
111-
['exception' => $cause],
105+
[ErrorContext::EXCEPTION_CLASS => $errorMessage->getExceptionClass()],
112106
);
113107
$messageChannel->send(
114108
$messageBuilder

packages/Ecotone/src/Messaging/Handler/Recoverability/ErrorContext.php

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
class ErrorContext
1111
{
1212
public const WHOLE_ERROR_CONTEXT = [
13-
self::EXCEPTION,
13+
self::EXCEPTION_CLASS,
1414
self::EXCEPTION_STACKTRACE,
1515
self::EXCEPTION_FILE,
1616
self::EXCEPTION_LINE,
@@ -19,13 +19,15 @@ class ErrorContext
1919
];
2020

2121
public const EXCEPTION = 'exception';
22-
public const EXCEPTION_STACKTRACE = 'exception-stacktrace';
23-
public const EXCEPTION_FILE = 'exception-file';
24-
public const EXCEPTION_LINE = 'exception-line';
25-
public const EXCEPTION_CODE = 'exception-code';
26-
public const EXCEPTION_MESSAGE = 'exception-message';
22+
public const EXCEPTION_MESSAGE = 'ecotone.exception.message';
23+
public const EXCEPTION_CLASS = 'ecotone.exception.class';
24+
public const EXCEPTION_STACKTRACE = 'ecotone.exception.stacktrace';
25+
public const EXCEPTION_FILE = 'ecotone.exception.file';
26+
public const EXCEPTION_LINE = 'ecotone.exception.line';
27+
public const EXCEPTION_CODE = 'ecotone.exception.code';
2728
public const DLQ_MESSAGE_REPLIED = 'ecotone.dlq.message_replied';
2829

30+
private string $exceptionClass;
2931
private string $messageId;
3032
private int $failedTimestamp;
3133
private string $stackTrace;
@@ -34,10 +36,11 @@ class ErrorContext
3436
private string $code;
3537
private string $message;
3638

37-
private function __construct(string $messageId, int $failedTimestamp, string $message, string $stackTrace, string $code, string $file, string $line)
39+
private function __construct(string $messageId, int $failedTimestamp, string $exceptionClass, string $message, string $stackTrace, string $code, string $file, string $line)
3840
{
3941
$this->messageId = $messageId;
4042
$this->failedTimestamp = $failedTimestamp;
43+
$this->exceptionClass = $exceptionClass;
4144
$this->stackTrace = $stackTrace;
4245
$this->file = $file;
4346
$this->line = $line;
@@ -50,6 +53,7 @@ public static function fromHeaders(array $messageHeaders): self
5053
return new self(
5154
$messageHeaders[MessageHeaders::MESSAGE_ID],
5255
$messageHeaders[MessageHeaders::TIMESTAMP],
56+
$messageHeaders[self::EXCEPTION_CLASS] ?? '',
5357
$messageHeaders[self::EXCEPTION_MESSAGE],
5458
$messageHeaders[self::EXCEPTION_STACKTRACE],
5559
$messageHeaders[self::EXCEPTION_CODE],
@@ -68,6 +72,11 @@ public function getFailedTimestamp(): int
6872
return $this->failedTimestamp;
6973
}
7074

75+
public function getExceptionClass(): string
76+
{
77+
return $this->exceptionClass;
78+
}
79+
7180
public function getStackTrace(): string
7281
{
7382
return $this->stackTrace;
@@ -92,4 +101,16 @@ public function getMessage(): string
92101
{
93102
return $this->message;
94103
}
104+
105+
public function toArray(): array
106+
{
107+
return [
108+
self::EXCEPTION_CLASS => $this->exceptionClass,
109+
self::EXCEPTION_MESSAGE => $this->message,
110+
self::EXCEPTION_STACKTRACE => $this->stackTrace,
111+
self::EXCEPTION_FILE => $this->file,
112+
self::EXCEPTION_LINE => $this->line,
113+
self::EXCEPTION_CODE => $this->code,
114+
];
115+
}
95116
}

packages/Ecotone/src/Messaging/MessageHeaders.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ public static function getFrameworksHeaderNames(): array
178178
self::STREAM_BASED_SOURCED,
179179
MessagingEntrypoint::ENTRYPOINT,
180180
self::CHANNEL_SEND_RETRY_NUMBER,
181-
ErrorContext::EXCEPTION,
182181
];
183182
}
184183

@@ -252,7 +251,7 @@ public static function unsetEnqueueMetadata(array $metadata): array
252251
$metadata[self::POLLED_CHANNEL_NAME],
253252
$metadata[self::CONSUMER_POLLING_METADATA],
254253
$metadata[self::REPLY_CHANNEL],
255-
$metadata[self::TEMPORARY_SPAN_CONTEXT_HEADER]
254+
$metadata[self::TEMPORARY_SPAN_CONTEXT_HEADER],
256255
);
257256

258257
return $metadata;

packages/Ecotone/src/Messaging/Support/ErrorMessage.php

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Ecotone\Messaging\Handler\Recoverability\ErrorContext;
66
use Ecotone\Messaging\Message;
77
use Ecotone\Messaging\MessageHeaders;
8+
use Ecotone\Messaging\MessagingException;
89
use Throwable;
910

1011
/**
@@ -22,11 +23,31 @@ private function __construct(
2223
) {
2324
}
2425

26+
public static function createFromMessage(Message $message): self
27+
{
28+
if (!self::isErrorMessage($message)) {
29+
throw MessagingException::create('Trying to create error message from message that is not generic message.');
30+
}
31+
32+
return new self($message);
33+
}
34+
35+
public static function isErrorMessage(Message $message): bool
36+
{
37+
foreach (ErrorContext::WHOLE_ERROR_CONTEXT as $errorContextKey) {
38+
if (!$message->getHeaders()->containsKey($errorContextKey)) {
39+
return false;
40+
}
41+
}
42+
43+
return true;
44+
}
45+
2546
public static function create(Message $message, Throwable $cause): self
2647
{
2748
return new self(
2849
MessageBuilder::fromMessage($message)
29-
->setHeader(ErrorContext::EXCEPTION, $cause)
50+
->setHeader(ErrorContext::EXCEPTION_CLASS, get_class($cause))
3051
->setHeader(ErrorContext::EXCEPTION_MESSAGE, $cause->getMessage())
3152
->setHeader(ErrorContext::EXCEPTION_STACKTRACE, $cause->getTraceAsString())
3253
->setHeader(ErrorContext::EXCEPTION_FILE, $cause->getFile())
@@ -52,8 +73,38 @@ public function getPayload(): mixed
5273
return $this->message->getPayload();
5374
}
5475

55-
public function getException(): Throwable
76+
public function getErrorContext(): ErrorContext
77+
{
78+
return ErrorContext::fromHeaders($this->getHeaders()->headers());
79+
}
80+
81+
public function getExceptionClass(): string
82+
{
83+
return $this->getHeaders()->get(ErrorContext::EXCEPTION_CLASS);
84+
}
85+
86+
public function getExceptionMessage(): string
87+
{
88+
return $this->getHeaders()->get(ErrorContext::EXCEPTION_MESSAGE);
89+
}
90+
91+
public function getExceptionStackTrace(): string
92+
{
93+
return $this->getHeaders()->get(ErrorContext::EXCEPTION_STACKTRACE);
94+
}
95+
96+
public function getExceptionFile(): string
97+
{
98+
return $this->getHeaders()->get(ErrorContext::EXCEPTION_FILE);
99+
}
100+
101+
public function getExceptionLine(): string
102+
{
103+
return $this->getHeaders()->get(ErrorContext::EXCEPTION_LINE);
104+
}
105+
106+
public function getExceptionCode(): string
56107
{
57-
return $this->getHeaders()->get(ErrorContext::EXCEPTION);
108+
return $this->getHeaders()->get(ErrorContext::EXCEPTION_CODE);
58109
}
59110
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Test\Ecotone\Messaging\Fixture\Handler\FailureHandler;
6+
7+
use Ecotone\Messaging\Attribute\Asynchronous;
8+
use Ecotone\Messaging\Attribute\InternalHandler;
9+
use Ecotone\Messaging\Support\ErrorMessage;
10+
use Monolog\ErrorHandler;
11+
12+
final class FailureErrorHandler
13+
{
14+
private ?ErrorMessage $message = null;
15+
16+
#[Asynchronous('async')]
17+
#[InternalHandler('errorHandler', endpointId: 'errorHandlerEndpoint')]
18+
public function handle(ErrorMessage $message): void
19+
{
20+
$this->message = $message;
21+
}
22+
23+
public function getMessage(): ?ErrorMessage
24+
{
25+
return $this->message;
26+
}
27+
}

0 commit comments

Comments
 (0)