Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Fallo Gracioso en el Serializador de Transporte

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

Nuestro nuevo y brillante transporte external_messages lee los mensajes de esta colamessages_from_external, que fingimos que está siendo rellenada por una aplicación externa. Tomamos este JSON y, enExternalJsonMessengerSerializer, lo descodificamos, creamos el objeto LogEmoji, lo ponemos en Envelope, incluso le añadimos un sello, y finalmente lo devolvemos, para que pueda ser enviado de nuevo a través del sistema de bus de mensajes.

Fallo en caso de JSON inválido

¡Esto tiene muy buena pinta! Pero hay dos mejoras que quiero hacer. En primer lugar, no hemos codificado de forma muy defensiva. Por ejemplo, ¿qué pasa si, por alguna razón, el mensaje contiene JSON no válido? Comprobemos eso: si null === $data, entonces lanza un new MessageDecodingFailedException('Invalid JSON')

... lines 1 - 6
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
... lines 8 - 10
class ExternalJsonMessageSerializer implements SerializerInterface
{
public function decode(array $encodedEnvelope): Envelope
{
... lines 15 - 19
if (null === $data) {
throw new MessageDecodingFailedException('Invalid JSON');
}
... lines 23 - 34
}
... lines 36 - 40
}

Te mostraré por qué usamos exactamente esta clase de excepción dentro de un minuto. Pero probemos esto con algún JSON no válido y... veamos qué ocurre. Ve a reiniciar el trabajador para que vea nuestro nuevo código:

php bin/console messenger:consume -vv external_messages

Luego, en el gestor RabbitMQ, vamos a cometer un error JSON muy molesto: añadir una coma después de la última propiedad. ¡Publica ese mensaje! Bien, muévete y... ¡explosión!

MessageDecodingFailedException: JSON inválido

Ah, y es interesante: ¡esto mató a nuestro proceso de trabajo! Sí, si se produce un error durante el proceso de descodificación, la excepción mata a tu trabajador. No es lo ideal... pero en realidad... no es un problema. En producción, ya estarás utilizando algo como el supervisor, que reiniciará el proceso cuando muera.

Fallo por falta de un campo JSON

Añadamos código para comprobar un posible problema diferente: comprobemos si falta esta clave emoji: si no isset($data['emoji']), esta vez lanza una excepción normal: throw new \Exception('Missing the emoji key!').

... lines 1 - 10
class ExternalJsonMessageSerializer implements SerializerInterface
{
public function decode(array $encodedEnvelope): Envelope
{
... lines 15 - 23
if (!isset($data['emoji'])) {
throw new \Exception('Missing the emoji key!');
}
... lines 27 - 34
}
... lines 36 - 40
}

Bien, pasa y reinicia el trabajador:

php bin/console messenger:consume -vv external_messages

De nuevo en Rabbit, elimina la coma extra y cambia emoji por emojis. Publica! En el terminal... ¡genial! ¡Ha explotado! Y aparte de la clase de excepción... parece idéntico al fallo que vimos antes:

Excepción: ¡Falta la tecla emoji!

Pero... acaba de ocurrir algo diferente. Intenta volver a ejecutar el trabajador:

php bin/console messenger:consume -vv external_messages

¡Woh! ¡Ha explotado! Falta la tecla emoji. Ejecútalo de nuevo:

php bin/console messenger:consume -vv external_messages

La magia de la MessageDecodingFailedException

¡El mismo error! Ésta es la diferencia entre lanzar unException normal en el serializador y el especial MessageDecodingFailedException. Cuando lanzas un MessageDecodingFailedException, tu serializador está diciendo básicamente:

¡Oye! Algo ha ido mal... y quiero lanzar una excepción. Pero, creo que deberíamos descartar este mensaje de la cola: no tiene sentido de intentarlo una y otra vez. ¡Kthxbai!

Y eso es súper importante. Si no descartamos este mensaje, cada vez que nuestro trabajador se reinicie, fallará con ese mismo mensaje... una y otra vez... para siempre. Cualquier mensaje nuevo empezará a acumularse detrás de él en la cola.

Así que cambiemos el Exception por MessageDecodingFailedException. Pruébalo ahora:

... lines 1 - 10
class ExternalJsonMessageSerializer implements SerializerInterface
{
public function decode(array $encodedEnvelope): Envelope
{
... lines 15 - 23
if (!isset($data['emoji'])) {
throw new MessageDecodingFailedException('Missing the emoji key!');
}
... lines 27 - 34
}
... lines 36 - 40
}
php bin/console messenger:consume -vv external_messages

Explotará la primera vez... pero el MessageDecodingFailedException debería haberlo eliminado de la cola. Cuando ejecutemos el trabajador ahora:

php bin/console messenger:consume -vv external_messages

¡Sí! El mensaje ha desaparecido y la cola está vacía.

A continuación, vamos a añadir un superpoder más a este serializador. ¿Qué pasa si ese sistema externo envía a nuestra aplicación muchos tipos diferentes de mensajes: no sólo un mensaje para registrar emojis, sino quizá también mensajes para borrar fotos o cocinar una pizza? ¿Cómo puede nuestro serializador averiguar qué mensajes son cada uno... y qué objeto de mensaje debe crear?

Leave a comment!

4
Login or Register to join the conversation
SWORP Avatar
SWORP Avatar SWORP | posted hace 11 meses | edited

Is there a way to handle all the exception from Messages the way it is done by Subscribers to KernelEvents::EXCEPTION or ExceptionListener->onKernelException?

Seems like exceptions occurring when processing messages is some separate event , other than KernelEvents::EXCEPTION.

For example, need to send email to admin, in case some problem occurs with the queue.
Thanks!

Reply

Hey SWORP!

Yea, that makes sense :). Have you tried the WorkerMessageFailedEvent event? It's triggered each time a message fails while being processed by the worker.

Cheers!

Reply
Julien Avatar

So the message is removed from the queue, and lost ? But if it's our fault ? I mean we didn't implemented yet. Could we leave the message but at the end of the queue ? or with a low priority. So we could implement it and then get it ?

Reply

Hey Julien,

I suppose failure transport is what you need here: https://symfonycasts.com/sc... . The idea is to retry a few times and if it still fails - moved that message to the failure transport where you will be able to retry it whenever you're ready.

I hope this helps!

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