If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeDe alguna manera queremos adjuntar un identificador único -una cadena cualquiera- que permanezca con el mensaje para siempre: tanto si se gestiona inmediatamente, como si se envía a un transporte, o incluso se vuelve a intentar varias veces.
¿Cómo podemos adjuntar más... "cosas" adicionales a un mensaje? Dándole nuestro propio sello! En el directorio Messenger/
, crea una nueva clase PHP llamada UniqueIdStamp
. Los sellos también tienen una sola regla: implementanMessengerEnvelopeMetadataAwareContainerReaderInterface
. No, estoy bromeando, ese sería un nombre tonto. Sólo tienen que implementar StampInterface
.
... lines 1 - 2 | |
namespace App\Messenger; | |
use Symfony\Component\Messenger\Stamp\StampInterface; | |
class UniqueIdStamp implements StampInterface | |
{ | |
... lines 9 - 19 | |
} |
Y... ¡eso es todo! Se trata de una interfaz vacía que sólo sirve para "marcar" objetos como sellos. Dentro... podemos hacer lo que queramos... siempre que PHP pueda serializar este mensaje... lo que básicamente significa: siempre que contenga datos simples. Añadamos una propiedad private $uniqueId
, y luego un constructor sin argumentos. Dentro, digamos$this->uniqueId = uniqid()
. En la parte inferior, ve a Código -> Generar -o Comando+N en un Mac- y genera el getter... que devolverá un string
.
... lines 1 - 2 | |
namespace App\Messenger; | |
use Symfony\Component\Messenger\Stamp\StampInterface; | |
class UniqueIdStamp implements StampInterface | |
{ | |
private $uniqueId; | |
public function __construct() | |
{ | |
$this->uniqueId = uniqid(); | |
} | |
public function getUniqueId(): string | |
{ | |
return $this->uniqueId; | |
} | |
} |
Sello, ¡hecho!
A continuación, dentro de AuditMiddleware
, antes de llamar al siguiente middleware -que llamará al resto del middleware y, en última instancia, manejará o enviará el mensaje- vamos a añadir el sello.
Pero, cuidado: tenemos que asegurarnos de que sólo adjuntamos el sello una vez. Como veremos dentro de un minuto, un mensaje puede pasar al bus -y, por tanto, al middleware- ¡muchas veces! Una vez cuando se envía inicialmente y otra cuando se recibe del transporte y se maneja. Si el manejo de ese mensaje falla y se vuelve a intentar, pasaría por el bus aún más veces.
Por tanto, empieza por comprobar si null === $envelope->last(UniqueIdStamp::class)
, y luego$envelope = $envelope->with(new UniqueIdStamp())
.
... lines 1 - 8 | |
class AuditMiddleware implements MiddlewareInterface | |
{ | |
public function handle(Envelope $envelope, StackInterface $stack): Envelope | |
{ | |
if (null === $envelope->last(UniqueIdStamp::class)) { | |
$envelope = $envelope->with(new UniqueIdStamp()); | |
} | |
... lines 16 - 21 | |
} | |
} |
Aquí hay algunas cosas interesantes. En primer lugar, cada Envelope
es "inmutable", lo que significa que, sólo por la forma en que se escribió esa clase, no puedes cambiar ningún dato en ella. Cuando llamas a $envelope->with()
para añadir un nuevo sello, en realidad no modifica el Envelope
. No, internamente, hace un clon de sí mismo más el nuevo sello.
Eso... no es muy importante, salvo que tienes que acordarte de decir$envelope = $envelope->with()
para que $envelope
se convierta en el nuevo objeto estampado.
Además, en lo que respecta a los sellos, un Envelope
podría, en teoría, contener varios sellos de la misma clase. El método $envelope->last()
dice:
Dame el más reciente añadido
UniqueIdStamp
o null si no hay ninguno.
Gracias a nuestro trabajo, debajo de la sentencia if -independientemente de si este mensaje se acaba de enviar... o se acaba de recibir de un transporte... o se está reintentando- nuestro Envelope
debería tener exactamente un UniqueIdStamp
. Recógelo con$stamp = $envelope->last(UniqueIdStamp::class)
. También voy a añadir una pequeña pista a mi editor para que sepa que esto es específicamente un UniqueIdStamp
.
... lines 1 - 8 | |
class AuditMiddleware implements MiddlewareInterface | |
{ | |
public function handle(Envelope $envelope, StackInterface $stack): Envelope | |
{ | |
if (null === $envelope->last(UniqueIdStamp::class)) { | |
$envelope = $envelope->with(new UniqueIdStamp()); | |
} | |
/** @var UniqueIdStamp $stamp */ | |
$stamp = $envelope->last(UniqueIdStamp::class); | |
... lines 19 - 21 | |
} | |
} |
Para ver si esto funciona, vamos a dump($stamp->getUniqueId())
.
... lines 1 - 8 | |
class AuditMiddleware implements MiddlewareInterface | |
{ | |
public function handle(Envelope $envelope, StackInterface $stack): Envelope | |
{ | |
if (null === $envelope->last(UniqueIdStamp::class)) { | |
$envelope = $envelope->with(new UniqueIdStamp()); | |
} | |
/** @var UniqueIdStamp $stamp */ | |
$stamp = $envelope->last(UniqueIdStamp::class); | |
dump($stamp->getUniqueId()); | |
return $stack->next()->handle($envelope, $stack); | |
} | |
} |
¡Vamos a probarlo! Si hemos hecho bien nuestro trabajo, para un mensaje asíncrono, ese dump()
se ejecutará una vez cuando se envíe el mensaje y otra vez dentro del trabajador cuando se reciba del transporte y se gestione.
Actualiza la página para estar seguro, y luego sube una imagen. Para ver si nuestro dump()
ha sido alcanzado, utilizaré el enlace de la barra de herramientas de depuración de la web para abrir el perfilador de esa petición. Haz clic en "Depuración" a la izquierda y... ¡ahí está! ¡Nuestro identificador único! Dentro de unos minutos, nos aseguraremos de que este código también se ejecute en el trabajador.
Y como el middleware se ejecuta para cada mensaje, también deberíamos poder verlo al borrar un mensaje. Haz clic en eso, luego abre el perfilador de la petición DELETE y haz clic en "Depurar". ¡Ja! Esta vez hay dos identificadores únicos distintos porque al borrar se envían dos mensajes diferentes.
A continuación, ¡vamos a hacer algo útil con esto! Dentro de nuestro middleware, vamos a registrar todo el ciclo de vida de un solo mensaje: cuando se envía originalmente, cuando se envía a un transporte y cuando se recibe de un transporte y se gestiona. Para saber en qué parte del proceso se encuentra el mensaje, vamos a utilizar de nuevo los sellos. Pero en lugar de crear nuevos sellos, leeremos los sellos del núcleo.
Hi there
is there a way to get retry count on Symfony message handler.
Messenger consumer command throws an error and retries it 5 times according to configuration. So I want to get retry count in handler so I can add some logic accordingly.
Hi!
For anyone looking, this question (a very interesting question!) is answered over here: https://symfonycasts.com/sc...
Cheers!
Hi, I would like to track the state of all messages to a tracking table, does any one know if this functionality is implemented or is planned to? thanks a lot!
Hey SamuelVicent!
You have a few options here. The easiest, but the one that contains the least information, would be to use the "doctrine" transport so that your messages are stored in a database. Then you can query them to get some information. But, this table structure is "fixed" and it's meant to help Messenger - not to give you all the information that you need, for example :).
So, if you need more information or control, I would register an event listener (or several) and update a separate table as things happen. If you look in the Worker class - https://github.com/symfony/... - you'll see a number of different events you can listen to (search for dispatchEvent).
But, let's back up: what kind of tracking would you like to do?
Cheers!
Hi, first of all, thanks for the feedback.
Let me explain my experience on this:
A few weeks ago we created 2 listeners for WorkerMessageFailedEvent and WorkerMessageHandledEvent.
We use mariaDb as a database engine for tracking the state of each message (only track messages of type SendEmailMessage.)
It worked in some way, but had some problems with deadlocks tables. We saw a solution on symfony github issue 42345.
We have been working on some solution using a custom Middleware and playing with Stamps but still have not a robust code.
We will try to apply the github solution to see if the Event approach works.
That's the reason why I asked you if you know some message tracking implementation.
Thanks a lot :)
Hey SamuelVicent!
Hmm, that's very interesting about the deadlocks! Let me know how it goes, I'd like to know :).
Cheers!
Hi Ryan,
We finally used listeners for tracking handled and failed messages.
Had to use rabbitMq as transport for managing messages, an a database in mariadb for the tracking (for knowing which messages were send properly or not).
Apart from this, using MariaDb as transport gave us DeadLock problems in a master to master setup (sync db between 2 servers).
Hope it helps as a experience :)
Regards and merry christmas!
Thanks for sharing all of that! And yes, I think that, in the real world, using the database (at least MySQL/MariaDB - the Pgsql queue transport is much more robust in messenger... because pgsql is much more robust) can cause dead lock issues.
Anyways, I always like to hear solutions that work nicely - thanks for sharing and a merry christmas back to you!
I understand main idea about adding stamps without duplicates, but I don't understand code design in Messenger source code. It's very confusing to me :)
Why methods "last" and "with" have different signatures? Why we need check stamp by FQCN but can't instantiate stamp by FQCN inside "with" method? What's the purpose of keeping stamps in array by "FQCN" => "stamp instance" as "key" => "value" inside envelope?
Hey Dariia M.
The with()
method clones such Envelope and if you pass in any stamps it will add them to the new cloned envelope object. It's just a way to create your current envelope with more or the same stamps it already has
The last()
method only finds the latest stamp added to your envelope by a FQCN. Since an envelope can have many stamps of the same type, storing them in an array indexed by their FQCN is a handy way to fetch them by type
Cheers!
// 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
}
}
I really thought the
MessengerEnvelopeMetadataAwareContainerReaderInterface
thing was real at first ..!