Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Retraso en el reintento y estrategia de reintento

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

Por defecto, un mensaje se reintentará tres veces y luego se perderá para siempre. Pues bien, en unos minutos... Te mostraré cómo puedes evitar que incluso esos mensajes se pierdan.

De todos modos... el proceso... ¡simplemente funciona! Y es aún más genial de lo que parece a primera vista. Es un poco difícil de ver -sobre todo porque hay una suspensión en nuestro manejador- pero este mensaje se envió para el reintento nº 3 en la marca de tiempo de 13 segundos y finalmente se manejó de nuevo en la marca de tiempo de 17 segundos -un retraso de 4 segundos-. Ese retraso no se debió a que nuestro trabajador estuviera ocupado hasta entonces: fue 100% intencionado.

Compruébalo: Pulsaré Ctrl+C para detener el trabajador y luego lo ejecutaré:

php bin/console config:dump framework messenger

Esto debería darnos un gran árbol de configuración "de ejemplo" que puedes poner bajo la tecla framework messenger config. Me encanta este comando: es una forma estupenda de encontrar opciones que quizá no sabías que existían.

¡Genial! Fíjate bien en la clave transports: debajo aparece un transporte de "ejemplo" con todas las opciones de configuración posibles. Una de ellas es retry_strategy, donde podemos controlar el número máximo de reintentos y el retardo que debe haber entre esos reintentos.

Este número delay es más inteligente de lo que parece: funciona junto con el "multiplicador" para crear un retardo exponencialmente creciente. Con esta configuración, el primer reintento se retrasará un segundo, el segundo 2 segundos y el tercero 4 segundos.

Esto es importante porque, si un mensaje falla debido a algún problema temporal -como la conexión a un servidor de terceros-, es posible que no quieras volver a intentarlo inmediatamente. De hecho, puedes optar por establecer estos valores mucho más altos para que se reintente quizás 1 minuto o incluso un día después.

Probemos también un comando similar:

php bin/console debug:config framework messenger

En lugar de mostrar una configuración de ejemplo, esto nos dice cuál es nuestra configuración actual, incluyendo cualquier valor por defecto: nuestro transporte async tiene un retry_strategy, que por defecto tiene 3 reintentos máximos con un retraso de 1000 milisegundos y un multiplicador de 2.

Configurar el retardo

Hagamos esto un poco más interesante. En el manejador, hagamos que siempre falle añadiendo || true.

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

Ahora, en messenger, juguemos con la configuración del reintento. Espera... pero el transporte asyncestá configurado como una cadena... ¿podemos incluir opciones de configuración bajo eso? No Bueno, sí, más o menos. En cuanto necesites configurar un transporte más allá de los detalles de la conexión, tendrás que colocar esta cadena en la siguiente línea y asignarla a una clave dsn. Ahora podemos añadir retry_strategy, y vamos a establecer el retraso en 2 segundos en lugar de 1.

framework:
messenger:
... lines 3 - 5
transports:
# https://symfony.com/doc/current/messenger.html#transports
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
delay: 2000
... lines 12 - 20

Ah, y también quiero mencionar esta tecla service. Si quieres controlar completamente la configuración del reintento -incluso tener una lógica de reintento diferente por mensaje- puedes crear un servicio que implemente RetryStrategyInterface y poner su id de servicio -normalmente su nombre de clase- aquí mismo.

En cualquier caso, veamos qué ocurre con el retraso más largo: reinicia el proceso del trabajador:

php bin/console messenger:consume -vv

Esta vez, sube sólo una foto para que podamos ver cómo falla una y otra vez. Y... ¡sí! Falla y envía para el reintento nº 1... luego vuelve a fallar y envía para el reintento nº 2. ¡Pero fíjate en el retraso! del 09 al 11 - 2 segundos - luego del 11 al 15 - un retraso de 4 segundos. Y... si... somos... súper... pacientes... ¡sí! El reintento nº 3 comienza 8 segundos después. Entonces es "rechazado" - eliminado de la cola - y perdido para siempre. ¡Trágico!

Los reintentos son geniales... pero no me gusta esa última parte: cuando el mensaje se pierde finalmente para siempre. Cambia el reintento a 500: así será más fácil de probar

framework:
messenger:
... lines 3 - 5
transports:
# https://symfony.com/doc/current/messenger.html#transports
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
delay: 500
... lines 12 - 20

A continuación, vamos a hablar de un concepto especial llamado "transporte de fallos": una alternativa mejor que permitir que los mensajes fallidos simplemente... desaparezcan.

Leave a comment!

10
Login or Register to join the conversation
Sven Avatar
Sven Avatar Sven | posted hace 21 días | edited

@weaverryan or anybody who knows; is there a specific reason why the MultiplierRetryStategy is marked @final? I want to reuse all of the logic, except for when a specific exception is thrown (in that case I want to change the waiting time). I can't decorate the service either, since it's marked abstract.

Reply

Hey @Sven!

Sorry for the slow reply :). Hmm. It looks like I did that https://github.com/symfony/symfony/blame/b4d215c96fee8e208cdc82945b657a62913f9320/src/Symfony/Component/Messenger/Retry/MultiplierRetryStrategy.php#L31 ;)

The reason is mostly that, in Symfony, we tend to make things final until someone has a legitimate case for sub-classing them. And the answer is usually that you should use decoration. But I see your point about the service being abstract: https://github.com/symfony/symfony/blob/b4d215c96fee8e208cdc82945b657a62913f9320/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php#L157

However, you should be able to decorate the concrete version of this: a concrete service is made for each transport: https://github.com/symfony/symfony/blob/b4d215c96fee8e208cdc82945b657a62913f9320/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L2275 - so decorate THAT service id, and I think it should work.

Let me know if that works :). It looks doable to me - though needing to hunt down these specific, concrete service ids is a bit tricky.

Cheers!

Reply
Aldwin Avatar

You mentioned in the video a way to override the way of retrying messages and implementing one on our own.
Apparently, it's terribly what I needed right now on my project and the scarce info regarding the subject, even after checking the documention, isn't really making it easy to implement.
Do you have any study material regarding it?

Reply

Hey @Vandarkholme

I haven't implemented a custom Retry strategy but I think you can do it without much troubles.
First, you have to create a class that implements Symfony\Component\Messenger\Retry\RetryStrategyInterface
Then, put your new services id in messenger config


// config/packages/messenger.yaml
framework:
    messenger:
        transports:
        name:
            retry_strategy:
                service: App\Your\ServiceStrategy

And last, you have to implement the methods defined by such interface. You can give a look to this class to get an idea of what to do Symfony\Component\Messenger\Retry\MultiplierRetryStrategy

I hope this helps. Cheers!

Reply
Richard P. Avatar
Richard P. Avatar Richard P. | posted hace 3 años

Hey!

I think i found a bug in the messenger:
- i sent my messages as persistent (delivery_method=2) to RabbitMQ
- those messages in the actual queue were persistent (correct)
- when the worker consumes a message but it fails, the retry mechanism is called and a temporary queue is created where the message was sent
- this is correct, BUT and here comes the bug
- the message in that temp queue is not persistent, so if my retry mechanism has a delay of days... and the server/service stops/restarts those messages will be lost
Did i do something wrong or the bug really exists?

Thank you for your great work on symfonycasts!

Reply

Hey @Richard!

Ah, it is very possible this is a bug! But... it there *may* be a finished PR that will fix this - https://github.com/symfony/...

If you're interested, you could hack that onto your system and see if you get a better result - I would actually love to know if that fixes things for you. If it doesn't fix it, I would need to look closer, but indeed - my guess is that this would be a bug.

Cheers!

Reply
Nfq A. Avatar

Thank Symfony Cast for the great video!

After we send a message with a DelayStamp to tell to execute the message in next 5 days
If later we want to extend the delay into 10 days, is it possible ?

Thanks,
Alex

Reply

Hey Nfq A.!

Hmm, so you want to send the message with a 5 day delay. Then, *before* it's processed, you want the ability to *extend* that delay to 10 days? I don't think that's easily possible. Basically, the nature of queues are that once you put them into a queue... you are just supposed to "dumbly" read them out when they become available. There would be no way to "ask" the queue for a specific message so that you could change the stamp.

However, this *is* possible... you just need to handle it in your app :). For example, you could set some database flag that says the message isn't ready to be processed yet (or even a date when it *should* be processed). Then, when the message is consumed after 5 days, your handler would check for this. If the message *is* ready to be processed, it would handle it. If not, it would take that message and re-send it into the message bus with a new, 5 day delay (the original 5 day delay + the new 5 day delay = 10 days).

I hope that helps!

Cheers!

Reply
Nfq A. Avatar

Thank you Weaverryan,

That solution is very simple and totally fix my need!

(by the way I am very happy when received your answer the first time, I has followed your tutors for a long time, which helped improving my skills and mindset a lot! ^^ )

Happy day and keep the good work SymfonyCasts team!

Cheers!

Reply

Hey Nfq A.!

> That solution is very simple and totally fix my need

Yay, perfect!

> Happy day and keep the good work SymfonyCasts team!

Thanks to you for following us ❤️

Cheers!

Reply
Cat in space

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

Este tutorial está construido con Symfony 4.3, pero funcionará bien en Symfony 4.4 o 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
    }
}
userVoice