Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Investigating & Retrying Failed Messages

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Apparently now that we've configured a failure_transport, if handling a message still isn't working after 3 retries, instead of being sent to /dev/null, they're sent to another transport - in our case called "failed". That transport is... really... the same as any other transport... and we could use the messenger:consume command to try to process those messages again.

But, there's a better way. Run:

php bin/console messenger

Seeing Messages on the Failed Queue

Hey! Shiny new commands are hiding here! Three under messenger:failed. Try out that messenger:failed:show one:

php bin/console messenger:failed:show

Nice! There are our 4 failed messages... just sitting there wait for us to look at them. Let's pretend that we're not sure what went wrong with these messages and want to check them out. Start by passing the 115 id:

php bin/console messenger:failed:show 115

I love this: it shows us the error message, error class and a history of the message's misadventures through our system! It failed, was redelivered to the async transport at 05, at 06 and then at 07, it finally failed and was redelivered to the failed transport.

If we add a -vv on the command...

php bin/console messenger:failed:show 115 -vv

Now we can see a full stack trace of what happened on that exception.

This is a really powerful way to figure out what went wrong and what to do next: do we have a bug in our app that we need to fix before retrying this? Or maybe it was a temporary failure and we can try again now? Or maybe, for some reason, we just want to remove this message entirely.

If you did want to remove this without retrying, that's the messenger:failed:remove command.

Retrying Failed Messages

But... let's retry this! Back in the handler, change this back to fail randomly.

... lines 1 - 13
class AddPonkaToImageHandler implements MessageHandlerInterface, LoggerAwareInterface
{
... lines 16 - 30
public function __invoke(AddPonkaToImage $addPonkaToImage)
{
... lines 33 - 46
if (rand(0, 10) < 7) {
throw new \Exception('I failed randomly!!!!');
}
... lines 50 - 56
}
}

There are two ways to work with the retry command: you can retry a specific id like you see here or you can retry the messages one-by-one. Let's do that. Run:

php bin/console messenger:failed:retry

This is kind of similar to how messenger:consume works, except that it asks you before trying each message and, instead of running this command all the time on production, you'll run it manually whenever you have some failed messages that you need to process.

Cool! We see the details and it asks if we want to retry this. Like with show, you can pass -vv to see the full message details. Say "yes". It processes... and then continues to the next. Actually, let me try that again with -vv so we can see what's going on:

php bin/console messenger:failed:retry -vv

When Failed Messages... Fail Again

This time we see all the details. Say "yes" again and... nice: "Received message", "Message handled" and onto the next message. We're on a roll! Notice that this message's id is 117 - that'll be important in a second. Hit yes to retry this message too.

Woh! This time it failed again! What does that mean? Well remember, the failure transport is really just a normal transport that we're using in a special way. And so, when a message fails here, Messenger... retries it! Yea it was sent back to the failure transport!

I'll hit Control+C and re-run the show command:

php bin/console messenger:failed:show

That id 119 was not there when we started. Nope, when message 117 was processed, it failed, was redelivered to the failure transport as id 119, and then was removed. And so, unless you change your configuration, messages will be retried 3 times on the failure transport before finally being completely discarded.

Oh, but if you look at the retried message closer:

php bin/console messenger:failed:show 119 -vv

There's a bit of a bug: the error and error class are missing. The data is still in the database... it's just not displayed correctly here. But you can see the message's history: including that it was sent to the failed transport and then sent again to the failed transport.

By the way, you can pass a --force option to the retry command if you want it to retry messages one-by-one without asking you each time whether or not it should do it. Also, not all the transport types - like AMQP or Redis - support all of the features we just saw if you use it as your failure transport. That may change in the future, but at this moment - Doctrine is the most robust transport to use for failures.

Anyways, as cool as failing is, let's go back and remove the code that's breaking our handler. Because... it's time to take a step deeper into how Messenger works: it's time to talk about middleware.

Leave a comment!

4
Login or Register to join the conversation
Default user avatar
Default user avatar Fabien Pot | posted 1 year ago

Actually, I wouldn't say "Doctrine is the most robust transport to use for failures".
1. you have one failed transport for every other transport. You can't have handle failed messages from other transports in different ways. E.g. Failed messages from transport A should be handled every 1 hour, failed messages from transport B should be handled every 5 hours.
2. you cannot move messages failed messages from transport to transport, e.g. after X retries on transport A message should be moved to less prioritized transport B not to block more prioritized transport A.
With rabbitmq you have this advantage that rabbit is acknowledged after max_retries that message was rejected and you can forward this message to the dead letter exchange (or the same exchange but different queue with another routing key) which is configured for another transport in messenger configuration.

Reply

Hey @Fabien!

That’s a very fair criticism - and your information is correct and detailed :).

What I should have said is that - in symfony - the doctrine transport as a failure transport - implements a few features that the Amqp transport does not, specifically the ability to return a list of all the items in the queue (which one of the failure commands allows you to do, if your failure transport supports listing).

Your setup / ideas with rabbitmq are indeed more robust. The failure transport in symfony is a feature you get with no effort or setup, but you’re right that it won’t do the things you’re saying :).

Cheers!

Reply
Default user avatar

Thank you Ryan. I appreciate that.

About the list. With rabbitmq, if you would use this transport for failed messages you can list failed messages via rabbitmq UI (or rabbitmqctl).
What I think the messenger component is missing is a failure transport per transport (and failure transport could also have it's failure transport and so on).

Reply

Hey Fabien!

> About the list. With rabbitmq, if you would use this transport for failed messages you can list failed messages via rabbitmq UI (or rabbitmqctl).

That’s true. Iirc it does this by reading then out and requeing them, which is ok, but we decided against doing this in messenger simply as a way to list the messages (and we still couldn’t list them all... since we wouldn’t read 10k messages and requeue then just for a list :p)

> What I think the messenger component is missing is a failure transport per transport (and failure transport could also have it's failure transport and so on).

Totally! When I implanted the failure transport in symfony 4.3, I was already a couple of days passed feature freeze and was cutting corners to get this included. The corner I cut was supporting multiple failure transport... which given enough time is not too hard to add.

There is a pull request open to add this - https://github.com/symfony/... - I need to help push it across the finish line :).

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial is built with Symfony 4.3, but will work well on Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // v1.8.0
        "doctrine/doctrine-bundle": "^1.6.10", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // v2.0.0
        "doctrine/orm": "^2.5.11", // v2.6.3
        "intervention/image": "^2.4", // 2.4.2
        "league/flysystem-bundle": "^1.0", // 1.1.0
        "phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.1
        "sensio/framework-extra-bundle": "^5.3", // v5.3.1
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/flex": "^1.9", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/property-access": "4.3.*", // v4.3.2
        "symfony/property-info": "4.3.*", // v4.3.2
        "symfony/serializer": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.5", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    },
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.7", // v1.0.7
        "symfony/debug-bundle": "4.3.*", // v4.3.2
        "symfony/maker-bundle": "^1.0", // v1.12.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/var-dumper": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2
    }
}