I have implemented a Message Transport protocol in a Symfony-Project, that implements the TransportInterface.
While playing around with it and learning how it works, I came across a weird issue that I like to elaborate on and understand more:
When a message is missing a corresponding message handler, the Message Transport will first be called with the method send, followed by a method call to reject.
I would assume, that the method reject will be executed only. Why is the message being sent once more? I figured out, that it has nothing to do with the setting of max_retries.
My custom Message Transport is doing the following:
get: it reads the message from a custom table inside theget-method.send: it just logs "send" into the console with the corresponding envelopereject: it just logs "reject" into the console with the corresponding envelope
What I am doing for testing is:
- I write manually a message into the message-table, which automatically causes the
getmethod to retrieve the corresponding message - messenger tries to find a suitable message handler and finds nothing because there is none yet
- messenger is printing to the console:
07:58:06 INFO [messenger] Received message App\Message\IngestMessage ["class" => "App\Message\IngestMessage"]
07:58:06 WARNING [messenger] Error thrown while handling message App\Message\IngestMessage. Sending for retry #1 using 1000 ms delay. Error: "No handler for message "App\Message\IngestMessage"." ["class" => "App\Message\IngestMessage","retryCount" => 1,"delay" => 1000,"error" => "No handler for message "App\Message\IngestMessage".","exception" => Symfony\Component\Messenger\Exception\NoHandlerForMessageException^ { …}]
- then it logs "SEND" to the console because the
send-method will be executed which is doingconsole.log("SEND") - it logs "REJECT" to the console because the
reject-method will be executed which is doingconsole.log("REJECT")
Code:
messenger.yml:
framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
ingest:
dsn: '%env(MESSENGER_TRANSPORT_DSN_INGEST)%'
retry_strategy:
max_retries: 5
delay: 1000
multiplier: 2
max_delay: 0
routing:
'App\Message\IngestMessage': ingest
IngestDataTransport.php:
class IngestDataTransport implements TransportInterface
{
public function __construct(private readonly DataBucketManager $dataBucketManager)
{}
public function get(): iterable
{
$dto = $this->dataBucketManager->pop();
echo ".";
if(!$dto) {
return [];
}
dump("GET", $dto);
$event = IngestMessage::fromDataBucketDTO($dto);
$envelop = new Envelope($event);
return [$envelop];
}
public function ack(Envelope $envelope): void
{
// TODO: Implement ack() method.
dump('ACK');
}
public function reject(Envelope $envelope): void
{
// TODO: Implement reject() method.
dump('REJECT', $envelope->getMessage());
}
public function send(Envelope $envelope): Envelope
{
dump('SEND', $envelope->getMessage());
return $envelope;
}
}
Once I implement a corresponding message handler, neither the SEND-method nor the REJECT-method is executed, which makes total sense.
However, why comes reject with a send? It would always duplicate a failed message.