In a previous article, we introduced the Messenger outbox, which saves the messages you send in the same database transaction as your changes. Symfony 8.2 adds the other half of the pattern: an inbox to keep track of messages your application has already processed.

Skipping Redelivered Messages

Nicolas Grekas
Contributed by Nicolas Grekas in #66957

Brokers deliver messages at least once. If a worker commits the changes of its handlers but crashes (or loses the connection) before acknowledging the message, the broker delivers it again and the handlers run twice. That's how a customer ends up charged twice or receives the same email twice. Until now, you had to make each handler idempotent yourself.

The new inbox transport option lets Messenger recognize these redeliveries and skip handlers that have already completed successfully. Inside the transaction that runs your handlers, Symfony stores the transport name and the message id in a messenger_inbox database table. When a redelivered message is already in that table, its handlers are skipped and the message is acknowledged:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# config/packages/messenger.yaml
framework:
    messenger:
        # adds the message id that the inbox needs to every message
        identity_stamps: true

        transports:
            orders:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                inbox: true
                # or: inbox: { connection: billing }

        buses:
            messenger.bus.default:
                # the inbox needs your handlers to run in a database transaction
                middleware: [doctrine_transaction]

If a handler fails, the transaction rolls back both its changes and the inbox entry, so the message can be processed again on the next retry. If two workers receive the same message, the second waits for the first worker's transaction to finish.

Run messenger:setup-transports to create the inbox table. The table is also included in your Doctrine schema, so you can create it through a migration.

Things to keep in mind about the inbox:

  • it requires the Doctrine Messenger bridge (symfony/doctrine-messenger) and must use the same database connection as your handlers;
  • retention (7 days by default) must be longer than the time your transport can take to redeliver a message;
  • messages retried from a failure transport are recorded under the transport where they originally failed;
  • messages handled by batch handlers are not recorded in the inbox.

Messages dispatched with DispatchAfterCurrentBusStamp must go to a transport that uses an outbox. The inbox entry is committed before these messages are sent, so a failed send could otherwise lose them. Messenger refuses to send them without an outbox.

Published in #Living on the edge