Padrões de integração no Mule: você já usa, só não sabe o nome
- Por Bruno Souza
- Integração
- 11 min de leitura
Nesta página
Código
Quem coloca um Scatter-Gather num flow do Mule está usando um padrão descrito em 2003, com o mesmo nome, no livro Enterprise Integration Patterns, de Gregor Hohpe e Bobby Woolf. O mesmo vale para o Choice, o For Each, o Transform Message e as filas, os message exchanges e a dead letter queue do Anypoint MQ. São padrões de integração embutidos em componentes, e dá para usar todos por anos sem saber que são padrões. Este artigo é para quem já monta flows no Mule 4 e quer saber o que esse vocabulário acrescenta: o problema que cada componente resolve, o que ele cobra e quando ele sobra.
Um livro de 2003 que ainda dá nome às coisas
O livro reúne 65 padrões para projetar integração por mensagens sem ficar preso a um produto (Hohpe). Cada padrão tem nome, ícone, um problema escrito como pergunta e uma solução. O Idempotent Receiver parte da pergunta de como um receptor lida com mensagens duplicadas. O Dead Letter Channel pergunta o que o sistema de mensagens faz com uma mensagem que não consegue entregar (Idempotent Receiver, Dead Letter Channel).
O prefácio é de setembro de 2003 (amostra da Pearson), e as referências são da época: a página do Dead Letter Channel cita o MQSeries da IBM e um livro sobre JMS. Os produtos mudaram, as perguntas não. Uma mensagem duplicada numa fila JMS e um evento duplicado no Anypoint MQ pedem a mesma decisão: o que fazer quando a mesma coisa chega duas vezes.
Os padrões que já estão nos componentes
| Componente no Mule | Padrão no livro | O problema que resolve |
|---|---|---|
| Fila do Anypoint MQ | Garantir que um único receptor consuma cada mensagem | |
| Scatter-Gather | Enviar a mesma mensagem a vários destinos e juntar as respostas numa só | |
| Choice | Mandar cada mensagem ao destino certo a partir do conteúdo dela | |
| For Each | Processar, item a item, uma mensagem que traz vários itens | |
| Transform Message | Fazer sistemas com formatos diferentes trocarem dados | |
| Message exchange do Anypoint MQ | Entregar uma cópia do evento a cada interessado | |
| Dead letter queue do Anypoint MQ | Tirar do caminho a mensagem que não pode ser entregue | |
| Idempotent Message Validator | Receber a mesma mensagem duas vezes sem efeito dobrado |
A documentação do Mule descreve cada um desses componentes pelo que ele faz. O Scatter-Gather executa as rotas em paralelo e agrega o resultado (Scatter-Gather). O Choice executa só a primeira rota cuja expressão for verdadeira, ou a rota padrão (Choice). O For Each divide o payload em elementos e processa um por um (For Each). O Transform converte os dados de entrada para outra estrutura ou formato (Transform). No Anypoint MQ, um message exchange envia, por padrão, cada mensagem a todas as filas ligadas a ele (message exchanges); apesar do nome, ele não tem nada a ver com o Anypoint Exchange, onde ficam os assets. Cada fila pode ter a sua dead letter queue (filas). As páginas do livro descrevem os mesmos mecanismos a partir do problema (Scatter-Gather, Content-Based Router, Splitter, Message Translator, Publish-Subscribe Channel).
O nome também mostra do que o componente é feito. No livro, o Scatter-Gather envia a requisição a vários destinatários, por uma Recipient List ou por um canal publish-subscribe, e usa um Aggregator para juntar as respostas numa só (Scatter-Gather). São dois padrões dentro de um componente, e o segundo traz uma pergunta que o primeiro não tem: o que fazer quando uma das respostas falha. O Mule responde de um jeito específico. Ele espera todas as rotas terminarem e, se alguma falhou, lança MULE:COMPOSITE_ROUTING, com os erros e também os resultados das rotas que deram certo (Scatter-Gather). Quem trata esse erro decide se uma resposta parcial serve.
A fila do Anypoint MQ é um Point-to-Point Channel: cada mensagem é consumida por um único receptor (Point-to-Point Channel). Trocar essa fila por uma chamada HTTP muda mais que o transporte. Com HTTP, se o receptor estiver fora do ar, a chamada falha na hora e o reenvio vira problema de quem chamou. Com o Anypoint MQ, a mensagem fica no broker até ser consumida, pelo tempo de vida configurado na fila, de no máximo duas semanas (filas).
O VM Connector também tem filas, e isso confunde. Ele não é um broker: as filas dele vivem dentro do runtime do Mule e servem para a comunicação dentro de uma aplicação ou entre aplicações no mesmo Mule (VM Connector). As filas VM persistentes nem existem no CloudHub 2.0 ou no Runtime Fabric. Uma fila VM não substitui uma fila do Anypoint MQ: se a mensagem precisa esperar um receptor que está fora da aplicação, quem guarda essa mensagem é o broker.
O correlationId que aparece em todo log do Mule é parente do Correlation Identifier. No livro, o ID vai na requisição e volta na resposta, para quem recebeu a resposta saber a que requisição ela pertence (Correlation Identifier). No Mule, o ID acompanha a requisição de uma aplicação para a outra. O HTTP Listener lê o header X-CORRELATION-ID, ou gera um ID quando ele não vem, e o HTTP Request tem a opção Send Correlation Id, que vem como AUTO (Correlation ID, HTTP Connector). Num teste MUnit com o Mule 4.9, em que um flow chama outro por HTTP com essa opção no padrão, o flow chamado recebeu no header o mesmo ID de quem chamou. Entre duas aplicações o caminho é o mesmo header, então os logs das duas mostram o mesmo ID, e dá para seguir a requisição de ponta a ponta. Os dois usos ligam mensagens da mesma conversa: o do livro olha para a resposta, o do Mule para o caminho.
O que muda quando você sabe o nome
O Idempotent Message Validator mostra bem a diferença. Ele deixa seguir no flow só as mensagens com um ID ainda não visto e lança MULE:DUPLICATE_MESSAGE para as repetidas (Idempotent Message Validator). Na forma mínima, sem nenhum parâmetro, ele fica assim (a sintaxe é a da documentação):
<idempotent-message-validator doc:name="Idempotent Message Validator"/>
Quem conhece só o componente coloca essa linha no flow e segue. Quem conhece o padrão parte do problema: a mesma mensagem pode chegar mais de uma vez, e o receptor precisa produzir o mesmo efeito recebendo uma ou duas vezes (Idempotent Receiver). Dessa definição saem duas perguntas que a linha acima não responde.
O que identifica “a mesma mensagem”? Sem configuração, o ID é #[correlationId], e esse ID não foi feito para isso. Ele erra nos dois sentidos. Um cliente que reenvia o mesmo pedido sem o header X-CORRELATION-ID chega com um ID novo, gerado pelo Mule, e o duplicado passa (Correlation ID). E o contrário: uma aplicação Mule que chama a sua API três vezes dentro de um For Each, com três pedidos diferentes, manda o mesmo ID nas três chamadas. Num teste MUnit com o validador sem configuração, a primeira chamada passou e as outras duas foram barradas como duplicadas. O ID certo é uma chave de negócio, como o número do pedido, informada em idExpression.
Por quanto tempo lembrar? Sem configuração, os IDs ficam num object store não persistente, com validade de 5 minutos (parâmetros do validador). Um reenvio seis minutos depois, ou depois de um restart da aplicação, passa como mensagem nova. A janela certa depende de quanto tempo os reenvios podem demorar no seu cenário, e o object store pode ser persistente.
Nos dois casos o componente fez exatamente o que a documentação diz. Quem errou foi quem configurou, porque não perguntou qual problema a configuração devia resolver.
O padrão não falha; falha a escolha ou a implementação
Eu nunca vi um padrão dar errado. O erro é de quem implementa, como no validador configurado só com os valores padrão. E existe o outro lado: o arquiteto que projeta algo desnecessário, que poderia ser mais simples.
Um exemplo hipotético desse segundo caso: um evento de pedido criado que só o sistema de faturamento consome sai por um message exchange do Anypoint MQ, com uma única fila ligada a ele, e passa por um validador idempotente no consumidor, que guarda os IDs num object store. Cada peça tem uma justificativa no livro, mas duas delas resolvem problemas que não existem aqui. Há um único interessado, então o message exchange não distribui nada. E a operação de faturamento já é idempotente pela chave do pedido, então o validador só acrescenta um store para dimensionar. A dead letter queue da fila é outra conversa: a mensagem que falha sempre vai existir, com um consumidor ou com dez.
Saber o nome do padrão também ajuda a ver quando ele não é necessário:
- Publish-subscribe faz sentido quando há, ou vai haver logo, mais de um consumidor do mesmo evento. Com um único consumidor, uma fila basta, e o message exchange vira mais um lugar para procurar a mensagem que sumiu.
- Idempotent receiver com object store faz sentido quando a operação de destino não é idempotente por natureza. Um upsert pela chave de negócio pode resolver o duplicado sem um store para dimensionar, desde que a operação não tenha outros efeitos, como um e-mail enviado ou um evento publicado.
- Scatter-Gather faz sentido quando as chamadas são independentes e todas as respostas importam. Se a segunda depende da primeira, não dá para rodar em paralelo. E como ele espera todas as rotas, uma rota lenta segura a resposta inteira.
Os padrões num cenário de pedidos
No Mule, alguns desses padrões já vêm embutidos nos componentes e conectores, e muitas vezes a gente usa sem saber que está usando um padrão. A série existe para dar nome ao que já está ali, e quem se aprofunda em cada padrão é o wiki, com uma página por padrão. Cada página traz o problema, o diagrama do padrão no livro, o diagrama de como ele fica no Mule, um exemplo e quando não usar. O catálogo de padrões de integração no Mule liga cada componente ao seu padrão e à sua página.
A arquitetura do exemplo
O diagrama mostra o cenário que as páginas usam e onde alguns dos padrões aparecem nele.
A API de pedidos recebe o pedido e publica o evento de pedido criado num message exchange do Anypoint MQ, que entrega uma cópia à fila de cada interessado: pagamento, estoque e nota fiscal (publish-subscribe). O consumidor de pagamento pode receber o mesmo pedido mais de uma vez e precisa cobrá-lo uma vez só (idempotent receiver). Quando o consumidor de nota fiscal não confirma uma mensagem depois de esgotadas as tentativas de entrega, o Anypoint MQ a move para a DLQ da fila (dead letter channel); por padrão são 10 tentativas (filas). O consumidor de pagamento chama o provedor de pagamento com um ID, o provedor responde depois por callback com o mesmo ID, e a confirmação do pagamento usa esse ID para ligar a resposta à requisição que a originou (correlation identifier).
O que roda dentro do Mule tem exemplo executável com teste MUnit no repositório mulesoft-patterns-examples, sem sistemas externos. O que depende do broker, como o message exchange e a DLQ, aparece no wiki com a configuração do Anypoint MQ explicada a partir da documentação, porque não roda sem um broker.
Antes de abrir o wiki, vale um exercício: abrir uma aplicação Mule que você mantém, listar os componentes da tabela acima que ela usa e, para cada um, responder qual problema ele resolve ali.