kithara.patterns.dead-letter-backoff

with-dead-letter-backoff

(with-dead-letter-backoff components & [{:keys [backoff backoff-exchange retry-exchange queue]}])

Wrap the given component(s) with setup of dead letter queues and exchanges. The following options can be given:

  • :queue: options for the dead letter queue (including :queue-name, :durable?, :exclusive? and :auto-delete?),
  • :backoff-exchange: options for the exchange the dead letter queue will be bound to (including :exchange-name, :durable?, :exclusive? and :auto-delete?),
  • :retry-exchange: options for the exchange that messages-to-retry will be published to (see :backoff-exchange for options),
  • :backoff: a map of :min and :max giving minimal and maximal backoff intervals in milliseconds (default: 50ms <= backoff <= 1m).

All three values can be strings, in which case they will be used for queue/exchange names and result in a non-durable setup. If no names are given, they will be derived from the consumer queue’s. Example:

(defonce rabbitmq-consumer
  (-> ...
      (with-dead-letter-backoff
        {:queue            "dead-letters"
         :backoff-exchange "dead-letters-backoff"
         :retry-exchange   "dead-letters-retry"})
      (kithara.core/with-queue
        "consumer-queue"
        ...)
      ...))

Topology

The following is a minimal example, auto-generating names for exchanges/queues:

(require '[kithara.core :as kithara])

(defonce rabbitmq-consumer
  (-> (kithara/consumer ...)
      (with-dead-letter-backoff)
      (kithara/with-queue
        "consumer-queue"
        {:exchange "consumer-exchange", :routing-keys ["#"]})
      (kithara/with-channel)
      (kithara/with-connection)))

This will create/expect the following exchanges/queues:

consumer-exchange exchange has to exist
consumer-queue--retry exchange fanout exchange
consumer-queue--backoff exchange fanout exchange
consumer-queue--dead-letters queue x-dead-letter-exchange: <retry exchange>

And the following bindings:

consumer-queue - # via consumer-exchange
- all from consumer-queue--retry (fanout)
consumer-queue--dead-letters all from consumer-queue--backoff (fanout)

Behaviour

If a consumer NACKs or REJECTs a message with the :requeue? flag set (default on NACK), or message processing throws an exception, the message will be dead-lettered. This means:

  1. Publish the message to the backoff exchange, setting an “expiration” value. The message will be added to the dead-letter queue.
  2. Once the timeout expires, the message will be published to the retry exchange. Since a binding to this exchange was added to the original consumer queue, the message will reappear there.

Caveat

There is one caveat (also noted in the RabbitMQ documentation), consisting of expiry only happening at the head of the queue. This means that all messages in the dead letter queue will take at least the same time as the current head to be republished.

See: https://www.rabbitmq.com/ttl.html

with-durable-dead-letter-backoff

(with-durable-dead-letter-backoff components & [{:keys [backoff backoff-exchange retry-exchange queue]}])

See with-dead-letter-backoff. Will create/expect durable, non-exclusive and non-auto-delete dead-letter queues/exchanges.

Note that this makes only sense if the original consumer queue has the same properties, since otherwise you’ll lose dead-lettered messages on retry.