diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index a7421573e0f24..8d0a2dd604c77 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -1313,6 +1313,10 @@ function ($a) { ->prototype('variable') ->end() ->end() + ->scalarNode('failure_transport') + ->defaultNull() + ->info('Transport name to send failed messages to (after all retries have failed).') + ->end() ->arrayNode('retry_strategy') ->addDefaultsIfNotSet() ->beforeNormalization() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 4b3e4427a0cba..375c698f1af1e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1915,15 +1915,38 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $container->setAlias('messenger.default_serializer', $config['serializer']['default_serializer']); } + $failureTransports = []; + if ($config['failure_transport']) { + if (!isset($config['transports'][$config['failure_transport']])) { + throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $config['failure_transport'])); + } + + $container->setAlias('messenger.failure_transports.default', 'messenger.transport.'.$config['failure_transport']); + $failureTransports[] = $config['failure_transport']; + } + + $failureTransportsByName = []; + foreach ($config['transports'] as $name => $transport) { + if ($transport['failure_transport']) { + $failureTransports[] = $transport['failure_transport']; + $failureTransportsByName[$name] = $transport['failure_transport']; + } elseif ($config['failure_transport']) { + $failureTransportsByName[$name] = $config['failure_transport']; + } + } + $senderAliases = []; $transportRetryReferences = []; foreach ($config['transports'] as $name => $transport) { $serializerId = $transport['serializer'] ?? 'messenger.default_serializer'; - $transportDefinition = (new Definition(TransportInterface::class)) ->setFactory([new Reference('messenger.transport_factory'), 'createTransport']) ->setArguments([$transport['dsn'], $transport['options'] + ['transport_name' => $name], new Reference($serializerId)]) - ->addTag('messenger.receiver', ['alias' => $name]) + ->addTag('messenger.receiver', [ + 'alias' => $name, + 'is_failure_transport' => \in_array($name, $failureTransports), + ] + ) ; $container->setDefinition($transportId = 'messenger.transport.'.$name, $transportDefinition); $senderAliases[$name] = $transportId; @@ -1954,6 +1977,18 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $senderReferences[$serviceId] = new Reference($serviceId); } + foreach ($config['transports'] as $name => $transport) { + if ($transport['failure_transport']) { + if (!isset($senderReferences[$transport['failure_transport']])) { + throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $transport['failure_transport'])); + } + } + } + + $failureTransportReferencesByTransportName = array_map(function ($failureTransportName) use ($senderReferences) { + return $senderReferences[$failureTransportName]; + }, $failureTransportsByName); + $messageToSendersMapping = []; foreach ($config['routing'] as $message => $messageConfiguration) { if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) { @@ -1984,19 +2019,17 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $container->getDefinition('messenger.retry_strategy_locator') ->replaceArgument(0, $transportRetryReferences); - if ($config['failure_transport']) { - if (!isset($senderReferences[$config['failure_transport']])) { - throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $config['failure_transport'])); - } - - $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener') - ->replaceArgument(0, $senderReferences[$config['failure_transport']]); + if (\count($failureTransports) > 0) { $container->getDefinition('console.command.messenger_failed_messages_retry') ->replaceArgument(0, $config['failure_transport']); $container->getDefinition('console.command.messenger_failed_messages_show') ->replaceArgument(0, $config['failure_transport']); $container->getDefinition('console.command.messenger_failed_messages_remove') ->replaceArgument(0, $config['failure_transport']); + + $failureTransportsByTransportNameServiceLocator = ServiceLocatorTagPass::register($container, $failureTransportReferencesByTransportName); + $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener') + ->replaceArgument(0, $failureTransportsByTransportNameServiceLocator); } else { $container->removeDefinition('messenger.failure.send_failed_message_to_failure_transport_listener'); $container->removeDefinition('console.command.messenger_failed_messages_retry'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php index 5aea86ba7a06b..1c05d8760e614 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php @@ -165,8 +165,8 @@ ->set('console.command.messenger_failed_messages_retry', FailedMessagesRetryCommand::class) ->args([ - abstract_arg('Receiver name'), - abstract_arg('Receiver'), + abstract_arg('Default failure receiver name'), + abstract_arg('Receivers'), service('messenger.routable_message_bus'), service('event_dispatcher'), service('logger'), @@ -175,15 +175,15 @@ ->set('console.command.messenger_failed_messages_show', FailedMessagesShowCommand::class) ->args([ - abstract_arg('Receiver name'), - abstract_arg('Receiver'), + abstract_arg('Default failure receiver name'), + abstract_arg('Receivers'), ]) ->tag('console.command') ->set('console.command.messenger_failed_messages_remove', FailedMessagesRemoveCommand::class) ->args([ - abstract_arg('Receiver name'), - abstract_arg('Receiver'), + abstract_arg('Default failure receiver name'), + abstract_arg('Receivers'), ]) ->tag('console.command') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php index a7d993d47e316..5d563c92e7152 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php @@ -172,7 +172,7 @@ ->set('messenger.failure.send_failed_message_to_failure_transport_listener', SendFailedMessageToFailureTransportListener::class) ->args([ - abstract_arg('failure transport'), + abstract_arg('failure transports'), service('logger')->ignoreOnInvalid(), ]) ->tag('kernel.event_subscriber') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 1027c3c6f9878..2d33aae50fe25 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -491,6 +491,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php new file mode 100644 index 0000000000000..8f85259aa6908 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports.php @@ -0,0 +1,19 @@ +loadFromExtension('framework', [ + 'messenger' => [ + 'transports' => [ + 'transport_1' => [ + 'dsn' => 'null://', + 'failure_transport' => 'failure_transport_1' + ], + 'transport_2' => 'null://', + 'transport_3' => [ + 'dsn' => 'null://', + 'failure_transport' => 'failure_transport_3' + ], + 'failure_transport_1' => 'null://', + 'failure_transport_3' => 'null://' + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php new file mode 100644 index 0000000000000..0cff76887b152 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_multiple_failure_transports_global.php @@ -0,0 +1,21 @@ +loadFromExtension('framework', [ + 'messenger' => [ + 'failure_transport' => 'failure_transport_global', + 'transports' => [ + 'transport_1' => [ + 'dsn' => 'null://', + 'failure_transport' => 'failure_transport_1' + ], + 'transport_2' => 'null://', + 'transport_3' => [ + 'dsn' => 'null://', + 'failure_transport' => 'failure_transport_3' + ], + 'failure_transport_global' => 'null://', + 'failure_transport_1' => 'null://', + 'failure_transport_3' => 'null://', + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml new file mode 100644 index 0000000000000..b8e9f19759429 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml new file mode 100644 index 0000000000000..c6e5c530fda1b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_multiple_failure_transports_global.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml new file mode 100644 index 0000000000000..863f18a7d1a1f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports.yml @@ -0,0 +1,12 @@ +framework: + messenger: + transports: + transport_1: + dsn: 'null://' + failure_transport: failure_transport_1 + transport_2: 'null://' + transport_3: + dsn: 'null://' + failure_transport: failure_transport_3 + failure_transport_1: 'null://' + failure_transport_3: 'null://' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml new file mode 100644 index 0000000000000..10023edb0b9fd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_multiple_failure_transports_global.yml @@ -0,0 +1,14 @@ +framework: + messenger: + failure_transport: failure_transport_global + transports: + transport_1: + dsn: 'null://' + failure_transport: failure_transport_1 + transport_2: 'null://' + transport_3: + dsn: 'null://' + failure_transport: failure_transport_3 + failure_transport_global: 'null://' + failure_transport_1: 'null://' + failure_transport_3: 'null://' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 9d5d1aa9fb544..531d368b27baa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -32,6 +32,7 @@ use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; use Symfony\Component\Cache\DependencyInjection\CachePoolPass; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass; @@ -710,12 +711,92 @@ public function testMessenger() $this->assertSame(TransportFactory::class, $container->getDefinition('messenger.transport_factory')->getClass()); } + public function testMessengerMultipleFailureTransports() + { + $container = $this->createContainerFromFile('messenger_multiple_failure_transports'); + + $failureTransport1Definition = $container->getDefinition('messenger.transport.failure_transport_1'); + $failureTransport1Tags = $failureTransport1Definition->getTag('messenger.receiver')[0]; + + $this->assertEquals([ + 'alias' => 'failure_transport_1', + 'is_failure_transport' => true, + ], $failureTransport1Tags); + + $failureTransport3Definition = $container->getDefinition('messenger.transport.failure_transport_3'); + $failureTransport3Tags = $failureTransport3Definition->getTag('messenger.receiver')[0]; + + $this->assertEquals([ + 'alias' => 'failure_transport_3', + 'is_failure_transport' => true, + ], $failureTransport3Tags); + + // transport 2 exists but does not appear in the mapping + $this->assertFalse($container->hasDefinition('messenger.transport.failure_transport_2')); + + $failureTransportsByTransportNameServiceLocator = $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0); + $failureTransports = $container->getDefinition((string) $failureTransportsByTransportNameServiceLocator)->getArgument(0); + $expectedTransportsByFailureTransports = [ + 'transport_1' => new Reference('messenger.transport.failure_transport_1'), + 'transport_3' => new Reference('messenger.transport.failure_transport_3'), + ]; + + $failureTransportsReferences = array_map(function (ServiceClosureArgument $serviceClosureArgument) { + $values = $serviceClosureArgument->getValues(); + + return array_shift($values); + }, $failureTransports); + $this->assertEquals($expectedTransportsByFailureTransports, $failureTransportsReferences); + } + + public function testMessengerMultipleFailureTransportsWithGlobalFailureTransport() + { + $container = $this->createContainerFromFile('messenger_multiple_failure_transports_global'); + + $this->assertEquals('messenger.transport.failure_transport_global', (string) $container->getAlias('messenger.failure_transports.default')); + + $failureTransport1Definition = $container->getDefinition('messenger.transport.failure_transport_1'); + $failureTransport1Tags = $failureTransport1Definition->getTag('messenger.receiver')[0]; + + $this->assertEquals([ + 'alias' => 'failure_transport_1', + 'is_failure_transport' => true, + ], $failureTransport1Tags); + + $failureTransport3Definition = $container->getDefinition('messenger.transport.failure_transport_3'); + $failureTransport3Tags = $failureTransport3Definition->getTag('messenger.receiver')[0]; + + $this->assertEquals([ + 'alias' => 'failure_transport_3', + 'is_failure_transport' => true, + ], $failureTransport3Tags); + + $failureTransportsByTransportNameServiceLocator = $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0); + $failureTransports = $container->getDefinition((string) $failureTransportsByTransportNameServiceLocator)->getArgument(0); + $expectedTransportsByFailureTransports = [ + 'failure_transport_1' => new Reference('messenger.transport.failure_transport_global'), + 'failure_transport_3' => new Reference('messenger.transport.failure_transport_global'), + 'failure_transport_global' => new Reference('messenger.transport.failure_transport_global'), + 'transport_1' => new Reference('messenger.transport.failure_transport_1'), + 'transport_2' => new Reference('messenger.transport.failure_transport_global'), + 'transport_3' => new Reference('messenger.transport.failure_transport_3'), + ]; + + $failureTransportsReferences = array_map(function (ServiceClosureArgument $serviceClosureArgument) { + $values = $serviceClosureArgument->getValues(); + + return array_shift($values); + }, $failureTransports); + $this->assertEquals($expectedTransportsByFailureTransports, $failureTransportsReferences); + } + public function testMessengerTransports() { $container = $this->createContainerFromFile('messenger_transports'); $this->assertTrue($container->hasDefinition('messenger.transport.default')); $this->assertTrue($container->getDefinition('messenger.transport.default')->hasTag('messenger.receiver')); - $this->assertEquals([['alias' => 'default']], $container->getDefinition('messenger.transport.default')->getTag('messenger.receiver')); + $this->assertEquals([ + ['alias' => 'default', 'is_failure_transport' => false], ], $container->getDefinition('messenger.transport.default')->getTag('messenger.receiver')); $transportArguments = $container->getDefinition('messenger.transport.default')->getArguments(); $this->assertEquals(new Reference('messenger.default_serializer'), $transportArguments[2]); @@ -756,7 +837,22 @@ public function testMessengerTransports() $this->assertSame(3, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(2)); $this->assertSame(100, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(3)); - $this->assertEquals(new Reference('messenger.transport.failed'), $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0)); + $failureTransportsByTransportNameServiceLocator = $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0); + $failureTransports = $container->getDefinition((string) $failureTransportsByTransportNameServiceLocator)->getArgument(0); + $expectedTransportsByFailureTransports = [ + 'beanstalkd' => new Reference('messenger.transport.failed'), + 'customised' => new Reference('messenger.transport.failed'), + 'default' => new Reference('messenger.transport.failed'), + 'failed' => new Reference('messenger.transport.failed'), + 'redis' => new Reference('messenger.transport.failed'), + ]; + + $failureTransportsReferences = array_map(function (ServiceClosureArgument $serviceClosureArgument) { + $values = $serviceClosureArgument->getValues(); + + return array_shift($values); + }, $failureTransports); + $this->assertEquals($expectedTransportsByFailureTransports, $failureTransportsReferences); } public function testMessengerRouting() diff --git a/src/Symfony/Component/Messenger/Command/AbstractFailedMessagesCommand.php b/src/Symfony/Component/Messenger/Command/AbstractFailedMessagesCommand.php index 77a6700500eed..0591ecbda990f 100644 --- a/src/Symfony/Component/Messenger/Command/AbstractFailedMessagesCommand.php +++ b/src/Symfony/Component/Messenger/Command/AbstractFailedMessagesCommand.php @@ -13,9 +13,12 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Dumper; +use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\InvalidArgumentException; use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp; use Symfony\Component\Messenger\Stamp\RedeliveryStamp; use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; @@ -35,20 +38,36 @@ */ abstract class AbstractFailedMessagesCommand extends Command { - private $receiverName; - private $receiver; + private $globalFailureReceiverName; + protected $failureTransports; - public function __construct(string $receiverName, ReceiverInterface $receiver) + public function __construct(?string $globalFailureReceiverName, $failureTransports) { - $this->receiverName = $receiverName; - $this->receiver = $receiver; + $this->failureTransports = $failureTransports; + if (!$failureTransports instanceof ServiceLocator) { + trigger_deprecation('symfony/messenger', '5.3', 'Passing a non-scalar value as 2nd argument to "%s()" is deprecated, pass a ServiceLocator instead.', __METHOD__); + + if (null === $globalFailureReceiverName) { + throw new InvalidArgumentException(sprintf('The argument "globalFailureReceiver" from method "%s()" must be not null if 2nd argument is not a ServiceLocator.', __METHOD__)); + } + + $this->failureTransports = new ServiceLocator([$globalFailureReceiverName => function () use ($failureTransports) { return $failureTransports; },]); + } + $this->globalFailureReceiverName = $globalFailureReceiverName; parent::__construct(); } protected function getReceiverName(): string { - return $this->receiverName; + trigger_deprecation('symfony/messenger', '5.3', 'The method "%s()" is deprecated, use getGlobalFailureReceiverName() instead.', __METHOD__); + + return $this->globalFailureReceiverName; + } + + protected function getGlobalFailureReceiverName(): ?string + { + return $this->globalFailureReceiverName; } /** @@ -153,9 +172,18 @@ protected function printPendingMessagesMessage(ReceiverInterface $receiver, Symf } } - protected function getReceiver(): ReceiverInterface + protected function getReceiver(/* string $name */): ReceiverInterface { - return $this->receiver; + if (1 > \func_num_args() && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { + @trigger_error(sprintf('The "%s()" method will have a new "string $name" argument in version 5.3, not defining it is deprecated since Symfony 5.2.', __METHOD__), \E_USER_DEPRECATED); + } + + $name = \func_num_args() > 0 ? func_get_arg(0) : $this->globalFailureReceiverName; + if (!$this->failureTransports->has($name)) { + throw new InvalidArgumentException(sprintf('The failure transport with name "%s" was not found. Available transports are: "%s".', $name, implode(', ', array_keys($this->failureTransports->getProvidedServices())))); + } + + return $this->failureTransports->get($name); } protected function getLastRedeliveryStampWithException(Envelope $envelope): ?RedeliveryStamp @@ -200,4 +228,27 @@ private function createCloner(): ?ClonerInterface return $cloner; } + + protected function printWarningAvailableFailureTransports(SymfonyStyle $io, ?string $failureTransportName): void + { + $failureTransports = array_keys($this->failureTransports->getProvidedServices()); + $failureTransportsCount = \count($failureTransports); + if ($failureTransportsCount > 1) { + $io->writeln([ + sprintf('> Loading messages from the global failure transport %s.', $failureTransportName), + '> To use a different failure transport, pass --transport=.', + sprintf('> Available failure transports are: %s', implode(', ', $failureTransports)), + "\n", + ]); + } + } + + protected function interactiveChooseFailureTransport(SymfonyStyle $io) + { + $failedTransports = array_keys($this->failureTransports->getProvidedServices()); + $question = new ChoiceQuestion('Select failed transport:', $failedTransports, 0); + $question->setMultiselect(false); + + return $io->askQuestion($question); + } } diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php index df21b9bcd4f5a..45bec7eb89267 100644 --- a/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php +++ b/src/Symfony/Component/Messenger/Command/FailedMessagesRemoveCommand.php @@ -38,6 +38,7 @@ protected function configure(): void ->setDefinition([ new InputArgument('id', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'Specific message id(s) to remove'), new InputOption('force', null, InputOption::VALUE_NONE, 'Force the operation without confirmation'), + new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport'), new InputOption('show-messages', null, InputOption::VALUE_NONE, 'Display messages before removing it (if multiple ids are given)'), ]) ->setDescription(self::$defaultDescription) @@ -59,20 +60,25 @@ protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); - $receiver = $this->getReceiver(); + $failureTransportName = $input->getOption('transport'); + if (null === $failureTransportName) { + $failureTransportName = $this->getGlobalFailureReceiverName(); + } + + $receiver = $this->getReceiver($failureTransportName); $shouldForce = $input->getOption('force'); $ids = (array) $input->getArgument('id'); $shouldDisplayMessages = $input->getOption('show-messages') || 1 === \count($ids); - $this->removeMessages($ids, $receiver, $io, $shouldForce, $shouldDisplayMessages); + $this->removeMessages($failureTransportName, $ids, $receiver, $io, $shouldForce, $shouldDisplayMessages); return 0; } - private function removeMessages(array $ids, ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce, bool $shouldDisplayMessages): void + private function removeMessages(string $failureTransportName, array $ids, ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce, bool $shouldDisplayMessages): void { if (!$receiver instanceof ListableReceiverInterface) { - throw new RuntimeException(sprintf('The "%s" receiver does not support removing specific messages.', $this->getReceiverName())); + throw new RuntimeException(sprintf('The "%s" receiver does not support removing specific messages.', $failureTransportName)); } foreach ($ids as $id) { diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php index 0e75c8f7c5a86..740b5dbe6638c 100644 --- a/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php +++ b/src/Symfony/Component/Messenger/Command/FailedMessagesRetryCommand.php @@ -34,6 +34,8 @@ */ class FailedMessagesRetryCommand extends AbstractFailedMessagesCommand { + private const DEFAULT_TRANSPORT_OPTION = 'choose'; + protected static $defaultName = 'messenger:failed:retry'; protected static $defaultDescription = 'Retry one or more messages from the failure transport'; @@ -41,13 +43,13 @@ class FailedMessagesRetryCommand extends AbstractFailedMessagesCommand private $messageBus; private $logger; - public function __construct(string $receiverName, ReceiverInterface $receiver, MessageBusInterface $messageBus, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger = null) + public function __construct(?string $globalReceiverName, $failureTransports, MessageBusInterface $messageBus, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger = null) { $this->eventDispatcher = $eventDispatcher; $this->messageBus = $messageBus; $this->logger = $logger; - parent::__construct($receiverName, $receiver); + parent::__construct($globalReceiverName, $failureTransports); } /** @@ -59,6 +61,7 @@ protected function configure(): void ->setDefinition([ new InputArgument('id', InputArgument::IS_ARRAY, 'Specific message id(s) to retry'), new InputOption('force', null, InputOption::VALUE_NONE, 'Force action without confirmation'), + new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION), ]) ->setDescription(self::$defaultDescription) ->setHelp(<<<'EOF' @@ -96,10 +99,19 @@ protected function execute(InputInterface $input, OutputInterface $output) $io->comment('Re-run the command with a -vv option to see logs about consumed messages.'); } - $receiver = $this->getReceiver(); + $failureTransportName = $input->getOption('transport'); + if (self::DEFAULT_TRANSPORT_OPTION === $failureTransportName) { + $this->printWarningAvailableFailureTransports($io, $this->getGlobalFailureReceiverName()); + } + if ('' === $failureTransportName || null === $failureTransportName) { + $failureTransportName = $this->interactiveChooseFailureTransport($io); + } + $failureTransportName = self::DEFAULT_TRANSPORT_OPTION === $failureTransportName ? $this->getGlobalFailureReceiverName() : $failureTransportName; + + $receiver = $this->getReceiver($failureTransportName); $this->printPendingMessagesMessage($receiver, $io); - $io->writeln(sprintf('To retry all the messages, run messenger:consume %s', $this->getReceiverName())); + $io->writeln(sprintf('To retry all the messages, run messenger:consume %s', $failureTransportName)); $shouldForce = $input->getOption('force'); $ids = $input->getArgument('id'); @@ -108,20 +120,20 @@ protected function execute(InputInterface $input, OutputInterface $output) throw new RuntimeException('Message id must be passed when in non-interactive mode.'); } - $this->runInteractive($io, $shouldForce); + $this->runInteractive($failureTransportName, $io, $shouldForce); return 0; } - $this->retrySpecificIds($ids, $io, $shouldForce); + $this->retrySpecificIds($failureTransportName, $ids, $io, $shouldForce); $io->success('All done!'); return 0; } - private function runInteractive(SymfonyStyle $io, bool $shouldForce) + private function runInteractive(string $failureTransportName, SymfonyStyle $io, bool $shouldForce) { - $receiver = $this->getReceiver(); + $receiver = $this->failureTransports->get($failureTransportName); $count = 0; if ($receiver instanceof ListableReceiverInterface) { // for listable receivers, find the messages one-by-one @@ -136,7 +148,7 @@ private function runInteractive(SymfonyStyle $io, bool $shouldForce) $id = $this->getMessageId($envelope); if (null === $id) { - throw new LogicException(sprintf('The "%s" receiver is able to list messages by id but the envelope is missing the TransportMessageIdStamp stamp.', $this->getReceiverName())); + throw new LogicException(sprintf('The "%s" receiver is able to list messages by id but the envelope is missing the TransportMessageIdStamp stamp.', $failureTransportName)); } $ids[] = $id; } @@ -146,11 +158,11 @@ private function runInteractive(SymfonyStyle $io, bool $shouldForce) break; } - $this->retrySpecificIds($ids, $io, $shouldForce); + $this->retrySpecificIds($failureTransportName, $ids, $io, $shouldForce); } } else { // get() and ask messages one-by-one - $count = $this->runWorker($this->getReceiver(), $io, $shouldForce); + $count = $this->runWorker($failureTransportName, $receiver, $io, $shouldForce); } // avoid success message if nothing was processed @@ -159,7 +171,7 @@ private function runInteractive(SymfonyStyle $io, bool $shouldForce) } } - private function runWorker(ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce): int + private function runWorker(string $failureTransportName, ReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce): int { $count = 0; $listener = function (WorkerMessageReceivedEvent $messageReceivedEvent) use ($io, $receiver, $shouldForce, &$count) { @@ -180,7 +192,7 @@ private function runWorker(ReceiverInterface $receiver, SymfonyStyle $io, bool $ $this->eventDispatcher->addListener(WorkerMessageReceivedEvent::class, $listener); $worker = new Worker( - [$this->getReceiverName() => $receiver], + [$failureTransportName => $receiver], $this->messageBus, $this->eventDispatcher, $this->logger @@ -195,12 +207,12 @@ private function runWorker(ReceiverInterface $receiver, SymfonyStyle $io, bool $ return $count; } - private function retrySpecificIds(array $ids, SymfonyStyle $io, bool $shouldForce) + private function retrySpecificIds(string $failureTransportName, array $ids, SymfonyStyle $io, bool $shouldForce) { - $receiver = $this->getReceiver(); + $receiver = $this->getReceiver($failureTransportName); if (!$receiver instanceof ListableReceiverInterface) { - throw new RuntimeException(sprintf('The "%s" receiver does not support retrying messages by id.', $this->getReceiverName())); + throw new RuntimeException(sprintf('The "%s" receiver does not support retrying messages by id.', $failureTransportName)); } foreach ($ids as $id) { @@ -210,7 +222,7 @@ private function retrySpecificIds(array $ids, SymfonyStyle $io, bool $shouldForc } $singleReceiver = new SingleMessageReceiver($receiver, $envelope); - $this->runWorker($singleReceiver, $io, $shouldForce); + $this->runWorker($failureTransportName, $singleReceiver, $io, $shouldForce); } } } diff --git a/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php b/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php index 9e8f23d759208..c292549e3a9c4 100644 --- a/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php +++ b/src/Symfony/Component/Messenger/Command/FailedMessagesShowCommand.php @@ -27,6 +27,8 @@ */ class FailedMessagesShowCommand extends AbstractFailedMessagesCommand { + private const DEFAULT_TRANSPORT_OPTION = 'choose'; + protected static $defaultName = 'messenger:failed:show'; protected static $defaultDescription = 'Show one or more messages from the failure transport'; @@ -39,6 +41,7 @@ protected function configure(): void ->setDefinition([ new InputArgument('id', InputArgument::OPTIONAL, 'Specific message id to show'), new InputOption('max', null, InputOption::VALUE_REQUIRED, 'Maximum number of messages to list', 50), + new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION), ]) ->setDescription(self::$defaultDescription) ->setHelp(<<<'EOF' @@ -61,26 +64,36 @@ protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); - $receiver = $this->getReceiver(); + $failureTransportName = $input->getOption('transport'); + if (self::DEFAULT_TRANSPORT_OPTION === $failureTransportName) { + $this->printWarningAvailableFailureTransports($io, $this->getGlobalFailureReceiverName()); + } + if ('' === $failureTransportName || null === $failureTransportName) { + $failureTransportName = $this->interactiveChooseFailureTransport($io); + } + $failureTransportName = self::DEFAULT_TRANSPORT_OPTION === $failureTransportName ? $this->getGlobalFailureReceiverName() : $failureTransportName; + + $receiver = $this->getReceiver($failureTransportName); + $this->printPendingMessagesMessage($receiver, $io); if (!$receiver instanceof ListableReceiverInterface) { - throw new RuntimeException(sprintf('The "%s" receiver does not support listing or showing specific messages.', $this->getReceiverName())); + throw new RuntimeException(sprintf('The "%s" receiver does not support listing or showing specific messages.', $failureTransportName)); } if (null === $id = $input->getArgument('id')) { - $this->listMessages($io, $input->getOption('max')); + $this->listMessages($failureTransportName, $io, $input->getOption('max')); } else { - $this->showMessage($id, $io); + $this->showMessage($failureTransportName, $id, $io); } return 0; } - private function listMessages(SymfonyStyle $io, int $max) + private function listMessages(?string $failedTransportName, SymfonyStyle $io, int $max) { /** @var ListableReceiverInterface $receiver */ - $receiver = $this->getReceiver(); + $receiver = $this->getReceiver($failedTransportName); $envelopes = $receiver->all($max); $rows = []; @@ -119,13 +132,13 @@ private function listMessages(SymfonyStyle $io, int $max) $io->comment(sprintf('Showing first %d messages.', $max)); } - $io->comment('Run messenger:failed:show {id} -vv to see message details.'); + $io->comment(sprintf('Run messenger:failed:show {id} --transport=%s -vv to see message details.', $failedTransportName)); } - private function showMessage(string $id, SymfonyStyle $io) + private function showMessage(?string $failedTransportName, string $id, SymfonyStyle $io) { /** @var ListableReceiverInterface $receiver */ - $receiver = $this->getReceiver(); + $receiver = $this->getReceiver($failedTransportName); $envelope = $receiver->find($id); if (null === $envelope) { throw new RuntimeException(sprintf('The message "%s" was not found.', $id)); @@ -135,8 +148,8 @@ private function showMessage(string $id, SymfonyStyle $io) $io->writeln([ '', - sprintf(' Run messenger:failed:retry %s to retry this message.', $id), - sprintf(' Run messenger:failed:remove %s to delete it.', $id), + sprintf(' Run messenger:failed:retry %s --transport=%s to retry this message.', $id, $failedTransportName), + sprintf(' Run messenger:failed:remove %s --transport=%s to delete it.', $id, $failedTransportName), ]); } } diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index 5b77326270371..1a64dac475b02 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -125,7 +125,7 @@ private function registerHandlers(ContainerBuilder $container, array $busIds) if (!\in_array($options['bus'], $busIds)) { $messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : ($r->implementsInterface(MessageSubscriberInterface::class) ? sprintf('returned by method "%s::getHandledMessages()"', $r->getName()) : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method)); - throw new RuntimeException(sprintf('Invalid configuration %s for message "%s": bus "%s" does not exist.', $messageLocation, $message, $options['bus'])); + throw new RuntimeException(sprintf('Invalid configuration "%s" for message "%s": bus "%s" does not exist.', $messageLocation, $message, $options['bus'])); } $buses = [$options['bus']]; @@ -134,7 +134,7 @@ private function registerHandlers(ContainerBuilder $container, array $busIds) if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) { $messageLocation = isset($tag['handles']) ? 'declared in your tag attribute "handles"' : ($r->implementsInterface(MessageSubscriberInterface::class) ? sprintf('returned by method "%s::getHandledMessages()"', $r->getName()) : sprintf('used as argument type in method "%s::%s()"', $r->getName(), $method)); - throw new RuntimeException(sprintf('Invalid handler service "%s": class or interface "%s" %s not found.', $serviceId, $message, $messageLocation)); + throw new RuntimeException(sprintf('Invalid handler service "%s": class or interface "%s" "%s" not found.', $serviceId, $message, $messageLocation)); } if (!$r->hasMethod($method)) { @@ -255,6 +255,14 @@ private function guessHandledClasses(\ReflectionClass $handlerClass, string $ser private function registerReceivers(ContainerBuilder $container, array $busIds) { $receiverMapping = []; + $failureTransportsMap = []; + if ($container->hasDefinition('console.command.messenger_failed_messages_retry')) { + $commandDefinition = $container->getDefinition('console.command.messenger_failed_messages_retry'); + $globalReceiverName = $commandDefinition->getArgument(0); + if (null !== $globalReceiverName) { + $failureTransportsMap[$commandDefinition->getArgument(0)] = new Reference('messenger.failure_transports.default'); + } + } foreach ($container->findTaggedServiceIds($this->receiverTag) as $id => $tags) { $receiverClass = $this->getServiceClass($container, $id); @@ -267,6 +275,9 @@ private function registerReceivers(ContainerBuilder $container, array $busIds) foreach ($tags as $tag) { if (isset($tag['alias'])) { $receiverMapping[$tag['alias']] = $receiverMapping[$id]; + if ($tag['is_failure_transport'] ?? false) { + $failureTransportsMap[$tag['alias']] = $receiverMapping[$id]; + } } } } @@ -303,6 +314,8 @@ private function registerReceivers(ContainerBuilder $container, array $busIds) $container->getDefinition('messenger.receiver_locator')->replaceArgument(0, $receiverMapping); + $failureTransportsLocator = ServiceLocatorTagPass::register($container, $failureTransportsMap); + $failedCommandIds = [ 'console.command.messenger_failed_messages_retry', 'console.command.messenger_failed_messages_show', @@ -311,7 +324,7 @@ private function registerReceivers(ContainerBuilder $container, array $busIds) foreach ($failedCommandIds as $failedCommandId) { if ($container->hasDefinition($failedCommandId)) { $definition = $container->getDefinition($failedCommandId); - $definition->replaceArgument(1, $receiverMapping[$definition->getArgument(0)]); + $definition->replaceArgument(1, $failureTransportsLocator); } } } diff --git a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php index 729dd5fec4409..afbc3c4b373d3 100644 --- a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php +++ b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageToFailureTransportListener.php @@ -10,6 +10,7 @@ namespace Symfony\Component\Messenger\EventListener; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; @@ -25,12 +26,19 @@ */ class SendFailedMessageToFailureTransportListener implements EventSubscriberInterface { - private $failureSender; + private $failureSenders; private $logger; - public function __construct(SenderInterface $failureSender, LoggerInterface $logger = null) + /** + * @param ContainerInterface $failureSenders + */ + public function __construct($failureSenders, LoggerInterface $logger = null) { - $this->failureSender = $failureSender; + if (!$failureSenders instanceof ContainerInterface) { + trigger_deprecation('symfony/messenger', '5.3', 'Passing a SenderInterface value as 1st argument to "%s()" is deprecated, pass a ServiceLocator instead.', __METHOD__); + } + + $this->failureSenders = $failureSenders; $this->logger = $logger; } @@ -40,6 +48,15 @@ public function onMessageFailed(WorkerMessageFailedEvent $event) return; } + if (!$this->hasFailureTransports($event)) { + return; + } + + $failureSender = $this->getFailureSender($event->getReceiverName()); + if (null === $failureSender) { + return; + } + $envelope = $event->getEnvelope(); // avoid re-sending to the failed sender @@ -56,11 +73,11 @@ public function onMessageFailed(WorkerMessageFailedEvent $event) if (null !== $this->logger) { $this->logger->info('Rejected message {class} will be sent to the failure transport {transport}.', [ 'class' => \get_class($envelope->getMessage()), - 'transport' => \get_class($this->failureSender), + 'transport' => \get_class($failureSender), ]); } - $this->failureSender->send($envelope); + $failureSender->send($envelope); } public static function getSubscribedEvents() @@ -69,4 +86,18 @@ public static function getSubscribedEvents() WorkerMessageFailedEvent::class => ['onMessageFailed', -100], ]; } + + private function getFailureSender(string $receiverName): SenderInterface + { + if ($this->failureSenders instanceof SenderInterface) { + return $this->failureSenders; + } + + return $this->failureSenders->get($receiverName); + } + + private function hasFailureTransports(WorkerMessageFailedEvent $event): bool + { + return ($this->failureSenders instanceof ContainerInterface && $this->failureSenders->has($event->getReceiverName())) || $this->failureSenders instanceof SenderInterface; + } } diff --git a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php index ba5e9cff00879..08dc2a0e1d920 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRemoveCommandTest.php @@ -13,12 +13,17 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand; use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\InvalidArgumentException; use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; class FailedMessagesRemoveCommandTest extends TestCase { + /** + * @group legacy + */ public function testRemoveSingleMessage() { $receiver = $this->createMock(ListableReceiverInterface::class); @@ -36,6 +41,30 @@ public function testRemoveSingleMessage() $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay()); } + public function testRemoveSingleMessageWithServiceLocator() + { + $globalFailureReceiverName = 'failure_receiver'; + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('find')->with(20)->willReturn(new Envelope(new \stdClass())); + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->expects($this->once())->method('has')->with($globalFailureReceiverName)->willReturn(true); + $serviceLocator->expects($this->any())->method('get')->with($globalFailureReceiverName)->willReturn($receiver); + + $command = new FailedMessagesRemoveCommand( + $globalFailureReceiverName, + $serviceLocator + ); + + $tester = new CommandTester($command); + $tester->execute(['id' => 20, '--force' => true]); + + $this->assertStringContainsString('Failed Message Details', $tester->getDisplay()); + $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay()); + } + + /** + * @group legacy + */ public function testRemoveUniqueMessage() { $receiver = $this->createMock(ListableReceiverInterface::class); @@ -53,6 +82,92 @@ public function testRemoveUniqueMessage() $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay()); } + public function testRemoveUniqueMessageWithServiceLocator() + { + $globalFailureReceiverName = 'failure_receiver'; + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('find')->with(20)->willReturn(new Envelope(new \stdClass())); + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->expects($this->once())->method('has')->with($globalFailureReceiverName)->willReturn(true); + $serviceLocator->expects($this->any())->method('get')->with($globalFailureReceiverName)->willReturn($receiver); + + $command = new FailedMessagesRemoveCommand( + $globalFailureReceiverName, + $serviceLocator + ); + + $tester = new CommandTester($command); + $tester->execute(['id' => [20], '--force' => true]); + + $this->assertStringContainsString('Failed Message Details', $tester->getDisplay()); + $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay()); + } + + public function testRemoveUniqueMessageWithServiceLocatorFromSpecificFailureTransport() + { + $failureReveiverName = 'specific_failure_receiver'; + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('find')->with(20)->willReturn(new Envelope(new \stdClass())); + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->expects($this->once())->method('has')->with($failureReveiverName)->willReturn(true); + $serviceLocator->expects($this->any())->method('get')->with($failureReveiverName)->willReturn($receiver); + + $command = new FailedMessagesRemoveCommand( + $failureReveiverName, + $serviceLocator + ); + + $tester = new CommandTester($command); + $tester->execute(['id' => [20], '--transport' => $failureReveiverName, '--force' => true]); + + $this->assertStringContainsString('Failed Message Details', $tester->getDisplay()); + $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay()); + } + + /** + * @group legacy + */ + public function testThrowExceptionIfFailureTransportNotDefined() + { + $failureReceiverName = 'failure_receiver'; + $receiver = $this->createMock(ListableReceiverInterface::class); + + $this->expectException(InvalidArgumentException::class); + $command = new FailedMessagesRemoveCommand( + null, + $receiver + ); + + $tester = new CommandTester($command); + $tester->execute(['id' => [20], '--transport' => $failureReceiverName, '--force' => true]); + + $this->assertStringContainsString('Failed Message Details', $tester->getDisplay()); + $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay()); + } + + public function testThrowExceptionIfFailureTransportNotDefinedWithServiceLocator() + { + $failureReceiverName = 'failure_receiver'; + + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->expects($this->once())->method('has')->with($failureReceiverName)->willReturn(false); + + $this->expectException(InvalidArgumentException::class); + $command = new FailedMessagesRemoveCommand( + $failureReceiverName, + $serviceLocator + ); + + $tester = new CommandTester($command); + $tester->execute(['id' => [20], '--transport' => $failureReceiverName, '--force' => true]); + + $this->assertStringContainsString('Failed Message Details', $tester->getDisplay()); + $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay()); + } + + /** + * @group legacy + */ public function testRemoveMultipleMessages() { $receiver = $this->createMock(ListableReceiverInterface::class); @@ -76,6 +191,37 @@ public function testRemoveMultipleMessages() $this->assertStringContainsString('Message with id 40 removed.', $tester->getDisplay()); } + public function testRemoveMultipleMessagesWithServiceLocator() + { + $globalFailureReceiverName = 'failure_receiver'; + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->exactly(3))->method('find')->withConsecutive([20], [30], [40])->willReturnOnConsecutiveCalls( + new Envelope(new \stdClass()), + null, + new Envelope(new \stdClass()) + ); + + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->expects($this->once())->method('has')->with($globalFailureReceiverName)->willReturn(true); + $serviceLocator->expects($this->any())->method('get')->with($globalFailureReceiverName)->willReturn($receiver); + + $command = new FailedMessagesRemoveCommand( + $globalFailureReceiverName, + $serviceLocator + ); + + $tester = new CommandTester($command); + $tester->execute(['id' => [20, 30, 40], '--force' => true]); + + $this->assertStringNotContainsString('Failed Message Details', $tester->getDisplay()); + $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay()); + $this->assertStringContainsString('The message with id "30" was not found.', $tester->getDisplay()); + $this->assertStringContainsString('Message with id 40 removed.', $tester->getDisplay()); + } + + /** + * @group legacy + */ public function testRemoveMultipleMessagesAndDisplayMessages() { $receiver = $this->createMock(ListableReceiverInterface::class); @@ -96,4 +242,30 @@ public function testRemoveMultipleMessagesAndDisplayMessages() $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay()); $this->assertStringContainsString('Message with id 30 removed.', $tester->getDisplay()); } + + public function testRemoveMultipleMessagesAndDisplayMessagesWithServiceLocator() + { + $globalFailureReceiverName = 'failure_receiver'; + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->exactly(2))->method('find')->withConsecutive([20], [30])->willReturnOnConsecutiveCalls( + new Envelope(new \stdClass()), + new Envelope(new \stdClass()) + ); + + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->expects($this->once())->method('has')->with($globalFailureReceiverName)->willReturn(true); + $serviceLocator->expects($this->any())->method('get')->with($globalFailureReceiverName)->willReturn($receiver); + + $command = new FailedMessagesRemoveCommand( + $globalFailureReceiverName, + $serviceLocator + ); + + $tester = new CommandTester($command); + $tester->execute(['id' => [20, 30], '--force' => true, '--show-messages' => true]); + + $this->assertStringContainsString('Failed Message Details', $tester->getDisplay()); + $this->assertStringContainsString('Message with id 20 removed.', $tester->getDisplay()); + $this->assertStringContainsString('Message with id 30 removed.', $tester->getDisplay()); + } } diff --git a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php index 901f70e1e1d69..e815707f80fc7 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesRetryCommandTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Messenger\Command\FailedMessagesRetryCommand; use Symfony\Component\Messenger\Envelope; @@ -21,6 +22,9 @@ class FailedMessagesRetryCommandTest extends TestCase { + /** + * @group legacy + */ public function testBasicRun() { $receiver = $this->createMock(ListableReceiverInterface::class); @@ -45,4 +49,99 @@ public function testBasicRun() $this->assertStringContainsString('[OK]', $tester->getDisplay()); } + + public function testBasicRunWithServiceLocator() + { + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->exactly(2))->method('find')->withConsecutive([10], [12])->willReturn(new Envelope(new \stdClass())); + // message will eventually be ack'ed in Worker + $receiver->expects($this->exactly(2))->method('ack'); + + $dispatcher = new EventDispatcher(); + $bus = $this->createMock(MessageBusInterface::class); + // the bus should be called in the worker + $bus->expects($this->exactly(2))->method('dispatch')->willReturn(new Envelope(new \stdClass())); + + $failureTransportName = 'failure_receiver'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + + $command = new FailedMessagesRetryCommand( + $failureTransportName, + $serviceLocator, + $bus, + $dispatcher + ); + + $tester = new CommandTester($command); + $tester->execute(['id' => [10, 12], '--force' => true]); + + $this->assertStringContainsString('[OK]', $tester->getDisplay()); + $this->assertStringNotContainsString('Available failure transports are:', $tester->getDisplay()); + } + + public function testBasicRunWithServiceLocatorMultipleFailedTransportsDefined() + { + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->method('all')->willReturn([]); + + $dispatcher = new EventDispatcher(); + $bus = $this->createMock(MessageBusInterface::class); + + $failureTransportName = 'failure_receiver'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + $serviceLocator->method('getProvidedServices')->willReturn([ + 'failure_receiver' => [], + 'failure_receiver_2' => [], + 'failure_receiver_3' => [], + ]); + + $command = new FailedMessagesRetryCommand( + $failureTransportName, + $serviceLocator, + $bus, + $dispatcher + ); + $tester = new CommandTester($command); + $tester->setInputs([0]); + $tester->execute(['--force' => true]); + + $expectedLadingMessage = << Available failure transports are: failure_receiver, failure_receiver_2, failure_receiver_3 +EOF; + $this->assertStringContainsString($expectedLadingMessage, $tester->getDisplay()); + } + + public function testBasicRunWithServiceLocatorWithSpecificFailureTransport() + { + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->exactly(2))->method('find')->withConsecutive([10], [12])->willReturn(new Envelope(new \stdClass())); + // message will eventually be ack'ed in Worker + $receiver->expects($this->exactly(2))->method('ack'); + + $dispatcher = new EventDispatcher(); + $bus = $this->createMock(MessageBusInterface::class); + // the bus should be called in the worker + $bus->expects($this->exactly(2))->method('dispatch')->willReturn(new Envelope(new \stdClass())); + + $failureTransportName = 'failure_receiver'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + + $command = new FailedMessagesRetryCommand( + $failureTransportName, + $serviceLocator, + $bus, + $dispatcher + ); + + $tester = new CommandTester($command); + $tester->execute(['id' => [10, 12], '--transport' => $failureTransportName, '--force' => true]); + + $this->assertStringContainsString('[OK]', $tester->getDisplay()); + } } diff --git a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php index 6c6f110f31b88..5afd452eba82f 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Messenger\Command\FailedMessagesShowCommand; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp; @@ -28,6 +29,9 @@ */ class FailedMessagesShowCommandTest extends TestCase { + /** + * @group legacy + */ public function testBasicRun() { $sentToFailureStamp = new SentToFailureTransportStamp('async'); @@ -51,7 +55,7 @@ public function testBasicRun() $tester->execute(['id' => 15]); $this->assertStringContainsString(sprintf(<<getDisplay(true)); } + public function testBasicRunWithServiceLocator() + { + $sentToFailureStamp = new SentToFailureTransportStamp('async'); + $redeliveryStamp = new RedeliveryStamp(0); + $errorStamp = ErrorDetailsStamp::create(new \Exception('Things are bad!', 123)); + $envelope = new Envelope(new \stdClass(), [ + new TransportMessageIdStamp(15), + $sentToFailureStamp, + $redeliveryStamp, + $errorStamp, + ]); + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('find')->with(15)->willReturn($envelope); + + $failureTransportName = 'failure_receiver'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + + $command = new FailedMessagesShowCommand( + $failureTransportName, + $serviceLocator + ); + + $tester = new CommandTester($command); + $tester->execute(['id' => 15]); + + $this->assertStringContainsString(sprintf(<<getRedeliveredAt()->format('Y-m-d H:i:s')), + $tester->getDisplay(true)); + } + + /** + * @group legacy + */ public function testMultipleRedeliveryFails() { $sentToFailureStamp = new SentToFailureTransportStamp('async'); @@ -101,6 +150,48 @@ public function testMultipleRedeliveryFails() $tester->getDisplay(true)); } + public function testMultipleRedeliveryFailsWithServiceLocator() + { + $sentToFailureStamp = new SentToFailureTransportStamp('async'); + $redeliveryStamp1 = new RedeliveryStamp(0); + $errorStamp = ErrorDetailsStamp::create(new \Exception('Things are bad!', 123)); + $redeliveryStamp2 = new RedeliveryStamp(0); + $envelope = new Envelope(new \stdClass(), [ + new TransportMessageIdStamp(15), + $sentToFailureStamp, + $redeliveryStamp1, + $errorStamp, + $redeliveryStamp2, + ]); + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('find')->with(15)->willReturn($envelope); + + $failureTransportName = 'failure_receiver'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + + $command = new FailedMessagesShowCommand( + $failureTransportName, + $serviceLocator + ); + $tester = new CommandTester($command); + $tester->execute(['id' => 15]); + $this->assertStringContainsString(sprintf(<<getRedeliveredAt()->format('Y-m-d H:i:s')), + $tester->getDisplay(true)); + } + /** * @group legacy */ @@ -136,6 +227,9 @@ public function testLegacyFallback() $tester->getDisplay(true)); } + /** + * @group legacy + */ public function testReceiverShouldBeListable() { $receiver = $this->createMock(ReceiverInterface::class); @@ -150,6 +244,28 @@ public function testReceiverShouldBeListable() $tester->execute(['id' => 15]); } + public function testReceiverShouldBeListableWithServiceLocator() + { + $receiver = $this->createMock(ReceiverInterface::class); + $failureTransportName = 'failure_receiver'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + + $command = new FailedMessagesShowCommand( + $failureTransportName, + $serviceLocator + ); + + $this->expectExceptionMessage('The "failure_receiver" receiver does not support listing or showing specific messages.'); + + $tester = new CommandTester($command); + $tester->execute(['id' => 15]); + } + + /** + * @group legacy + */ public function testListMessages() { $sentToFailureStamp = new SentToFailureTransportStamp('async'); @@ -179,6 +295,56 @@ public function testListMessages() $tester->getDisplay(true)); } + public function testListMessagesWithServiceLocator() + { + $sentToFailureStamp = new SentToFailureTransportStamp('async'); + $redeliveryStamp = new RedeliveryStamp(0); + $errorStamp = ErrorDetailsStamp::create(new \RuntimeException('Things are bad!')); + $envelope = new Envelope(new \stdClass(), [ + new TransportMessageIdStamp(15), + $sentToFailureStamp, + $redeliveryStamp, + $errorStamp, + ]); + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]); + + $failureTransportName = 'failure_receiver'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + $serviceLocator->method('getProvidedServices')->willReturn([ + $failureTransportName => [], + 'failure_receiver_2' => [], + 'failure_receiver_3' => [], + ]); + $command = new FailedMessagesShowCommand( + $failureTransportName, + $serviceLocator + ); + $tester = new CommandTester($command); + $tester->setInputs([0]); + $tester->execute([]); + + $this->assertStringContainsString(sprintf(<<getRedeliveredAt()->format('Y-m-d H:i:s')), + $tester->getDisplay(true)); + + $expectedLoadingMessage = << Available failure transports are: failure_receiver, failure_receiver_2, failure_receiver_3 +EOF; + + $this->assertStringContainsString($expectedLoadingMessage, $tester->getDisplay()); + $this->assertStringContainsString('Run messenger:failed:show {id} --transport=failure_receiver -vv to see message details.', $tester->getDisplay()); + + } + + /** + * @group legacy + */ public function testListMessagesReturnsNoMessagesFound() { $receiver = $this->createMock(ListableReceiverInterface::class); @@ -194,6 +360,28 @@ public function testListMessagesReturnsNoMessagesFound() $this->assertStringContainsString('[OK] No failed messages were found.', $tester->getDisplay(true)); } + public function testListMessagesReturnsNoMessagesFoundWithServiceLocator() + { + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('all')->with()->willReturn([]); + $failureTransportName = 'failure_receiver'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + + $command = new FailedMessagesShowCommand( + $failureTransportName, + $serviceLocator + ); + + $tester = new CommandTester($command); + $tester->execute([]); + $this->assertStringContainsString('[OK] No failed messages were found.', $tester->getDisplay(true)); + } + + /** + * @group legacy + */ public function testListMessagesReturnsPaginatedMessages() { $sentToFailureStamp = new SentToFailureTransportStamp('async'); @@ -216,6 +404,36 @@ public function testListMessagesReturnsPaginatedMessages() $this->assertStringContainsString('Showing first 1 messages.', $tester->getDisplay(true)); } + public function testListMessagesReturnsPaginatedMessagesWithServiceLocator() + { + $sentToFailureStamp = new SentToFailureTransportStamp('async'); + $envelope = new Envelope(new \stdClass(), [ + new TransportMessageIdStamp(15), + $sentToFailureStamp, + new RedeliveryStamp(0), + ErrorDetailsStamp::create(new \RuntimeException('Things are bad!')), + ]); + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]); + + $failureTransportName = 'failure_receiver'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + + $command = new FailedMessagesShowCommand( + $failureTransportName, + $serviceLocator + ); + + $tester = new CommandTester($command); + $tester->execute(['--max' => 1]); + $this->assertStringContainsString('Showing first 1 messages.', $tester->getDisplay(true)); + } + + /** + * @group legacy + */ public function testInvalidMessagesThrowsException() { $sentToFailureStamp = new SentToFailureTransportStamp('async'); @@ -236,6 +454,29 @@ public function testInvalidMessagesThrowsException() $tester->execute(['id' => 15]); } + public function testInvalidMessagesThrowsExceptionWithServiceLocator() + { + $receiver = $this->createMock(ListableReceiverInterface::class); + + $failureTransportName = 'failure_receiver'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + + $command = new FailedMessagesShowCommand( + $failureTransportName, + $serviceLocator + ); + + $this->expectExceptionMessage('The message "15" was not found.'); + + $tester = new CommandTester($command); + $tester->execute(['id' => 15]); + } + + /** + * @group legacy + */ public function testVeryVerboseOutputForSingleMessageContainsExceptionWithTrace() { $exception = new \RuntimeException('Things are bad!'); @@ -275,4 +516,83 @@ public function testVeryVerboseOutputForSingleMessageContainsExceptionWithTrace( __FILE__, $exceptionLine, $exceptionLine), $tester->getDisplay(true)); } + + public function testVeryVerboseOutputForSingleMessageContainsExceptionWithTraceWithServiceLocator() + { + $exception = new \RuntimeException('Things are bad!'); + $exceptionLine = __LINE__ - 1; + $envelope = new Envelope(new \stdClass(), [ + new TransportMessageIdStamp(15), + new SentToFailureTransportStamp('async'), + new RedeliveryStamp(0), + ErrorDetailsStamp::create($exception), + ]); + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('find')->with(42)->willReturn($envelope); + + $failureTransportName = 'failure_receiver'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + + $command = new FailedMessagesShowCommand($failureTransportName, $serviceLocator); + $tester = new CommandTester($command); + $tester->execute(['id' => 42], ['verbosity' => OutputInterface::VERBOSITY_VERY_VERBOSE]); + $this->assertStringMatchesFormat(sprintf(<<<'EOF' +%%A +Exception: +========== + +RuntimeException { + message: "Things are bad!" + code: 0 + file: "%s" + line: %d + trace: { + %%s%%eTests%%eCommand%%eFailedMessagesShowCommandTest.php:%d { + Symfony\Component\Messenger\Tests\Command\FailedMessagesShowCommandTest->testVeryVerboseOutputForSingleMessageContainsExceptionWithTraceWithServiceLocator() + › { + › $exception = new \RuntimeException('Things are bad!'); + › $exceptionLine = __LINE__ - 1; + } +%%A +EOF + , + __FILE__, $exceptionLine, $exceptionLine), + $tester->getDisplay(true)); + } + + public function testListMessagesWithServiceLocatorFromSpecificTransport() + { + $sentToFailureStamp = new SentToFailureTransportStamp('async'); + $redeliveryStamp = new RedeliveryStamp(0); + $errorStamp = ErrorDetailsStamp::create(new \RuntimeException('Things are bad!')); + $envelope = new Envelope(new \stdClass(), [ + new TransportMessageIdStamp(15), + $sentToFailureStamp, + $redeliveryStamp, + $errorStamp, + ]); + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]); + + $failureTransportName = 'failure_receiver_another'; + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($failureTransportName)->willReturn(true); + $serviceLocator->method('get')->with($failureTransportName)->willReturn($receiver); + + $command = new FailedMessagesShowCommand( + 'global_but_not_used', + $serviceLocator + ); + + $tester = new CommandTester($command); + $tester->execute(['--transport' => $failureTransportName]); + $this->assertStringContainsString(sprintf(<<getRedeliveredAt()->format('Y-m-d H:i:s')), + $tester->getDisplay(true)); + } } diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php index 580b588d755c4..1235bbbdcbd1d 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -22,6 +22,8 @@ use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpReceiver; use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; use Symfony\Component\Messenger\Command\DebugCommand; +use Symfony\Component\Messenger\Command\FailedMessagesRetryCommand; +use Symfony\Component\Messenger\Command\FailedMessagesShowCommand; use Symfony\Component\Messenger\Command\SetupTransportsCommand; use Symfony\Component\Messenger\DataCollector\MessengerDataCollector; use Symfony\Component\Messenger\DependencyInjection\MessengerPass; @@ -402,7 +404,7 @@ public function testItRegistersHandlersOnDifferentBuses() public function testItThrowsAnExceptionOnUnknownBus() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Invalid configuration returned by method "Symfony\Component\Messenger\Tests\DependencyInjection\HandlerOnUndefinedBus::getHandledMessages()" for message "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage": bus "some_undefined_bus" does not exist.'); + $this->expectExceptionMessage('Invalid configuration "returned by method "Symfony\Component\Messenger\Tests\DependencyInjection\HandlerOnUndefinedBus::getHandledMessages()"" for message "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage": bus "some_undefined_bus" does not exist.'); $container = $this->getContainerBuilder(); $container ->register(HandlerOnUndefinedBus::class, HandlerOnUndefinedBus::class) @@ -415,7 +417,7 @@ public function testItThrowsAnExceptionOnUnknownBus() public function testUndefinedMessageClassForHandler() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" used as argument type in method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler::__invoke()" not found.'); + $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" "used as argument type in method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandler::__invoke()"" not found.'); $container = $this->getContainerBuilder(); $container ->register(UndefinedMessageHandler::class, UndefinedMessageHandler::class) @@ -428,7 +430,7 @@ public function testUndefinedMessageClassForHandler() public function testUndefinedMessageClassForHandlerImplementingMessageHandlerInterface() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaHandlerInterface": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" used as argument type in method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaHandlerInterface::__invoke()" not found.'); + $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaHandlerInterface": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" "used as argument type in method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaHandlerInterface::__invoke()"" not found.'); $container = $this->getContainerBuilder(); $container ->register(UndefinedMessageHandlerViaHandlerInterface::class, UndefinedMessageHandlerViaHandlerInterface::class) @@ -441,7 +443,7 @@ public function testUndefinedMessageClassForHandlerImplementingMessageHandlerInt public function testUndefinedMessageClassForHandlerImplementingMessageSubscriberInterface() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaSubscriberInterface": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" returned by method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaSubscriberInterface::getHandledMessages()" not found.'); + $this->expectExceptionMessage('Invalid handler service "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaSubscriberInterface": class or interface "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessage" "returned by method "Symfony\Component\Messenger\Tests\DependencyInjection\UndefinedMessageHandlerViaSubscriberInterface::getHandledMessages()"" not found.'); $container = $this->getContainerBuilder(); $container ->register(UndefinedMessageHandlerViaSubscriberInterface::class, UndefinedMessageHandlerViaSubscriberInterface::class) @@ -693,6 +695,34 @@ private function assertHandlerDescriptor(ContainerBuilder $container, array $map $this->assertEquals($options[$index] ?? [], $definitionArguments[1]); } } + + public function testFailedCommandsRegisteredWithServiceLocatorArgumentReplaced() + { + $globalReceiverName = 'global_failure_transport'; + $container = $this->getContainerBuilder($messageBusId = 'message_bus'); + + $container->register('console.command.messenger_failed_messages_retry', FailedMessagesRetryCommand::class) + ->setArgument(0, $globalReceiverName) + ->setArgument(1, null) + ->setArgument(2, new Reference($messageBusId)); + $container->register('console.command.messenger_failed_messages_show', FailedMessagesShowCommand::class) + ->setArgument(0, $globalReceiverName) + ->setArgument(1, null); + $container->register('console.command.messenger_failed_messages_remove', FailedMessagesRetryCommand::class) + ->setArgument(0, $globalReceiverName) + ->setArgument(1, null); + + (new MessengerPass())->process($container); + + $retryDefinition = $container->getDefinition('console.command.messenger_failed_messages_retry'); + $this->assertNotNull($retryDefinition->getArgument(1)); + + $showDefinition = $container->getDefinition('console.command.messenger_failed_messages_show'); + $this->assertNotNull($showDefinition->getArgument(1)); + + $removeDefinition = $container->getDefinition('console.command.messenger_failed_messages_remove'); + $this->assertNotNull($removeDefinition->getArgument(1)); + } } class DummyHandler diff --git a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageToFailureTransportListenerTest.php b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageToFailureTransportListenerTest.php index c489ac45e33c4..6ef74fef5cdb3 100644 --- a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageToFailureTransportListenerTest.php +++ b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageToFailureTransportListenerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\EventListener\SendFailedMessageToFailureTransportListener; @@ -20,6 +21,9 @@ class SendFailedMessageToFailureTransportListenerTest extends TestCase { + /** + * @group legacy + */ public function testItSendsToTheFailureTransport() { $sender = $this->createMock(SenderInterface::class); @@ -43,11 +47,56 @@ public function testItSendsToTheFailureTransport() $listener->onMessageFailed($event); } + public function testItSendsToTheFailureTransportWithSenderLocator() + { + $receiverName = 'my_receiver'; + $sender = $this->createMock(SenderInterface::class); + $sender->expects($this->once())->method('send')->with($this->callback(function ($envelope) use ($receiverName) { + /* @var Envelope $envelope */ + $this->assertInstanceOf(Envelope::class, $envelope); + + /** @var SentToFailureTransportStamp $sentToFailureTransportStamp */ + $sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class); + $this->assertNotNull($sentToFailureTransportStamp); + $this->assertSame($receiverName, $sentToFailureTransportStamp->getOriginalReceiverName()); + + return true; + }))->willReturnArgument(0); + + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->expects($this->once())->method('has')->willReturn(true); + $serviceLocator->expects($this->once())->method('get')->with($receiverName)->willReturn($sender); + $listener = new SendFailedMessageToFailureTransportListener($serviceLocator); + + $exception = new \Exception('no!'); + $envelope = new Envelope(new \stdClass()); + $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception); + + $listener->onMessageFailed($event); + } + + /** + * @group legacy + */ public function testDoNothingOnRetry() { $sender = $this->createMock(SenderInterface::class); $sender->expects($this->never())->method('send'); $listener = new SendFailedMessageToFailureTransportListener($sender); + $envelope = new Envelope(new \stdClass()); + $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', new \Exception()); + $event->setForRetry(); + + $listener->onMessageFailed($event); + } + + public function testDoNothingOnRetryWithServiceLocator() + { + $sender = $this->createMock(SenderInterface::class); + $sender->expects($this->never())->method('send'); + + $serviceLocator = $this->createMock(ServiceLocator::class); + $listener = new SendFailedMessageToFailureTransportListener($serviceLocator); $envelope = new Envelope(new \stdClass()); $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', new \Exception()); @@ -56,6 +105,9 @@ public function testDoNothingOnRetry() $listener->onMessageFailed($event); } + /** + * @group legacy + */ public function testDoNotRedeliverToFailed() { $sender = $this->createMock(SenderInterface::class); @@ -69,4 +121,65 @@ public function testDoNotRedeliverToFailed() $listener->onMessageFailed($event); } + + public function testDoNotRedeliverToFailedWithServiceLocator() + { + $receiverName = 'my_receiver'; + + $sender = $this->createMock(SenderInterface::class); + $sender->expects($this->never())->method('send'); + $serviceLocator = $this->createMock(ServiceLocator::class); + + $listener = new SendFailedMessageToFailureTransportListener($serviceLocator); + $envelope = new Envelope(new \stdClass(), [ + new SentToFailureTransportStamp($receiverName), + ]); + $event = new WorkerMessageFailedEvent($envelope, $receiverName, new \Exception()); + + $listener->onMessageFailed($event); + } + + public function testDoNothingIfFailureTransportIsNotDefined() + { + $sender = $this->createMock(SenderInterface::class); + $sender->expects($this->never())->method('send'); + + $serviceLocator = $this->createMock(ServiceLocator::class); + $listener = new SendFailedMessageToFailureTransportListener($serviceLocator, null); + + $exception = new \Exception('no!'); + $envelope = new Envelope(new \stdClass()); + $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception); + + $listener->onMessageFailed($event); + } + + public function testItSendsToTheFailureTransportWithMultipleFailedTransports() + { + $receiverName = 'my_receiver'; + $sender = $this->createMock(SenderInterface::class); + $sender->expects($this->once())->method('send')->with($this->callback(function ($envelope) use ($receiverName) { + /* @var Envelope $envelope */ + $this->assertInstanceOf(Envelope::class, $envelope); + + /** @var SentToFailureTransportStamp $sentToFailureTransportStamp */ + $sentToFailureTransportStamp = $envelope->last(SentToFailureTransportStamp::class); + $this->assertNotNull($sentToFailureTransportStamp); + $this->assertSame($receiverName, $sentToFailureTransportStamp->getOriginalReceiverName()); + + return true; + }))->willReturnArgument(0); + + $serviceLocator = $this->createMock(ServiceLocator::class); + $serviceLocator->method('has')->with($receiverName)->willReturn(true); + $serviceLocator->method('get')->with($receiverName)->willReturn($sender); + + $listener = new SendFailedMessageToFailureTransportListener($serviceLocator); + + $exception = new \Exception('no!'); + $envelope = new Envelope(new \stdClass()); + $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception); + + $listener->onMessageFailed($event); + } } diff --git a/src/Symfony/Component/Messenger/Tests/FailureIntegrationTest.php b/src/Symfony/Component/Messenger/Tests/FailureIntegrationTest.php index 88e32dd845ea2..00c3571d678f6 100644 --- a/src/Symfony/Component/Messenger/Tests/FailureIntegrationTest.php +++ b/src/Symfony/Component/Messenger/Tests/FailureIntegrationTest.php @@ -13,6 +13,8 @@ use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use Psr\Log\NullLogger; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; @@ -38,6 +40,9 @@ class FailureIntegrationTest extends TestCase { + /** + * @group legacy + */ public function testRequeueMechanism() { $transport1 = new DummyFailureTestSenderAndReceiver(); @@ -216,6 +221,333 @@ public function testRequeueMechanism() // the failure transport is empty because it worked $this->assertEmpty($failureTransport->getMessagesWaitingToBeReceived()); } + + public function testRequeueMechanismWithServiceLocator() + { + $transport1 = new DummyFailureTestSenderAndReceiver(); + $transport2 = new DummyFailureTestSenderAndReceiver(); + $failureTransport = new DummyFailureTestSenderAndReceiver(); + $sendersLocatorFailureTransport = new ServiceLocator([ + 'transport1' => function () use ($failureTransport) { + return $failureTransport; + }, + 'transport2' => function () use ($failureTransport) { + return $failureTransport; + }, + ]); + + $transports = [ + 'transport1' => $transport1, + 'transport2' => $transport2, + 'the_failure_transport' => $failureTransport, + ]; + + $locator = $this->createMock(ContainerInterface::class); + $locator->expects($this->any()) + ->method('has') + ->willReturn(true); + $locator->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($transportName) use ($transports) { + return $transports[$transportName]; + }); + $senderLocator = new SendersLocator( + [DummyMessage::class => ['transport1', 'transport2']], + $locator + ); + + $retryStrategyLocator = $this->createMock(ContainerInterface::class); + $retryStrategyLocator->expects($this->any()) + ->method('has') + ->willReturn(true); + $retryStrategyLocator->expects($this->any()) + ->method('get') + ->willReturn(new MultiplierRetryStrategy(1)); + + // using to so we can lazily get the bus later and avoid circular problem + $transport1HandlerThatFails = new DummyTestHandler(true); + $allTransportHandlerThatWorks = new DummyTestHandler(false); + $transport2HandlerThatWorks = new DummyTestHandler(false); + $handlerLocator = new HandlersLocator([ + DummyMessage::class => [ + new HandlerDescriptor($transport1HandlerThatFails, [ + 'from_transport' => 'transport1', + 'alias' => 'handler_that_fails', + ]), + new HandlerDescriptor($allTransportHandlerThatWorks, [ + 'alias' => 'handler_that_works1', + ]), + new HandlerDescriptor($transport2HandlerThatWorks, [ + 'from_transport' => 'transport2', + 'alias' => 'handler_that_works2', + ]), + ], + ]); + + $dispatcher = new EventDispatcher(); + $bus = new MessageBus([ + new FailedMessageProcessingMiddleware(), + new SendMessageMiddleware($senderLocator), + new HandleMessageMiddleware($handlerLocator), + ]); + $dispatcher->addSubscriber(new AddErrorDetailsStampListener()); + $dispatcher->addSubscriber(new SendFailedMessageForRetryListener($locator, $retryStrategyLocator)); + + $dispatcher->addSubscriber(new SendFailedMessageToFailureTransportListener($sendersLocatorFailureTransport)); + $dispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(1)); + + $runWorker = function (string $transportName) use ($transports, $bus, $dispatcher): ?\Throwable { + $throwable = null; + $failedListener = function (WorkerMessageFailedEvent $event) use (&$throwable) { + $throwable = $event->getThrowable(); + }; + $dispatcher->addListener(WorkerMessageFailedEvent::class, $failedListener); + + $worker = new Worker([$transportName => $transports[$transportName]], $bus, $dispatcher); + + $worker->run(); + + $dispatcher->removeListener(WorkerMessageFailedEvent::class, $failedListener); + + return $throwable; + }; + + // send the message + $envelope = new Envelope(new DummyMessage('API')); + $bus->dispatch($envelope); + + // message has been sent + $this->assertCount(1, $transport1->getMessagesWaitingToBeReceived()); + $this->assertCount(1, $transport2->getMessagesWaitingToBeReceived()); + $this->assertCount(0, $failureTransport->getMessagesWaitingToBeReceived()); + + // receive the message - one handler will fail and the message + // will be sent back to transport1 to be retried + /* + * Receive the message from "transport1" + */ + $throwable = $runWorker('transport1'); + // make sure this is failing for the reason we think + $this->assertInstanceOf(HandlerFailedException::class, $throwable); + // handler for transport1 and all transports were called + $this->assertSame(1, $transport1HandlerThatFails->getTimesCalled()); + $this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled()); + $this->assertSame(0, $transport2HandlerThatWorks->getTimesCalled()); + // one handler failed and the message is retried (resent to transport1) + $this->assertCount(1, $transport1->getMessagesWaitingToBeReceived()); + $this->assertEmpty($failureTransport->getMessagesWaitingToBeReceived()); + + /* + * Receive the message for a (final) retry + */ + $runWorker('transport1'); + // only the "failed" handler is called a 2nd time + $this->assertSame(2, $transport1HandlerThatFails->getTimesCalled()); + $this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled()); + // handling fails again, message is sent to failure transport + $this->assertCount(0, $transport1->getMessagesWaitingToBeReceived()); + $this->assertCount(1, $failureTransport->getMessagesWaitingToBeReceived()); + /** @var Envelope $failedEnvelope */ + $failedEnvelope = $failureTransport->getMessagesWaitingToBeReceived()[0]; + /** @var SentToFailureTransportStamp $sentToFailureStamp */ + $sentToFailureStamp = $failedEnvelope->last(SentToFailureTransportStamp::class); + $this->assertNotNull($sentToFailureStamp); + /** @var ErrorDetailsStamp $errorDetailsStamp */ + $errorDetailsStamp = $failedEnvelope->last(ErrorDetailsStamp::class); + $this->assertNotNull($errorDetailsStamp); + $this->assertSame('Failure from call 2', $errorDetailsStamp->getExceptionMessage()); + + /* + * Failed message is handled, fails, and sent for a retry + */ + $throwable = $runWorker('the_failure_transport'); + // make sure this is failing for the reason we think + $this->assertInstanceOf(HandlerFailedException::class, $throwable); + // only the "failed" handler is called a 3rd time + $this->assertSame(3, $transport1HandlerThatFails->getTimesCalled()); + $this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled()); + // handling fails again, message is retried + $this->assertCount(1, $failureTransport->getMessagesWaitingToBeReceived()); + // transport2 still only holds the original message + // a new message was never mistakenly delivered to it + $this->assertCount(1, $transport2->getMessagesWaitingToBeReceived()); + + /* + * Message is retried on failure transport then discarded + */ + $runWorker('the_failure_transport'); + // only the "failed" handler is called a 4th time + $this->assertSame(4, $transport1HandlerThatFails->getTimesCalled()); + $this->assertSame(1, $allTransportHandlerThatWorks->getTimesCalled()); + // handling fails again, message is discarded + $this->assertCount(0, $failureTransport->getMessagesWaitingToBeReceived()); + + /* + * Execute handlers on transport2 + */ + $runWorker('transport2'); + // transport1 handler is not called again + $this->assertSame(4, $transport1HandlerThatFails->getTimesCalled()); + // all transport handler is now called again + $this->assertSame(2, $allTransportHandlerThatWorks->getTimesCalled()); + // transport1 handler called for the first time + $this->assertSame(1, $transport2HandlerThatWorks->getTimesCalled()); + // all transport should be empty + $this->assertEmpty($transport1->getMessagesWaitingToBeReceived()); + $this->assertEmpty($transport2->getMessagesWaitingToBeReceived()); + $this->assertEmpty($failureTransport->getMessagesWaitingToBeReceived()); + + /* + * Dispatch the original message again + */ + $bus->dispatch($envelope); + // handle the failing message so it goes into the failure transport + $runWorker('transport1'); + $runWorker('transport1'); + // now make the handler work! + $transport1HandlerThatFails->setShouldThrow(false); + $runWorker('the_failure_transport'); + // the failure transport is empty because it worked + $this->assertEmpty($failureTransport->getMessagesWaitingToBeReceived()); + } + + public function testMultipleFailedTransportsWithoutGlobalFailureTransport() + { + $transport1 = new DummyFailureTestSenderAndReceiver(); + $transport2 = new DummyFailureTestSenderAndReceiver(); + $failureTransport1 = new DummyFailureTestSenderAndReceiver(); + $failureTransport2 = new DummyFailureTestSenderAndReceiver(); + + $sendersLocatorFailureTransport = new ServiceLocator([ + 'transport1' => function () use ($failureTransport1) { + return $failureTransport1; + }, + 'transport2' => function () use ($failureTransport2) { + return $failureTransport2; + }, + ]); + + $transports = [ + 'transport1' => $transport1, + 'transport2' => $transport2, + 'the_failure_transport1' => $failureTransport1, + 'the_failure_transport2' => $failureTransport2, + ]; + + $locator = $this->createMock(ContainerInterface::class); + $locator->expects($this->any()) + ->method('has') + ->willReturn(true); + $locator->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($transportName) use ($transports) { + return $transports[$transportName]; + }); + $senderLocator = new SendersLocator( + [DummyMessage::class => ['transport1', 'transport2']], + $locator + ); + + // retry strategy with zero retries so it goes to the failed transport after failure + $retryStrategyLocator = $this->createMock(ContainerInterface::class); + $retryStrategyLocator->expects($this->any()) + ->method('has') + ->willReturn(true); + $retryStrategyLocator->expects($this->any()) + ->method('get') + ->willReturn(new MultiplierRetryStrategy(0)); + + // using to so we can lazily get the bus later and avoid circular problem + $transport1HandlerThatFails = new DummyTestHandler(true); + $transport2HandlerThatFails = new DummyTestHandler(true); + $handlerLocator = new HandlersLocator([ + DummyMessage::class => [ + new HandlerDescriptor($transport1HandlerThatFails, [ + 'from_transport' => 'transport1', + ]), + new HandlerDescriptor($transport2HandlerThatFails, [ + 'from_transport' => 'transport2', + ]), + ], + ]); + + $dispatcher = new EventDispatcher(); + $bus = new MessageBus([ + new FailedMessageProcessingMiddleware(), + new SendMessageMiddleware($senderLocator), + new HandleMessageMiddleware($handlerLocator), + ]); + + $dispatcher->addSubscriber(new SendFailedMessageForRetryListener($locator, $retryStrategyLocator)); + $dispatcher->addSubscriber(new SendFailedMessageToFailureTransportListener( + $sendersLocatorFailureTransport, + new NullLogger() + )); + $dispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(1)); + + $runWorker = function (string $transportName) use ($transports, $bus, $dispatcher): ?\Throwable { + $throwable = null; + $failedListener = function (WorkerMessageFailedEvent $event) use (&$throwable) { + $throwable = $event->getThrowable(); + }; + $dispatcher->addListener(WorkerMessageFailedEvent::class, $failedListener); + + $worker = new Worker([$transportName => $transports[$transportName]], $bus, $dispatcher); + + $worker->run(); + + $dispatcher->removeListener(WorkerMessageFailedEvent::class, $failedListener); + + return $throwable; + }; + + // send the message + $envelope = new Envelope(new DummyMessage('API')); + $bus->dispatch($envelope); + + // message has been sent + $this->assertCount(1, $transport1->getMessagesWaitingToBeReceived()); + $this->assertCount(1, $transport2->getMessagesWaitingToBeReceived()); + $this->assertCount(0, $failureTransport1->getMessagesWaitingToBeReceived()); + $this->assertCount(0, $failureTransport2->getMessagesWaitingToBeReceived()); + + // Receive the message from "transport1" + $throwable = $runWorker('transport1'); + $this->assertInstanceOf(HandlerFailedException::class, $throwable); + // handler for transport1 is called + $this->assertSame(1, $transport1HandlerThatFails->getTimesCalled()); + $this->assertSame(0, $transport2HandlerThatFails->getTimesCalled()); + // one handler failed and the message is sent to the failed transport of transport1 + $this->assertCount(1, $failureTransport1->getMessagesWaitingToBeReceived()); + $this->assertCount(0, $failureTransport2->getMessagesWaitingToBeReceived()); + + // consume the failure message failed on "transport1" + $runWorker('the_failure_transport1'); + // "transport1" handler is called again from the "the_failed_transport1" and it fails + $this->assertSame(2, $transport1HandlerThatFails->getTimesCalled()); + $this->assertSame(0, $transport2HandlerThatFails->getTimesCalled()); + $this->assertCount(0, $failureTransport1->getMessagesWaitingToBeReceived()); + $this->assertCount(0, $failureTransport2->getMessagesWaitingToBeReceived()); + + // Receive the message from "transport2" + $throwable = $runWorker('transport2'); + $this->assertInstanceOf(HandlerFailedException::class, $throwable); + $this->assertSame(2, $transport1HandlerThatFails->getTimesCalled()); + // handler for "transport2" is called + $this->assertSame(1, $transport2HandlerThatFails->getTimesCalled()); + $this->assertCount(0, $failureTransport1->getMessagesWaitingToBeReceived()); + // the failure transport "the_failure_transport2" has 1 new message failed from "transport2" + $this->assertCount(1, $failureTransport2->getMessagesWaitingToBeReceived()); + + // Consume the failure message failed on "transport2" + $runWorker('the_failure_transport2'); + $this->assertSame(2, $transport1HandlerThatFails->getTimesCalled()); + // "transport2" handler is called again from the "the_failed_transport2" and it fails + $this->assertSame(2, $transport2HandlerThatFails->getTimesCalled()); + $this->assertCount(0, $failureTransport1->getMessagesWaitingToBeReceived()); + // After the message fails again, the message is discarded from the "the_failure_transport2" + $this->assertCount(0, $failureTransport2->getMessagesWaitingToBeReceived()); + } } class DummyFailureTestSenderAndReceiver implements ReceiverInterface, SenderInterface