Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Scheduler][Messenger] Message can't be redispatched with Symfony serializer in between #53562

Copy link
Copy link
Closed
@christian-kolb

Description

@christian-kolb
Issue body actions

Symfony version(s) affected

6.4.0

Description

When using the RedispatchMessage in combination with the scheduler and the symfony serializer (with JSON), then the messenger fails while trying to deserialize the message. This is due to the MessageContext class which expects the TriggerInterface for the $trigger property. When the serializer serialized the message to JSON, the information which implementation of the trigger is used, is lost.
I think we're missing a custom normalizer for the MessageContext that is able to infer the correct implementation depending on the content.

This is not a problem when the default PHP serialization is used as the relevant class information is backed into the serialized content. I guess that's also the reason that this issue didn't come up yet.

How to reproduce

Create a schedule with any message that transfers to a different transport (so that the message is written to a message queue).

use Symfony\Component\Messenger\Message\RedispatchMessage;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;

#[AsSchedule('default')]
final readonly class ScheduleProvider implements ScheduleProviderInterface
{
    #[\Override]
    public function getSchedule(): Schedule
    {
        $schedule = new Schedule();
        $schedule
            ->add(RecurringMessage::every(
                '5 seconds', // Just for easier testing
                new RedispatchMessage(
                    new DeleteOldNotificationsMessage(), // Class is in same directory
                    'async',
                ),
            ));

        return $schedule;
    }
}

Configure the messenger to use the Symfony serializer like shown in the documentation:

return static function (FrameworkConfig $framework, ContainerConfigurator $container) {
    $messenger = $framework->messenger();

    // -- Serializer
    $messenger
        ->serializer()
        ->defaultSerializer('messenger.transport.symfony_serializer')
        ->symfonySerializer()
            ->format('json');

    // Async - All low priority tasks
    $messenger
        ->transport('async')
        ->dsn('doctrine://default')
        ->options([
            'queue_name' =>'async',
        ]);
    ;
...

Make sure to run the async messenger queue and at the then start the scheduler:

php bin/console messenger:consume -v scheduler_default

Then the job queue will fail with the following error stack:

In Serializer.php line 127:
2024-01-17T12:13:27.950138967Z                                                                                
2024-01-17T12:13:27.950143217Z   [Symfony\Component\Messenger\Exception\MessageDecodingFailedException]       
2024-01-17T12:13:27.950146133Z   Could not decode stamp: The type of the "trigger" attribute for class "Symf  
2024-01-17T12:13:27.950148925Z   ony\Component\Scheduler\Generator\MessageContext" must be one of "Symfony\C  
2024-01-17T12:13:27.950151717Z   omponent\Scheduler\Trigger\TriggerInterface" ("array" given).                
2024-01-17T12:13:27.950154467Z                                                                                
2024-01-17T12:13:27.950157092Z 
2024-01-17T12:13:27.950159425Z Exception trace:
2024-01-17T12:13:27.950161800Z   at /var/www/html/vendor/symfony/messenger/Transport/Serialization/Serializer.php:127
2024-01-17T12:13:27.950164300Z  Symfony\Component\Messenger\Transport\Serialization\Serializer->decodeStamps() at /var/www/html/vendor/symfony/messenger/Transport/Serialization/Serializer.php:72
2024-01-17T12:13:27.950167675Z  Symfony\Component\Messenger\Transport\Serialization\Serializer->decode() at /var/www/html/vendor/symfony/doctrine-messenger/Transport/DoctrineReceiver.php:138
2024-01-17T12:13:27.950170467Z  Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceiver->createEnvelopeFromData() at /var/www/html/vendor/symfony/doctrine-messenger/Transport/DoctrineReceiver.php:65
2024-01-17T12:13:27.950173467Z  Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineReceiver->get() at /var/www/html/vendor/symfony/doctrine-messenger/Transport/DoctrineTransport.php:42
2024-01-17T12:13:27.950176300Z  Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport->get() at /var/www/html/vendor/symfony/messenger/Worker.php:102
2024-01-17T12:13:27.950189550Z  Symfony\Component\Messenger\Worker->run() at /var/www/html/vendor/symfony/messenger/Command/ConsumeMessagesCommand.php:238
2024-01-17T12:13:27.950192425Z  Symfony\Component\Messenger\Command\ConsumeMessagesCommand->execute() at /var/www/html/vendor/symfony/console/Command/Command.php:326
2024-01-17T12:13:27.950195175Z  Symfony\Component\Console\Command\Command->run() at /var/www/html/vendor/symfony/console/Application.php:1096
2024-01-17T12:13:27.950197842Z  Symfony\Component\Console\Application->doRunCommand() at /var/www/html/vendor/symfony/framework-bundle/Console/Application.php:126
2024-01-17T12:13:27.950204300Z  Symfony\Bundle\FrameworkBundle\Console\Application->doRunCommand() at /var/www/html/vendor/symfony/console/Application.php:324
2024-01-17T12:13:27.950207008Z  Symfony\Component\Console\Application->doRun() at /var/www/html/vendor/symfony/framework-bundle/Console/Application.php:80
2024-01-17T12:13:27.950209592Z  Symfony\Bundle\FrameworkBundle\Console\Application->doRun() at /var/www/html/vendor/symfony/console/Application.php:175
2024-01-17T12:13:27.950212175Z  Symfony\Component\Console\Application->run() at /var/www/html/bin/console:40

Possible Solution

Having a custom normalizer that can denormalize the TriggerInterface depending on the context. That of course would not solve the issue when someone implements their own trigger. But then it's "simple" to construct a custom normalizer for the project.

This is a simplified version of such a normalizer that specifically only handles PeriodicalTrigger (which fixes my local problems):

final class TriggerDenormalizer implements DenormalizerInterface
{
    /**
     * @param array{
     *   intervalInSeconds: int,
     *   from: string,
     *   until: string,
     *   description: string,
     * } $data
     */
    public function denormalize($data, string $type, string $format = null, array $context = []): TriggerInterface
    {
        return new PeriodicalTrigger(
            $data['intervalInSeconds'],
            $data['from'],
            $data['until'],
        );
    }

    public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool
    {
        return $type === TriggerInterface::class;
    }

    public function getSupportedTypes(?string $format): array
    {
        return [
            TriggerInterface::class => true,
        ];
    }
}

Additional Context

The primary question I'm having is about the approach. Does this makes sense to have an additional normalizer? If so, does it make sense to extend the triggers with logic for normalization and denormalization? They are very flexible to use from the outside, but as a side effect not very "normalizable". The example I've added is a poor man version that doesn't cover a lot of the internals and I'm not sure whether with the current architecture it even could be handled from the outside or whether we need some kind of logic exposed to the outside specific to normalization.
Would ask for direction here.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.