Skip to content

Commit

Permalink
feature #51593 [Messenger] Add the --all option to the `messenger:f…
Browse files Browse the repository at this point in the history
…ailed:remove` command (alexandre-daubois)

This PR was merged into the 6.4 branch.

Discussion
----------

[Messenger] Add the `--all` option to the `messenger:failed:remove` command

| Q             | A
| ------------- | ---
| Branch?       | 6.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | Todo

We have a development server (which we don't have direct access to the database). As this server serves as a test for our devs, error messages can accumulate in our failure transport. We wanted to use the `messenger:failed:remove` command to remove them, but unfortunately, we must provide ids individually. This is problematic as we have several hundreds of failed messages.

This PR adds the `--all` option to the command. This option **must** be used with the `--force` option (juste like `doctrine:schema:update --force` actually) to work. Example output:

```bash
$ bin/console messenger:failed:remove --all --force

...

Failed Message Details
======================

 [WARNING] Message does not appear to have been sent to this transport after failing

 ------------ -------------------------
  Class        App\Message\YourMessage
  Message Id   6
 ------------ -------------------------

 ! [NOTE] 4 messages were removed.
```

As you can see, you can of course still use the `--show-messages` option jointly.

Commits
-------

d1d39c0 [Messenger] Add `--all` option to the `messenger:failed:remove` command
  • Loading branch information
nicolas-grekas committed Sep 29, 2023
2 parents 1244ec9 + d1d39c0 commit 4b21e21
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 10 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Component/Messenger/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CHANGELOG
* Deprecate `StopWorkerOnSignalsListener` in favor of using the `SignalableCommandInterface`
* Add `HandlerDescriptor::getOptions`
* Add support for multiple Redis Sentinel hosts
* Add `--all` option to the `messenger:failed:remove` command

6.3
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;

/**
* @author Ryan Weaver <ryan@symfonycasts.com>
Expand All @@ -32,7 +32,8 @@ protected function configure(): void
{
$this
->setDefinition([
new InputArgument('id', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'Specific message id(s) to remove'),
new InputArgument('id', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Specific message id(s) to remove'),
new InputOption('all', null, InputOption::VALUE_NONE, 'Remove all failed messages from the transport'),
new InputOption('force', null, InputOption::VALUE_NONE, 'Force the operation without confirmation'),
new InputOption('transport', null, InputOption::VALUE_OPTIONAL, 'Use a specific failure transport', self::DEFAULT_TRANSPORT_OPTION),
new InputOption('show-messages', null, InputOption::VALUE_NONE, 'Display messages before removing it (if multiple ids are given)'),
Expand All @@ -43,6 +44,10 @@ protected function configure(): void
<info>php %command.full_name% {id1} [{id2} ...]</info>
The specific ids can be found via the messenger:failed:show command.
You can remove all failed messages from the failure transport by using the "--all" option:
<info>php %command.full_name% --all</info>
EOF
)
;
Expand All @@ -61,18 +66,32 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$shouldForce = $input->getOption('force');
$ids = (array) $input->getArgument('id');
$shouldDisplayMessages = $input->getOption('show-messages') || 1 === \count($ids);
$this->removeMessages($failureTransportName, $ids, $receiver, $io, $shouldForce, $shouldDisplayMessages);
$shouldDeleteAllMessages = $input->getOption('all');

return 0;
}
$idsCount = \count($ids);
if (!$shouldDeleteAllMessages && !$idsCount) {
throw new RuntimeException('Please specify at least one message id. If you want to remove all failed messages, use the "--all" option.');
} elseif ($shouldDeleteAllMessages && $idsCount) {
throw new RuntimeException('You cannot specify message ids when using the "--all" option.');
}

$shouldDisplayMessages = $input->getOption('show-messages') || 1 === $idsCount;

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.', $failureTransportName));
}

if ($shouldDeleteAllMessages) {
$this->removeAllMessages($receiver, $io, $shouldForce, $shouldDisplayMessages);
} else {
$this->removeMessagesById($ids, $receiver, $io, $shouldForce, $shouldDisplayMessages);
}

return 0;
}

private function removeMessagesById(array $ids, ListableReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce, bool $shouldDisplayMessages): void
{
foreach ($ids as $id) {
$this->phpSerializer?->acceptPhpIncompleteClass();
try {
Expand All @@ -99,4 +118,31 @@ private function removeMessages(string $failureTransportName, array $ids, Receiv
}
}
}

private function removeAllMessages(ListableReceiverInterface $receiver, SymfonyStyle $io, bool $shouldForce, bool $shouldDisplayMessages): void
{
if (!$shouldForce) {
if ($receiver instanceof MessageCountAwareInterface) {
$question = sprintf('Do you want to permanently remove all (%d) messages?', $receiver->getMessageCount());
} else {
$question = 'Do you want to permanently remove all failed messages?';
}

if (!$io->confirm($question, false)) {
return;
}
}

$count = 0;
foreach ($receiver->all() as $envelope) {
if ($shouldDisplayMessages) {
$this->displaySingleMessage($envelope, $io);
}

$receiver->reject($envelope);
++$count;
}

$io->note(sprintf('%d messages were removed.', $count));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
namespace Symfony\Component\Messenger\Tests\Command;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Tester\CommandCompletionTester;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceiver;
use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
Expand Down Expand Up @@ -207,7 +209,7 @@ public function testCompleteId()
$globalFailureReceiverName = 'failure_receiver';

$receiver = $this->createMock(ListableReceiverInterface::class);
$receiver->expects($this->once())->method('all')->with(50)->willReturn([
$receiver->expects($this->once())->method('all')->willReturn([
Envelope::wrap(new \stdClass(), [new TransportMessageIdStamp('2ab50dfa1fbf')]),
Envelope::wrap(new \stdClass(), [new TransportMessageIdStamp('78c2da843723')]),
]);
Expand All @@ -233,7 +235,7 @@ public function testCompleteIdWithSpecifiedTransport()
$anotherFailureReceiverName = 'another_receiver';

$receiver = $this->createMock(ListableReceiverInterface::class);
$receiver->expects($this->once())->method('all')->with(50)->willReturn([
$receiver->expects($this->once())->method('all')->willReturn([
Envelope::wrap(new \stdClass(), [new TransportMessageIdStamp('2ab50dfa1fbf')]),
Envelope::wrap(new \stdClass(), [new TransportMessageIdStamp('78c2da843723')]),
]);
Expand All @@ -253,4 +255,100 @@ public function testCompleteIdWithSpecifiedTransport()

$this->assertSame(['2ab50dfa1fbf', '78c2da843723'], $suggestions);
}

public function testOptionAllIsSetWithIdsThrows()
{
$globalFailureReceiverName = 'failure_receiver';

$serviceLocator = $this->createMock(ServiceLocator::class);
$serviceLocator->expects($this->once())->method('has')->with($globalFailureReceiverName)->willReturn(true);
$serviceLocator->expects($this->any())->method('get')->with($globalFailureReceiverName)->willReturn($this->createMock(ListableReceiverInterface::class));

$command = new FailedMessagesRemoveCommand('failure_receiver', $serviceLocator);
$tester = new CommandTester($command);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('You cannot specify message ids when using the "--all" option.');
$tester->execute(['id' => [20], '--all' => true]);
}

public function testOptionAllIsSetWithoutForceAsksConfirmation()
{
$globalFailureReceiverName = 'failure_receiver';

$receiver = $this->createMock(ListableReceiverInterface::class);
$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('failure_receiver', $serviceLocator);
$tester = new CommandTester($command);

$tester->execute(['--all' => true]);

$this->assertSame(0, $tester->getStatusCode());
$this->assertStringContainsString('Do you want to permanently remove all failed messages? (yes/no)', $tester->getDisplay());
}

public function testOptionAllIsSetWithoutForceAsksConfirmationOnMessageCountAwareInterface()
{
$globalFailureReceiverName = 'failure_receiver';

$receiver = $this->createMock(DoctrineReceiver::class);
$receiver->expects($this->once())->method('getMessageCount')->willReturn(2);

$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('failure_receiver', $serviceLocator);
$tester = new CommandTester($command);

$tester->execute(['--all' => true]);

$this->assertSame(0, $tester->getStatusCode());
$this->assertStringContainsString('Do you want to permanently remove all (2) messages? (yes/no)', $tester->getDisplay());
}

public function testOptionAllIsNotSetNorIdsThrows()
{
$globalFailureReceiverName = 'failure_receiver';

$serviceLocator = $this->createMock(ServiceLocator::class);
$serviceLocator->expects($this->once())->method('has')->with($globalFailureReceiverName)->willReturn(true);
$serviceLocator->expects($this->any())->method('get')->with($globalFailureReceiverName)->willReturn($this->createMock(ListableReceiverInterface::class));

$command = new FailedMessagesRemoveCommand('failure_receiver', $serviceLocator);
$tester = new CommandTester($command);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Please specify at least one message id. If you want to remove all failed messages, use the "--all" option.');
$tester->execute([]);
}

public function testRemoveAllMessages()
{
$globalFailureReceiverName = 'failure_receiver';
$receiver = $this->createMock(ListableReceiverInterface::class);

$series = [
new Envelope(new \stdClass()),
new Envelope(new \stdClass()),
new Envelope(new \stdClass()),
new Envelope(new \stdClass()),
];

$receiver->expects($this->once())->method('all')->willReturn($series);

$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(['--all' => true, '--force' => true, '--show-messages' => true]);

$this->assertStringContainsString('Failed Message Details', $tester->getDisplay());
$this->assertStringContainsString('4 messages were removed.', $tester->getDisplay());
}
}

0 comments on commit 4b21e21

Please sign in to comment.