diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index b7f604f41c00c..e48f8471bf053 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -15,6 +15,15 @@ CHANGELOG * [BC BREAK] If listening to exceptions while using `AmqpSender` or `AmqpReceiver`, `\AMQPException` is no longer thrown in favor of `TransportException`. + + * Added `prefetch_count` AMQP option which set the channel prefetch count + + * Added `consume_fatal` (default `true`) and `consume_requeue` (default `false`) AMQP options which allow consumer to + continue processing messages or nack it with requeue. + + * Added `UnrecoverableMessageExceptionInterface` and `RecoverableMessageExceptionInterface` into AMQP transport + exception for nack with or without requeue, and then continue to consume the other messages. + 4.2.0 ----- diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceiverTest.php b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceiverTest.php index 8e224e0653df7..e6e84dfbbd3e2 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceiverTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpReceiverTest.php @@ -16,7 +16,8 @@ use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\Transport\AmqpExt\AmqpReceiver; use Symfony\Component\Messenger\Transport\AmqpExt\Connection; -use Symfony\Component\Messenger\Transport\AmqpExt\Exception\RejectMessageExceptionInterface; +use Symfony\Component\Messenger\Transport\AmqpExt\Exception\RecoverableMessageExceptionInterface; +use Symfony\Component\Messenger\Transport\AmqpExt\Exception\UnrecoverableMessageExceptionInterface; use Symfony\Component\Messenger\Transport\Serialization\Serializer; use Symfony\Component\Serializer as SerializerComponent; use Symfony\Component\Serializer\Encoder\JsonEncoder; @@ -69,7 +70,7 @@ public function testItNonAcknowledgeTheMessageIfAnExceptionHappened() $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); $connection->method('get')->willReturn($envelope); - $connection->expects($this->once())->method('nack')->with($envelope); + $connection->expects($this->once())->method('nack')->with($envelope, AMQP_NOPARAM); $receiver = new AmqpReceiver($connection, $serializer); $receiver->receive(function () { @@ -78,9 +79,9 @@ public function testItNonAcknowledgeTheMessageIfAnExceptionHappened() } /** - * @expectedException \Symfony\Component\Messenger\Tests\Transport\AmqpExt\WillNeverWorkException + * @expectedException \Symfony\Component\Messenger\Exception\TransportException */ - public function testItRejectsTheMessageIfTheExceptionIsARejectMessageExceptionInterface() + public function testItThrowsATransportExceptionIfItCannotAcknowledgeMessage() { $serializer = new Serializer( new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) @@ -94,18 +95,19 @@ public function testItRejectsTheMessageIfTheExceptionIsARejectMessageExceptionIn $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); $connection->method('get')->willReturn($envelope); - $connection->expects($this->once())->method('reject')->with($envelope); + + $connection->method('ack')->with($envelope)->willThrowException(new \AMQPException()); $receiver = new AmqpReceiver($connection, $serializer); - $receiver->receive(function () { - throw new WillNeverWorkException('Well...'); + $receiver->receive(function (?Envelope $envelope) use ($receiver) { + $receiver->stop(); }); } /** * @expectedException \Symfony\Component\Messenger\Exception\TransportException */ - public function testItThrowsATransportExceptionIfItCannotAcknowledgeMessage() + public function testItThrowsATransportExceptionIfItCannotRejectMessage() { $serializer = new Serializer( new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) @@ -119,19 +121,18 @@ public function testItThrowsATransportExceptionIfItCannotAcknowledgeMessage() $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); $connection->method('get')->willReturn($envelope); - - $connection->method('ack')->with($envelope)->willThrowException(new \AMQPException()); + $connection->method('nack')->with($envelope, AMQP_NOPARAM)->willThrowException(new \AMQPException()); $receiver = new AmqpReceiver($connection, $serializer); - $receiver->receive(function (?Envelope $envelope) use ($receiver) { - $receiver->stop(); + $receiver->receive(function () { + throw new InterruptException('Well...'); }); } /** * @expectedException \Symfony\Component\Messenger\Exception\TransportException */ - public function testItThrowsATransportExceptionIfItCannotRejectMessage() + public function testItThrowsATransportExceptionIfItCannotNonAcknowledgeMessage() { $serializer = new Serializer( new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) @@ -145,18 +146,102 @@ public function testItThrowsATransportExceptionIfItCannotRejectMessage() $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); $connection->method('get')->willReturn($envelope); - $connection->method('reject')->with($envelope)->willThrowException(new \AMQPException()); + + $connection->method('nack')->with($envelope)->willThrowException(new \AMQPException()); $receiver = new AmqpReceiver($connection, $serializer); $receiver->receive(function () { - throw new WillNeverWorkException('Well...'); + throw new InterruptException('Well...'); + }); + } + + public function testItNackAndRequeueTheMessageIfTheExceptionIsARecoverableMessageExceptionInterface() + { + $serializer = new Serializer( + new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) + ); + + $envelope = $this->getMockBuilder(\AMQPEnvelope::class)->getMock(); + $envelope->method('getBody')->willReturn('{"message": "Hi"}'); + $envelope->method('getHeaders')->willReturn([ + 'type' => DummyMessage::class, + ]); + + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); + $connection->method('get')->willReturn($envelope); + $connection->expects($this->exactly(3))->method('nack')->with($envelope, AMQP_REQUEUE); + + $receiver = new AmqpReceiver($connection, $serializer); + $count = 1; + $receiver->receive(function () use (&$count, $receiver) { + if ($count++ >= 3) { + $receiver->stop(); + } + throw new RecoverableMessageException('Temporary...'); + }); + } + + public function testItNackWithoutRequeueTheMessageIfTheExceptionIsAnUnrecoverableMessageExceptionInterface() + { + $serializer = new Serializer( + new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) + ); + + $envelope = $this->getMockBuilder(\AMQPEnvelope::class)->getMock(); + $envelope->method('getBody')->willReturn('{"message": "Hi"}'); + $envelope->method('getHeaders')->willReturn([ + 'type' => DummyMessage::class, + ]); + + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); + $connection->method('get')->willReturn($envelope); + $connection->expects($this->once())->method('nack')->with($envelope, AMQP_NOPARAM); + $connection->expects($this->once())->method('ack')->with($envelope); + + $receiver = new AmqpReceiver($connection, $serializer); + $count = 0; + $receiver->receive(function () use (&$count, $receiver) { + ++$count; + if (1 === $count) { + throw new UnrecoverableMessageException('Temporary...'); + } + $receiver->stop(); + }); + } + + public function testItNackWithoutRequeueTheMessageIfTheExceptionIsAThrowableExceptionAndContinue() + { + $serializer = new Serializer( + new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) + ); + + $envelope = $this->getMockBuilder(\AMQPEnvelope::class)->getMock(); + $envelope->method('getBody')->willReturn('{"message": "Hi"}'); + $envelope->method('getHeaders')->willReturn([ + 'type' => DummyMessage::class, + ]); + + $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); + $connection->method('getConnectionCredentials')->willReturn(['consume_fatal' => false]); + $connection->method('get')->willReturn($envelope); + $connection->expects($this->once())->method('nack')->with($envelope, AMQP_NOPARAM); + $connection->expects($this->once())->method('ack')->with($envelope); + + $receiver = new AmqpReceiver($connection, $serializer); + $count = 0; + $receiver->receive(function () use (&$count, $receiver) { + ++$count; + if (1 === $count) { + throw new InterruptException('Temporary...'); + } + $receiver->stop(); }); } /** - * @expectedException \Symfony\Component\Messenger\Exception\TransportException + * @expectedException \Symfony\Component\Messenger\Tests\Transport\AmqpExt\InterruptException */ - public function testItThrowsATransportExceptionIfItCannotNonAcknowledgeMessage() + public function testItNackAndRequeueTheMessageIfTheExceptionIsAThrowableExceptionAndGenerateFatal() { $serializer = new Serializer( new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) @@ -169,9 +254,9 @@ public function testItThrowsATransportExceptionIfItCannotNonAcknowledgeMessage() ]); $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); + $connection->method('getConnectionCredentials')->willReturn(['consume_requeue' => true]); $connection->method('get')->willReturn($envelope); - - $connection->method('nack')->with($envelope)->willThrowException(new \AMQPException()); + $connection->expects($this->once())->method('nack')->with($envelope, AMQP_REQUEUE); $receiver = new AmqpReceiver($connection, $serializer); $receiver->receive(function () { @@ -184,6 +269,10 @@ class InterruptException extends \Exception { } -class WillNeverWorkException extends \Exception implements RejectMessageExceptionInterface +class RecoverableMessageException extends \Exception implements RecoverableMessageExceptionInterface +{ +} + +class UnrecoverableMessageException extends \Exception implements UnrecoverableMessageExceptionInterface { } diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/ConnectionTest.php b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/ConnectionTest.php index b8809368e5b3d..421f6294d9456 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/ConnectionTest.php @@ -251,6 +251,22 @@ public function testPublishWithQueueOptions() $connection = Connection::fromDsn('amqp://localhost/%2f/messages?queue[attributes][delivery_mode]=2&queue[attributes][headers][token]=uuid&queue[flags]=1', [], true, $factory); $connection->publish('body', $headers); } + + public function testSetChannelPrefetchWhenSetup() + { + $factory = new TestAmqpFactory( + $amqpConnection = $this->createMock(\AMQPConnection::class), + $amqpChannel = $this->createMock(\AMQPChannel::class), + $amqpQueue = $this->createMock(\AMQPQueue::class), + $amqpExchange = $this->createMock(\AMQPExchange::class) + ); + + $amqpChannel->expects($this->exactly(2))->method('setPrefetchCount')->with(2); + $connection = Connection::fromDsn('amqp://localhost/%2f/messages?prefetch_count=2', [], true, $factory); + $connection->setup(); + $connection = Connection::fromDsn('amqp://localhost/%2f/messages', ['prefetch_count' => 2], true, $factory); + $connection->setup(); + } } class TestAmqpFactory extends AmqpFactory diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php index cb7a4db013fa9..68eff9d97534a 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/AmqpReceiver.php @@ -12,7 +12,8 @@ namespace Symfony\Component\Messenger\Transport\AmqpExt; use Symfony\Component\Messenger\Exception\TransportException; -use Symfony\Component\Messenger\Transport\AmqpExt\Exception\RejectMessageExceptionInterface; +use Symfony\Component\Messenger\Transport\AmqpExt\Exception\RecoverableMessageExceptionInterface; +use Symfony\Component\Messenger\Transport\AmqpExt\Exception\UnrecoverableMessageExceptionInterface; use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; @@ -61,24 +62,34 @@ public function receive(callable $handler): void ])); $this->connection->ack($AMQPEnvelope); - } catch (RejectMessageExceptionInterface $e) { + } catch (RecoverableMessageExceptionInterface $e) { try { - $this->connection->reject($AMQPEnvelope); + $this->connection->nack($AMQPEnvelope, AMQP_REQUEUE); + } catch (\AMQPException $exception) { + throw new TransportException($exception->getMessage(), 0, $exception); + } + } catch (UnrecoverableMessageExceptionInterface $e) { + try { + $this->connection->nack($AMQPEnvelope, AMQP_NOPARAM); } catch (\AMQPException $exception) { throw new TransportException($exception->getMessage(), 0, $exception); } - - throw $e; } catch (\AMQPException $e) { throw new TransportException($e->getMessage(), 0, $e); } catch (\Throwable $e) { + $connectionCredentials = $this->connection->getConnectionCredentials() + [ + 'consume_fatal' => true, + 'consume_requeue' => false, + ]; + $flag = $connectionCredentials['consume_requeue'] ? AMQP_REQUEUE : AMQP_NOPARAM; try { - $this->connection->nack($AMQPEnvelope, AMQP_REQUEUE); + $this->connection->nack($AMQPEnvelope, $flag); } catch (\AMQPException $exception) { throw new TransportException($exception->getMessage(), 0, $exception); } - - throw $e; + if ($connectionCredentials['consume_fatal']) { + throw $e; + } } finally { if (\function_exists('pcntl_signal_dispatch')) { pcntl_signal_dispatch(); diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php index e576916b8d041..a0d771e6198dd 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php @@ -185,7 +185,7 @@ public function nack(\AMQPEnvelope $message, int $flags = AMQP_NOPARAM): bool public function setup(): void { - if (!$this->channel()->isConnected()) { + if (null === $this->amqpChannel || false === $this->amqpChannel->isConnected()) { $this->clear(); } @@ -206,6 +206,9 @@ public function channel(): \AMQPChannel } $this->amqpChannel = $this->amqpFactory->createChannel($connection); + if (isset($this->connectionCredentials['prefetch_count'])) { + $this->amqpChannel->setPrefetchCount($this->connectionCredentials['prefetch_count']); + } } return $this->amqpChannel; diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/Exception/RecoverableMessageExceptionInterface.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/Exception/RecoverableMessageExceptionInterface.php new file mode 100644 index 0000000000000..a3ed4c2410001 --- /dev/null +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/Exception/RecoverableMessageExceptionInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\AmqpExt\Exception; + +/** + * If something goes wrong while consuming and handling a message from the AMQP broker, if the exception that is thrown + * by the bus while dispatching the message implements this interface, the message will be nack and re-queued. + * + * Bus continue handling messages. + * + * @author Frederic Bouchery + * + * @experimental in 4.3 + */ +interface RecoverableMessageExceptionInterface extends \Throwable +{ +} diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/Exception/RejectMessageExceptionInterface.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/Exception/RejectMessageExceptionInterface.php index 9b820a7d8fbf8..e3099360d9546 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/Exception/RejectMessageExceptionInterface.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/Exception/RejectMessageExceptionInterface.php @@ -20,6 +20,10 @@ * * @author Samuel Roze * + * @deprecated use RecoverableMessageExceptionInterface or UnrecoverableMessageExceptionInterface instead. Now, it is + * handle as a `\Throwable`: `nack` instead of `reject` + * + * * @experimental in 4.2 */ interface RejectMessageExceptionInterface extends \Throwable diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/Exception/UnrecoverableMessageExceptionInterface.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/Exception/UnrecoverableMessageExceptionInterface.php new file mode 100644 index 0000000000000..9d1695c27acd6 --- /dev/null +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/Exception/UnrecoverableMessageExceptionInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Transport\AmqpExt\Exception; + +/** + * If something goes wrong while consuming and handling a message from the AMQP broker, if the exception that is thrown + * by the bus while dispatching the message implements this interface, the message will be nack and not re-queued. + * + * Bus continue handling messages. + * + * @author Frederic Bouchery + * + * @experimental in 4.3 + */ +interface UnrecoverableMessageExceptionInterface extends \Throwable +{ +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy