diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index 86c1d97234b00..053e710fc36d3 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Added `WorkerMetadata` class which allows you to access the configuration details of a worker, like `queueNames` and `transportNames` it consumes from. * New method `getMetadata()` was added to `Worker` class which returns the `WorkerMetadata` object. * Deprecate not setting the `reset_on_message` config option, its default value will change to `true` in 6.0 + * Add `ConfigurableAutoAckInterface` and `DelayedAckStamp` to not automatically ACK message. 5.3 --- diff --git a/src/Symfony/Component/Messenger/Handler/ConfigurableAutoAckInterface.php b/src/Symfony/Component/Messenger/Handler/ConfigurableAutoAckInterface.php new file mode 100644 index 0000000000000..2454c1537f2e2 --- /dev/null +++ b/src/Symfony/Component/Messenger/Handler/ConfigurableAutoAckInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Handler; + +use Symfony\Component\Messenger\Envelope; + +/** + * Marker interface for message handlers to configure if auto ACK is disabled. + * + * @author Grégoire Pineau + */ +interface ConfigurableAutoAckInterface +{ + public function isAutoAckDisabled(Envelope $envelope): bool; +} diff --git a/src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php b/src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php index eaf6b9508017b..9d990e3026959 100644 --- a/src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php +++ b/src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php @@ -16,8 +16,10 @@ use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Exception\HandlerFailedException; use Symfony\Component\Messenger\Exception\NoHandlerForMessageException; +use Symfony\Component\Messenger\Handler\ConfigurableAutoAckInterface; use Symfony\Component\Messenger\Handler\HandlerDescriptor; use Symfony\Component\Messenger\Handler\HandlersLocatorInterface; +use Symfony\Component\Messenger\Stamp\DelayedAckStamp; use Symfony\Component\Messenger\Stamp\HandledStamp; /** @@ -60,7 +62,10 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope try { $handler = $handlerDescriptor->getHandler(); - $handledStamp = HandledStamp::fromDescriptor($handlerDescriptor, $handler($message)); + if ($handler instanceof ConfigurableAutoAckInterface && $handler->isAutoAckDisabled($envelope)) { + $envelope = $envelope->with(new DelayedAckStamp()); + } + $handledStamp = HandledStamp::fromDescriptor($handlerDescriptor, $handler($message, $envelope)); $envelope = $envelope->with($handledStamp); $this->logger->info('Message {class} handled by {handler}', $context + ['handler' => $handledStamp->getHandlerName()]); } catch (\Throwable $e) { diff --git a/src/Symfony/Component/Messenger/Stamp/DelayedAckStamp.php b/src/Symfony/Component/Messenger/Stamp/DelayedAckStamp.php new file mode 100644 index 0000000000000..15a6171b7fd89 --- /dev/null +++ b/src/Symfony/Component/Messenger/Stamp/DelayedAckStamp.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Stamp; + +/** + * Apply this stamp to delay ACK of your message on a transport. + */ +final class DelayedAckStamp implements StampInterface +{ +} diff --git a/src/Symfony/Component/Messenger/Tests/Middleware/HandleMessageMiddlewareTest.php b/src/Symfony/Component/Messenger/Tests/Middleware/HandleMessageMiddlewareTest.php index c33bad5137d8c..9cf0b7e4d7a03 100644 --- a/src/Symfony/Component/Messenger/Tests/Middleware/HandleMessageMiddlewareTest.php +++ b/src/Symfony/Component/Messenger/Tests/Middleware/HandleMessageMiddlewareTest.php @@ -14,10 +14,12 @@ use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Exception\HandlerFailedException; use Symfony\Component\Messenger\Exception\NoHandlerForMessageException; +use Symfony\Component\Messenger\Handler\ConfigurableAutoAckInterface; use Symfony\Component\Messenger\Handler\HandlerDescriptor; use Symfony\Component\Messenger\Handler\HandlersLocator; use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; use Symfony\Component\Messenger\Middleware\StackMiddleware; +use Symfony\Component\Messenger\Stamp\DelayedAckStamp; use Symfony\Component\Messenger\Stamp\HandledStamp; use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; @@ -114,6 +116,45 @@ public function itAddsHandledStampsProvider(): iterable ]; } + /** + * @dataProvider itAddsDelayedAckStampProvider + */ + public function testItAddsDelayedAckStamp($handler, bool $stampIsExpected) + { + $message = new DummyMessage('Hey'); + $envelope = new Envelope($message); + + $middleware = new HandleMessageMiddleware(new HandlersLocator([ + DummyMessage::class => [$handler], + ])); + + try { + $envelope = $middleware->handle($envelope, $this->getStackMock(true)); + } catch (HandlerFailedException $e) { + $envelope = $e->getEnvelope(); + } + + $this->assertSame($stampIsExpected, null !== $envelope->last(DelayedAckStamp::class)); + } + + public function itAddsDelayedAckStampProvider(): iterable + { + yield 'It does not add stamp by default' => [ + new HandleMessageMiddlewareTestCallable(), + false, + ]; + + yield 'It does not add when object return false' => [ + new HandleMessageMiddlewareWithAckConfigurationTestCallable(false), + false, + ]; + + yield 'It adds when object return true' => [ + new HandleMessageMiddlewareWithAckConfigurationTestCallable(true), + true, + ]; + } + public function testThrowsNoHandlerException() { $this->expectException(NoHandlerForMessageException::class); @@ -137,3 +178,22 @@ public function __invoke() { } } + +class HandleMessageMiddlewareWithAckConfigurationTestCallable implements ConfigurableAutoAckInterface +{ + private $autoAckDisabled; + + public function __construct(bool $autoAckDisabled) + { + $this->autoAckDisabled = $autoAckDisabled; + } + + public function isAutoAckDisabled(Envelope $envelope): bool + { + return $this->autoAckDisabled; + } + + public function __invoke() + { + } +} diff --git a/src/Symfony/Component/Messenger/Tests/WorkerTest.php b/src/Symfony/Component/Messenger/Tests/WorkerTest.php index 6711a0febfea0..f654758320239 100644 --- a/src/Symfony/Component/Messenger/Tests/WorkerTest.php +++ b/src/Symfony/Component/Messenger/Tests/WorkerTest.php @@ -24,6 +24,7 @@ use Symfony\Component\Messenger\Exception\RuntimeException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp; +use Symfony\Component\Messenger\Stamp\DelayedAckStamp; use Symfony\Component\Messenger\Stamp\ReceivedStamp; use Symfony\Component\Messenger\Stamp\SentStamp; use Symfony\Component\Messenger\Stamp\StampInterface; @@ -329,6 +330,24 @@ public function testWorkerMessageReceivedEventMutability() $envelope = current($receiver->getAcknowledgedEnvelopes()); $this->assertCount(1, $envelope->all(\get_class($stamp))); } + + public function testWorkerDoesNotCallAckWhenDelayedAckStamp() + { + $envelope = new Envelope(new DummyMessage('Hello')); + $envelope = $envelope->with(new DelayedAckStamp()); + $receiver = new DummyReceiver([[$envelope]]); + + $bus = $this->createMock(MessageBusInterface::class); + $bus->method('dispatch')->willReturnArgument(0); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(1)); + + $worker = new Worker([$receiver], $bus, $dispatcher); + $worker->run(); + + $this->assertSame(0, $receiver->getAcknowledgeCount()); + } } class DummyReceiver implements ReceiverInterface diff --git a/src/Symfony/Component/Messenger/Worker.php b/src/Symfony/Component/Messenger/Worker.php index f32117529dbbe..f71d923598a44 100644 --- a/src/Symfony/Component/Messenger/Worker.php +++ b/src/Symfony/Component/Messenger/Worker.php @@ -24,6 +24,7 @@ use Symfony\Component\Messenger\Exception\RejectRedeliveredMessageException; use Symfony\Component\Messenger\Exception\RuntimeException; use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp; +use Symfony\Component\Messenger\Stamp\DelayedAckStamp; use Symfony\Component\Messenger\Stamp\ReceivedStamp; use Symfony\Component\Messenger\Transport\Receiver\QueueReceiverInterface; use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; @@ -171,7 +172,9 @@ private function handleMessage(Envelope $envelope, ReceiverInterface $receiver, $this->logger->info('{class} was handled successfully (acknowledging to transport).', $context); } - $receiver->ack($envelope); + if (null === $envelope->last(DelayedAckStamp::class)) { + $receiver->ack($envelope); + } } public function stop(): void