8000 [Messenger] Be able to get raw data when a message in not decodable by the PHP Serializer by lyrixx · Pull Request #39622 · symfony/symfony · GitHub < 8000 link rel="search" type="application/opensearchdescription+xml" href="/opensearch.xml" title="GitHub">
[go: up one dir, main page]

Skip to content

[Messenger] Be able to get raw data when a message in not decodable by the PHP Serializer #39622

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -191,20 +191,23 @@
service('messenger.routable_message_bus'),
service('event_dispatcher'),
service('logger'),
service('messenger.transport.native_php_serializer')->nullOnInvalid(),
])
->tag('console.command')

->set('console.command.messenger_failed_messages_show', FailedMessagesShowCommand::class)
->args([
abstract_arg('Default failure receiver name'),
abstract_arg('Receivers'),
service('messenger.transport.native_php_serializer')->nullOnInvalid(),
])
->tag('console.command')

->set('console.command.messenger_failed_messages_remove', FailedMessagesRemoveCommand::class)
->args([
abstract_arg('Default failure receiver name'),
abstract_arg('Receivers'),
service('messenger.transport.native_php_serializer')->nullOnInvalid(),
])
->tag('console.command')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Symfony\Component\Messenger\Stamp\ErrorDetailsStamp;
use Symfony\Component\Messenger\Stamp\MessageDecodingFailedStamp;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp;
use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\MessageCountAwareInterface;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Caster\TraceStub;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
Expand All @@ -44,13 +46,15 @@ abstract class AbstractFailedMessagesCommand extends Command
protected const DEFAULT_TRANSPORT_OPTION = 'choose';

protected $failureTransports;
protected ?PhpSerializer $phpSerializer;

private ?string $globalFailureReceiverName;

public function __construct(?string $globalFailureReceiverName, ServiceProviderInterface $failureTransports)
public function __construct(?string $globalFailureReceiverName, ServiceProviderInterface $failureTransports, PhpSerializer $phpSerializer = null)
{
$this->failureTransports = $failureTransports;
$this->globalFailureReceiverName = $globalFailureReceiverName;
$this->phpSerializer = $phpSerializer;

parent::__construct();
}
Expand Down Expand Up @@ -78,6 +82,8 @@ protected function displaySingleMessage(Envelope $envelope, SymfonyStyle $io)
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);
/** @var MessageDecodingFailedStamp|null $lastMessageDecodingFailedStamp */
$lastMessageDecodingFailedStamp = $envelope->last(MessageDecodingFailedStamp::class);

$rows = [
['Class', \get_class($envelope->getMessage())],
Expand Down Expand Up @@ -126,12 +132,18 @@ protected function displaySingleMessage(Envelope $envelope, SymfonyStyle $io)

if ($io->isVeryVerbose()) {
$io->title('Message:');
if (null !== $lastMessageDecodingFailedStamp) {
$io->error('The message could not be decoded. See below an APPROXIMATIVE representation of the class.');
}
$dump = new Dumper($io, null, $this->createCloner());
$io->writeln($dump($envelope->getMessage()));
$io->title('Exception:');
$flattenException = $lastErrorDetailsStamp?->getFlattenException();
$io->writeln(null === $flattenException ? '(no data)' : $dump($flattenException));
} else {
if (null !== $lastMessageDecodingFailedStamp) {
$io->error('The message could not be decoded.');
}
$io->writeln(' Re-run command with <info>-vv</info> to see more message & error details.');
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,13 @@ private function removeMessages(string $failureTransportName, array $ids, Receiv
}

foreach ($ids as $id) {
$envelope = $receiver->find($id);
$this->phpSerializer?->enableClassNotFoundCreation();
try {
$envelope = $receiver->find($id);
} finally {
$this->phpSerializer?->enableClassNotFoundCreation(false);
}

if (null === $envelope) {
$io->error(sprintf('The message with id "%s" was not found.', $id));
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
use Symfony\Component\Messenger\Exception\LogicException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\MessageDecodingFailedStamp;
use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
use Symfony\Component\Messenger\Transport\Receiver\SingleMessageReceiver;
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
use Symfony\Component\Messenger\Worker;
use Symfony\Contracts\Service\ServiceProviderInterface;

Expand All @@ -41,13 +42,13 @@ class FailedMessagesRetryCommand extends AbstractFailedMessagesCommand
private MessageBusInterface $messageBus;
private ?LoggerInterface $logger;

public function __construct(?string $globalReceiverName, ServiceProviderInterface $failureTransports, MessageBusInterface $messageBus, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger = null)
public function __construct(?string $globalReceiverName, ServiceProviderInterface $failureTransports, MessageBusInterface $messageBus, EventDispatcherInterface $eventDispatcher, LoggerInterface $logger = null, PhpSerializer $phpSerializer = null)
{
$this->eventDispatcher = $eventDispatcher;
$this->messageBus = $messageBus;
$this->logger = $logger;

parent::__construct($globalReceiverName, $failureTransports);
parent::__construct($globalReceiverName, $failureTransports, $phpSerializer);
}

protected function configure(): void
Expand Down Expand Up @@ -133,23 +134,23 @@ private function runInteractive(string $failureTransportName, SymfonyStyle $io,
// to be temporarily "acked", even if the user aborts
// handling the message
while (true) {
$ids = [];
foreach ($receiver->all(1) as $envelope) {
++$count;

$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.', $failureTransportName));
$envelopes = [];
$this->phpSerializer?->enableClassNotFoundCreation();
try {
foreach ($receiver->all(1) as $envelope) {
++$count;
$envelopes[] = $envelope;
}
$ids[] = $id;
} finally {
$this->phpSerializer?->enableClassNotFoundCreation(false);
}

// break the loop if all messages are consumed
if (0 === \count($ids)) {
if (0 === \count($envelopes)) {
break;
}

$this->retrySpecificIds($failureTransportName, $ids, $io, $shouldForce);
$this->retrySpecificEnvelops($envelopes, $failureTransportName, $io, $shouldForce);
}
} else {
// get() and ask messages one-by-one
Expand All @@ -171,6 +172,10 @@ private function runWorker(string $failureTransportName, ReceiverInterface $rece

$this->displaySingleMessage($envelope, $io);

if ($envelope->last(MessageDecodingFailedStamp::class)) {
throw new \RuntimeException(sprintf('The message with id "%s" could not decoded, it can only be shown or removed.', $this->getMessageId($envelope) ?? '?'));
}

$shouldHandle = $shouldForce || $io->confirm('Do you want to retry (yes) or delete this message (no)?');

if ($shouldHandle) {
Expand Down Expand Up @@ -207,7 +212,12 @@ private function retrySpecificIds(string $failureTransportName, array $ids, Symf
}

foreach ($ids as $id) {
$envelope = $receiver->find($id);
$this->phpSerializer?->enableClassNotFoundCreation();
try {
$envelope = $receiver->find($id);
} finally {
$this->phpSerializer?->enableClassNotFoundCreation(false);
}
if (null === $envelope) {
throw new RuntimeException(sprintf('The message "%s" was not found.', $id));
}
Expand All @@ -216,4 +226,14 @@ private function retrySpecificIds(string $failureTransportName, array $ids, Symf
$this->runWorker($failureTransportName, $singleReceiver, $io, $shouldForce);
}
}

private function retrySpecificEnvelops(array $envelopes, string $failureTransportName, SymfonyStyle $io, bool $shouldForce)
{
$receiver = $this->getReceiver($failureTransportName);

foreach ($envelopes as $envelope) {
$singleReceiver = new SingleMessageReceiver($receiver, $envelope);
$this->runWorker($failureTransportName, $singleReceiver, $io, $shouldForce);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,29 +96,29 @@ private function listMessages(?string $failedTransportName, SymfonyStyle $io, in
$io->comment(sprintf('Displaying only \'%s\' messages', $classFilter));
}

foreach ($envelopes as $envelope) {
$currentClassName = \get_class($envelope->getMessage());

if ($classFilter && $classFilter !== $currentClassName) {
continue;
}

/** @var RedeliveryStamp|null $lastRedeliveryStamp */
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);

$errorMessage = '';
if (null !== $lastErrorDetailsStamp) {
$errorMessage = $lastErrorDetailsStamp->getExceptionMessage();
$this->phpSerializer?->enableClassNotFoundCreation();
try {
foreach ($envelopes as $envelope) {
$currentClassName = \get_class($envelope->getMessage());

if ($classFilter && $classFilter !== $currentClassName) {
continue;
}

/** @var RedeliveryStamp|null $lastRedeliveryStamp */
$lastRedeliveryStamp = $envelope->last(RedeliveryStamp::class);
/** @var ErrorDetailsStamp|null $lastErrorDetailsStamp */
$lastErrorDetailsStamp = $envelope->last(ErrorDetailsStamp::class);

$rows[] = [
$this->getMessageId($envelope),
$currentClassName,
null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'),
$lastErrorDetailsStamp?->getExceptionMessage() ?? '',
];
}

$rows[] = [
$this->getMessageId($envelope),
$currentClassName,
null === $lastRedeliveryStamp ? '' : $lastRedeliveryStamp->getRedeliveredAt()->format('Y-m-d H:i:s'),
$errorMessage,
];
} finally {
$this->phpSerializer?->enableClassNotFoundCreation(false);
}

$rowsCount = \count($rows);
Expand Down Expand Up @@ -148,14 +148,19 @@ private function listMessagesPerClass(?string $failedTransportName, SymfonyStyle

$countPerClass = [];

foreach ($envelopes as $envelope) {
$c = \get_class($envelope->getMessage());
$this->phpSerializer?->enableClassNotFoundCreation();
try {
foreach ($envelopes as $envelope) {
$c = \get_class($envelope->getMessage());

if (!isset($countPerClass[$c])) {
$countPerClass[$c] = [$c, 0];
}
if (!isset($countPerClass[$c])) {
$countPerClass[$c] = [$c, 0];
}

++$countPerClass[$c][1];
++$countPerClass[$c][1];
}
} finally {
$this->phpSerializer?->enableClassNotFoundCreation(false);
}

if (0 === \count($countPerClass)) {
Expand All @@ -171,7 +176,12 @@ private function showMessage(?string $failedTransportName, string $id, SymfonySt
{
/** @var ListableReceiverInterface $receiver */
$receiver = $this->getReceiver($failedTransportName);
$envelope = $receiver->find($id);
$this->phpSerializer?->enableClassNotFoundCreation();
try {
$envelope = $receiver->find($id);
} finally {
$this->phpSerializer?->enableClassNotFoundCreation(false);
}
if (null === $envelope) {
throw new RuntimeException(sprintf('The message "%s" was not found.', $id));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Messenger\Stamp;

/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class MessageDecodingFailedStamp implements StampInterface
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class PhpSerializerTest extends TestCase
{
public function testEncodedIsDecodable()
{
$serializer = new PhpSerializer();
$serializer = $this->createPhpSerializer();

$envelope = new Envelope(new DummyMessage('Hello'));

Expand All @@ -36,7 +36,7 @@ public function testDecodingFailsWithMissingBodyKey()
$this->expectException(MessageDecodingFailedException::class);
$this->expectExceptionMessage('Encoded envelope should have at least a "body", or maybe you should implement your own serializer');

$serializer = new PhpSerializer();
$serializer = $this->createPhpSerializer();

$serializer->decode([]);
}
Expand All @@ -46,7 +46,7 @@ public function testDecodingFailsWithBadFormat()
$this->expectException(MessageDecodingFailedException::class);
$this->expectExceptionMessageMatches('/Could not decode/');

$serializer = new PhpSerializer();
$serializer = $this->createPhpSerializer();

$serializer->decode([
'body' => '{"message": "bar"}',
Expand All @@ -58,7 +58,7 @@ public function testDecodingFailsWithBadBase64Body()
$this->expectException(MessageDecodingFailedException::class);
$this->expectExceptionMessageMatches('/Could not decode/');

$serializer = new PhpSerializer();
$serializer = $this->createPhpSerializer();

$serializer->decode([
'body' => 'x',
Expand All @@ -70,7 +70,7 @@ public function testDecodingFailsWithBadClass()
$this->expectException(MessageDecodingFailedException::class);
$this->expectExceptionMessageMatches('/class "ReceivedSt0mp" not found/');

$serializer = new PhpSerializer();
$serializer = $this->createPhpSerializer();

$serializer->decode([
'body' => 'O:13:"ReceivedSt0mp":0:{}',
Expand All @@ -79,7 +79,7 @@ public function testDecodingFailsWithBadClass()

public function testEncodedSkipsNonEncodeableStamps()
{
$serializer = new PhpSerializer();
$serializer = $this->createPhpSerializer();

$envelope = new Envelope(new DummyMessage('Hello'), [
new DummyPhpSerializerNonSendableStamp(),
Expand All @@ -91,14 +91,19 @@ public function testEncodedSkipsNonEncodeableStamps()

public function testNonUtf8IsBase64Encoded()
{
$serializer = new PhpSerializer();
$serializer = $this->createPhpSerializer();

$envelope = new Envelope(new DummyMessage("\xE9"));

$encoded = $serializer->encode($envelope);
$this->assertTrue((bool) preg_match('//u', $encoded['body']), 'Encodes non-UTF8 payloads');
$this->assertEquals($envelope, $serializer->decode($encoded));
}

protected function createPhpSerializer(): PhpSerializer
{
return new PhpSerializer();
}
}

class DummyPhpSerializerNonSendableStamp implements NonSendableStampInterface
Expand Down
Loading
0