← Wiki

Dead Letter Channel no Mule: a dead letter queue do Anypoint MQ

  • 11 min de leitura
Nesta página

Esta página é para quem consome filas do Anypoint MQ num flow Mule e precisa decidir o destino da mensagem que falha sempre. Ela mostra o padrão Dead Letter Channel, como a dead letter queue (DLQ) do Anypoint MQ o implementa e o que fica com o broker e o que fica com a aplicação. O cenário é o da série: um pedido que passa por pagamento, estoque e nota fiscal.

A mensagem de nota fiscal que não pode sumir nem voltar para sempre

O livro Enterprise Integration Patterns parte de uma pergunta: o que o sistema de mensagens faz com uma mensagem que não consegue entregar (Dead Letter Channel)?

No cenário de pedidos, o consumidor de nota fiscal lê o evento de pedido criado e calcula os impostos antes de emitir a nota. Com o serviço de impostos fora do ar, ele não consegue processar a mensagem.

As duas saídas óbvias são ruins. Se o consumidor confirma e segue, o pedido se perde: a nota nunca sai, e ninguém fica sabendo. Devolver a mensagem à fila sem limite faz o consumidor tentar a mesma mensagem de novo e de novo, até o tempo de vida (TTL) das mensagens, configurado na fila, acabar e o Anypoint MQ apagá-la (filas). A mensagem precisa sair do caminho e continuar guardada em algum lugar.

No livro, quem tira a mensagem do caminho é o sistema de mensagens

Quando o sistema de mensagens conclui que não pode ou não deve entregar uma mensagem, ele pode movê-la para um Dead Letter Channel. O livro avisa que o jeito como esse canal funciona depende da implementação de cada sistema de mensagens, se ele tiver um, e que o canal também aparece com o nome de dead message queue ou dead letter queue (DLQ). Ao mover a mensagem, o sistema pode registrar o canal em que ela deveria ter sido entregue (Dead Letter Channel). Quem move a mensagem é o sistema de mensagens, não o receptor, e é isso que separa este padrão do Invalid Message Channel, mais abaixo.

O desenho do padrão

O diagrama redesenha o sketch da página do padrão e mostra quem tira a mensagem do caminho: o remetente envia pelo canal, a entrega ao receptor pretendido falha, e o sistema de mensagens redireciona a mensagem morta (dead message) para o Dead Letter Channel.

O Remetente envia uma mensagem ao Canal, dentro do Sistema de mensagens. A entrega ao Receptor pretendido falha, e o sistema redireciona a entrega: a Mensagem morta vai para o Dead Letter Channel.

No Anypoint MQ, a DLQ é da fila e o flow só confirma ou não

No Anypoint MQ, uma DLQ é uma fila comum que só recebe mensagens não entregues. Ela é atribuída na configuração da fila de origem, e quem move a mensagem é o broker (filas). Neste desenho, a aplicação Mule não escreve na DLQ: ela só decide se confirma (ACK) ou não confirma (NACK) cada mensagem.

A ponte com o livro está na contagem. No Anypoint MQ, um NACK conta como uma tentativa de entrega sem sucesso, e o fluxograma da documentação devolve à fila toda entrega não confirmada e confere o limite de tentativas antes de mover a mensagem para a DLQ (filas). Por isso, a mensagem que o consumidor não consegue processar é, para o broker, o que o livro chama de mensagem que não pode ser entregue.

Por isso esta página não tem exemplo executável no repositório mulesoft-patterns-examples: a configuração e o flow abaixo seguem a documentação e precisam de um broker Anypoint MQ.

A DLQ fica na configuração da fila

Em Anypoint Platform > MQ > Destinations, a configuração tem dois passos (filas):

  1. Crie primeiro a fila que vai servir de DLQ, order-created-invoice-dlq. Ela precisa ser do mesmo tipo da fila de origem (standard ou FIFO), estar na mesma região e no mesmo ambiente e pertencer à mesma conta.
  2. Na fila de nota fiscal, order-created-invoice, ligue Assign a Dead Letter Queue, escolha order-created-invoice-dlq em Dead Letter Queue Name e, se quiser, ajuste Delivery attempts before reroute: quantas vezes o Anypoint MQ tenta entregar a mensagem antes de movê-la para a DLQ. O padrão é 10, e o valor vai de 1 a 1000.

A Admin API do Anypoint MQ também cria e gerencia filas (APIs do Anypoint MQ). Na configuração que ela devolve, a fila de nota fiscal fica assim (trecho):

{
  "type": "queue",
  "queueId": "order-created-invoice",
  "fifo": false,
  "maxDeliveries": 10,
  "deadLetterQueueId": "order-created-invoice-dlq"
}

E a DLQ passa a listar as filas de origem no campo deadLetterSources (filas):

{
  "type": "queue",
  "queueId": "order-created-invoice-dlq",
  "deadLetterSources": ["order-created-invoice"]
}

Não confunda Delivery attempts before reroute com o Max Redelivery Count da aba Redelivery do conector: esse é um parâmetro da política de redelivery do Mule, que lança MULE:REDELIVERY_EXHAUSTED quando a mesma mensagem passa do limite de reentregas ao flow (filas, redelivery policy).

O que fica com o consumidor: ACK ou NACK

O Subscriber do conector usa por padrão o modo de confirmação AUTO: só faz o ACK, que remove a mensagem da fila, se o flow terminar sem erro. Se o flow terminar com um erro propagado, a mensagem recebe um NACK e volta para a fila. Se o flow não terminar dentro do acknowledgementTimeout (dois minutos, quando não configurado), ela também volta (Subscriber, ACK e NACK). O Subscriber também usa por padrão a estratégia prefetch, que guarda mensagens num buffer local; com ela, o Anypoint MQ tenta respeitar o acknowledgementTimeout, mas estende o prazo da mensagem que passa 80% dele no buffer, e a documentação recomenda não usar prefetch quando o prazo precisa ser respeitado (Subscriber, ACK e NACK).

Os outros dois modos mudam essa conta. No MANUAL, o próprio flow chama as operações ACK ou NACK com o ackToken da mensagem. No IMMEDIATE, a mensagem é confirmada e removida da fila antes do processamento e não volta se o processamento falhar. Para não perder mensagens nesse modo, a documentação recomenda uma DLQ (ACK e NACK), mas o broker não tem mais o que mover: a mensagem já saiu da fila, e quem a publicaria na DLQ seria o próprio flow, no tratamento de erro.

Com o modo padrão, o consumidor fica assim; issue-invoice é o flow que chama o serviço de impostos e emite a nota.

<!-- Configuração de referência -->
<!-- ${secure::...} exige um <secure-properties:config> na aplicação -->
<anypoint-mq:config name="Anypoint_MQ_Config">
  <anypoint-mq:connection url="${anypoint.mq.url}"
    clientId="${anypoint.mq.clientId}"
    clientSecret="${secure::anypoint.mq.clientSecret}"/>
</anypoint-mq:config>

<flow name="invoice-consumer">
  <anypoint-mq:subscriber config-ref="Anypoint_MQ_Config" destination="order-created-invoice"/>
  <flow-ref name="issue-invoice"/>
  <error-handler>
    <on-error-propagate type="ANY">
      <logger level="WARN" message="#['Nota fiscal não emitida; a mensagem volta para a fila. correlationId: ' ++ correlationId]"/>
    </on-error-propagate>
  </error-handler>
</flow>

O on-error-propagate só registra a falha; é o erro propagado que faz o Subscriber responder com NACK (On Error Propagate). Nenhuma linha manda a mensagem para a DLQ: quem conta as tentativas e move a mensagem é o broker.

O caminho da mensagem com a DLQ

O diagrama mostra o que a aplicação decide e o que o broker faz depois com uma mensagem da fila de nota fiscal.

No Anypoint MQ, a fila order-created-invoice (fila de nota fiscal) entrega ao Consumidor de nota fiscal (flow Mule). Com ACK, quando o flow terminou sem erro, a mensagem é removida da fila. Com NACK, ou sem resposta no acknowledgementTimeout, ela volta para a fila. Esgotadas as tentativas de entrega (10 por padrão), ela vai para a order-created-invoice-dlq (DLQ), e quem cuida da DLQ analisa e reprocessa.

Dead Letter Channel ou Invalid Message Channel

O livro tem um padrão vizinho para outro problema: a mensagem que chega, mas não faz sentido para o receptor. No Invalid Message Channel, o próprio receptor move a mensagem para um canal especial de mensagens que ele não conseguiu processar (Invalid Message Channel).

Um pedido sem o CPF ou CNPJ do cliente vai falhar nas 10 tentativas do mesmo jeito e chegar à DLQ misturado com os pedidos que só falharam porque o serviço de impostos estava fora do ar. Quando o flow sabe que a mensagem nunca vai ser válida, ele mesmo a manda para uma fila separada:

<!-- Configuração de referência -->
<flow name="invoice-consumer">
  <anypoint-mq:subscriber config-ref="Anypoint_MQ_Config" destination="order-created-invoice"/>
  <choice>
    <when expression="#[isEmpty(payload.customer.taxId)]">
      <raise-error type="INVOICE:INVALID_ORDER" description="Pedido sem CPF ou CNPJ do cliente"/>
    </when>
  </choice>
  <flow-ref name="issue-invoice"/>
  <error-handler>
    <on-error-continue type="INVOICE:INVALID_ORDER">
      <anypoint-mq:publish config-ref="Anypoint_MQ_Config" destination="order-created-invoice-invalid"/>
    </on-error-continue>
    <on-error-propagate type="ANY">
      <logger level="WARN" message="#['Nota fiscal não emitida; a mensagem volta para a fila. correlationId: ' ++ correlationId]"/>
    </on-error-propagate>
  </error-handler>
</flow>

O on-error-continue faz o flow terminar como se tivesse dado certo (On Error Continue), e com isso o modo AUTO confirma a mensagem na fila de nota fiscal depois que ela foi publicada em order-created-invoice-invalid (Raise Error, Publish). A fila de inválidas fica separada da DLQ porque a causa é outra: na DLQ, a mensagem pode voltar à fila depois que o serviço de impostos se recuperar; na fila de inválidas, o pedido precisa ser corrigido na origem, porque reenviá-lo como está daria o mesmo erro.

Cuidados com a DLQ

  • FIFO e DLQ não garantem a ordem. A DLQ de uma fila FIFO precisa ser FIFO, e mesmo assim a ordem se perde: a mensagem que vai para a DLQ sai da fila, e as seguintes são processadas sem ela. Se a ordem precisa ser estrita, a documentação diz para não atribuir DLQ à fila FIFO (filas).
  • A DLQ precisa de dono e de um caminho de volta. As mensagens da DLQ servem para descobrir por que não foram entregues, e, para recuperá-las, a documentação indica a API REST: ler da DLQ e escrever numa fila (filas). Alguém precisa saber que a DLQ recebeu mensagens e ter um procedimento para devolvê-las depois de corrigir a causa.
  • Olhar a fila de origem gasta tentativas. Numa fila com DLQ, ver mensagens pelo Message Browser conta como entrega, e devolvê-las à fila conta como NACK, uma tentativa sem sucesso (filas).
  • A DLQ também tem TTL. Como em qualquer fila, o TTL das mensagens (Message TTL), configurado na fila, vai até duas semanas (filas). Numa DLQ que ninguém olha, a mensagem é apagada quando o TTL da DLQ acabar.
  • Uma queda do serviço de impostos manda mensagens válidas para a DLQ. Com o serviço fora do ar, todas as mensagens falham e podem esgotar as tentativas. Para esse caso existe o circuit breaker do Subscriber, desligado por padrão e configurado com <anypoint-mq:circuit-breaker>: depois de errorsThreshold erros seguidos, ele para de consumir durante o tripTimeout. Ele reduz as tentativas gastas, mas não as elimina. Com o circuito aberto, as mensagens já consumidas recebem NACK e voltam para a fila; passado o tripTimeout, uma única mensagem testa o serviço e, se falhar, o circuito abre de novo (Subscriber). Como cada NACK conta como tentativa sem sucesso (filas), a inferência é que esses NACKs e os testes que falham também gastam tentativas.
  • Reprocessar pede um receptor idempotente. As filas standard entregam pelo menos uma vez, e às vezes mais de uma (filas). Uma mensagem que volta da DLQ pode encontrar parte do trabalho já feito, e o consumidor precisa produzir o mesmo efeito recebendo uma ou duas vezes, como mostra a página do idempotent receiver.

Quando a DLQ não resolve

A DLQ existe porque há uma fila entre quem envia e quem processa. Pelo mesmo raciocínio, ela não serve a um flow com HTTP Listener, em que não há fila entre quem chama e o flow: quem chamou está esperando a resposta, e o erro volta para ele na hora, com o status e o corpo que o contrato da API define. Não há mensagem guardada para o broker mover, e reenviar a requisição é decisão de quem chamou.

A DLQ também não substitui o tratamento de um erro que o flow sabe resolver. A mensagem que nunca vai ser válida vai para a fila de inválidas. Uma falha curta, que uma nova tentativa logo depois resolveria, pode ser repetida no próprio flow com o Until Successful antes de o erro ser propagado. As tentativas, somadas, precisam caber no acknowledgementTimeout: se o prazo acabar com o flow ainda tentando, a mensagem volta para a fila e pode ser entregue de novo, a este ou a outro consumidor, enquanto a primeira execução continua (ACK e NACK), e essa volta gasta uma tentativa de entrega (filas). A DLQ fica para o que sobra depois disso: a mensagem que o consumidor tentou processar e não conseguiu, e que alguém vai precisar olhar.

Os outros padrões da série, com o componente do Mule que implementa cada um, estão no catálogo de padrões de integração no Mule.