Skip to content

Commit

Permalink
bug #57112 [Messenger] Handle AMQPConnectionException when publishi…
Browse files Browse the repository at this point in the history
…ng a message (jwage)

This PR was merged into the 5.4 branch.

Discussion
----------

[Messenger] Handle `AMQPConnectionException` when publishing a message

| Q             | A
| ------------- | ---
| Branch?       | 5.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | backport of #54167
| License       | MIT

Commits
-------

e10aa0ea93 [Messenger] [Amqp] Handle AMQPConnectionException when publishing a message.
  • Loading branch information
xabbuh committed Jun 16, 2024
2 parents 9b3e7a7 + 14d5f22 commit ccadd8d
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 15 deletions.
67 changes: 67 additions & 0 deletions Tests/Transport/ConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,73 @@ public function testItCanBeConstructedWithTLSOptionsAndNonTLSDsn()
);
}

public function testItCanRetryPublishWhenAMQPConnectionExceptionIsThrown()
{
$factory = new TestAmqpFactory(
$amqpConnection = $this->createMock(\AMQPConnection::class),
$amqpChannel = $this->createMock(\AMQPChannel::class),
$amqpQueue = $this->createMock(\AMQPQueue::class),
$amqpExchange = $this->createMock(\AMQPExchange::class)
);

$amqpExchange->expects($this->exactly(2))
->method('publish')
->willReturnOnConsecutiveCalls(
$this->throwException(new \AMQPConnectionException('a socket error occurred')),
null
);

$connection = Connection::fromDsn('amqp://localhost', [], $factory);
$connection->publish('body');
}

public function testItCanRetryPublishWithDelayWhenAMQPConnectionExceptionIsThrown()
{
$factory = new TestAmqpFactory(
$amqpConnection = $this->createMock(\AMQPConnection::class),
$amqpChannel = $this->createMock(\AMQPChannel::class),
$amqpQueue = $this->createMock(\AMQPQueue::class),
$amqpExchange = $this->createMock(\AMQPExchange::class)
);

$amqpExchange->expects($this->exactly(2))
->method('publish')
->willReturnOnConsecutiveCalls(
$this->throwException(new \AMQPConnectionException('a socket error occurred')),
null
);

$connection = Connection::fromDsn('amqp://localhost', [], $factory);
$connection->publish('body', [], 5000);
}

public function testItWillRetryMaxThreeTimesWhenAMQPConnectionExceptionIsThrown()
{
$factory = new TestAmqpFactory(
$amqpConnection = $this->createMock(\AMQPConnection::class),
$amqpChannel = $this->createMock(\AMQPChannel::class),
$amqpQueue = $this->createMock(\AMQPQueue::class),
$amqpExchange = $this->createMock(\AMQPExchange::class)
);

$exception = new \AMQPConnectionException('a socket error occurred');

$amqpExchange->expects($this->exactly(4))
->method('publish')
->willReturnOnConsecutiveCalls(
$this->throwException($exception),
$this->throwException($exception),
$this->throwException($exception),
$this->throwException($exception)
);

self::expectException(get_class($exception));
self::expectExceptionMessage($exception->getMessage());

$connection = Connection::fromDsn('amqp://localhost', [], $factory);
$connection->publish('body');
}

private function createDelayOrRetryConnection(\AMQPExchange $delayExchange, string $deadLetterExchangeName, string $delayQueueName): Connection
{
$amqpConnection = $this->createMock(\AMQPConnection::class);
Expand Down
56 changes: 41 additions & 15 deletions Transport/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,19 +306,21 @@ public function publish(string $body, array $headers = [], int $delayInMs = 0, ?
$this->setupExchangeAndQueues(); // also setup normal exchange for delayed messages so delay queue can DLX messages to it
}

if (0 !== $delayInMs) {
$this->publishWithDelay($body, $headers, $delayInMs, $amqpStamp);
$this->withConnectionExceptionRetry(function () use ($body, $headers, $delayInMs, $amqpStamp) {
if (0 !== $delayInMs) {
$this->publishWithDelay($body, $headers, $delayInMs, $amqpStamp);

return;
}
return;
}

$this->publishOnExchange(
$this->exchange(),
$body,
$this->getRoutingKeyForMessage($amqpStamp),
$headers,
$amqpStamp
);
$this->publishOnExchange(
$this->exchange(),
$body,
$this->getRoutingKeyForMessage($amqpStamp),
$headers,
$amqpStamp
);
});
}

/**
Expand Down Expand Up @@ -570,13 +572,18 @@ public function exchange(): \AMQPExchange
private function clearWhenDisconnected(): void
{
if (!$this->channel()->isConnected()) {
$this->amqpChannel = null;
$this->amqpQueues = [];
$this->amqpExchange = null;
$this->amqpDelayExchange = null;
$this->clear();
}
}

private function clear(): void
{
$this->amqpChannel = null;
$this->amqpQueues = [];
$this->amqpExchange = null;
$this->amqpDelayExchange = null;
}

private function getDefaultPublishRoutingKey(): ?string
{
return $this->exchangeOptions['default_publish_routing_key'] ?? null;
Expand All @@ -593,6 +600,25 @@ private function getRoutingKeyForMessage(?AmqpStamp $amqpStamp): ?string
{
return (null !== $amqpStamp ? $amqpStamp->getRoutingKey() : null) ?? $this->getDefaultPublishRoutingKey();
}

private function withConnectionExceptionRetry(callable $callable): void
{
$maxRetries = 3;
$retries = 0;

retry:
try {
$callable();
} catch (\AMQPConnectionException $e) {
if (++$retries <= $maxRetries) {
$this->clear();

goto retry;
}

throw $e;
}
}
}

if (!class_exists(\Symfony\Component\Messenger\Transport\AmqpExt\Connection::class, false)) {
Expand Down

0 comments on commit ccadd8d

Please sign in to comment.