Pub-Sub-Store: Exemplo Prático de Arquitetura Publish/Subscribe
Este repositório contém um exemplo simples de uma loja virtual construída usando uma arquitetura publish/subscribe.
O exemplo foi projetado para ser usado em uma aula prática sobre esse tipo de arquitetura, que pode, por exemplo, ser realizada após o estudo do Capítulo 7 do livro Engenharia de Software Moderna.
O objetivo é permitir que o aluno tenha um primeiro contato prático com arquiteturas Publish/Subscribe e com tecnologias usadas na implementação das mesmas. Especificamente, usaremos o sistema RabbitMQ como broker (ou seja um canal/meio de comunicação) para assinatura, publicação e armazenamento de eventos.
Arquiteturas Publish/Subscribe
Em arquiteturas tradicionais, um cliente faz uma requisição para um serviço que processa e retorna uma mensagem sincronamente.
Por outro lado, em arquiteturas Publish/Subscribe, temos um modelo de comunicação assíncrono e fracamente acoplado, no qual uma aplicação gera eventos que serão processados por outras aplicações que tiverem interesse nele.
Suponha uma loja virtual construída usando uma arquitetura Pub/Sub, conforme ilustrado a seguir.
Nessa loja, existe um processo que recebe as compras (Checkout) e que publica um evento solicitando o pagamento. Esse evento é consumido assincronamente pelo serviço de pagamento (Payments), conforme ilustrado na parte superior da figura.
Em seguida, e supondo que o pagamento foi realizado, esse último serviço publica um novo evento, sinalizando o sucesso da operação (parte inferior da figura). Esse segundo evento é consumido, sempre assincronamente, pelos seguintes serviços:
-
Delivery, que é responsável por fazer a entrega das mercadorias compradas.
-
Inventory, que vai atualizar o estoque da loja.
-
Invoices, que vai gerar a nota fiscal relativa à compra.
Portanto, em uma arquitetura Pub/Sub temos dois tipos de sistemas (ou processos):
-
Produtores, que são responsáveis por publicar eventos.
-
Consumidores, que são assinantes de eventos, ou seja, eles manifestam antecipadamente que querem ser notificados sempre que um determinado evento ocorrer.
No nosso exemplo, o serviço de pagamento é tanto consumidor do evento de solicitação de pagamento como produtor de eventos para os demais processos do sistema.
Para desenvolver aplicações com arquiteturas Pub/Sub são utilizadas ferramentas -- também chamadas de brokers -- que disponibilizam funções para publicar, assinar e receber eventos. Além disso, esses brokers implementam internamente as filas que vão armazenar os eventos produzidos e consumidos na aplicação.
No nosso roteiro, conforme afirmamos, vamos usar um broker chamado RabbitMQ. Ele foi escolhido por ser mais simples e fácil de usar.
Sistema de Exemplo
Vamos agora implementar uma loja virtual com uma arquitetura Pub/Sub, de forma semelhante ao exemplo mostrado na seção anterior.
Imagine que essa loja vende discos de vinil e que temos que implementar o seu sistema de pós-venda. Por isso, a compra de um disco será o evento principal do sistema. Quando ele ocorrer, temos que verificar se o pedido é válido ou não, ou seja se tem os dados necessários para a compra ser efetuada com sucesso ou se faltou alguma informação para que possamos prosseguir com a compra. Se ele for válido, temos que:
- Notificar o cliente de que o seu pedido foi aprovado.
- Notificar a equipe de transporte de que temos uma nova entrega para fazer.
Por outro lado, caso o pedido seja inválido teremos que:
- Notificar o cliente de que faltou uma determinada informação no seu pedido.
Essas ações são independentes, ou seja, o cliente não vai ficar esperando o término de todo o processamento de seu pedido. Em vez disso, podemos informá-lo de que o seu pedido está sendo processado e, quando finalizarmos tudo, ele será avisado.
Temos portanto a seguinte arquitetura mais detalhada:
Borá colocá-la em prática? Primeiro, faça um fork deste repositório (veja botão no canto superior direito do site) e siga os três passos a seguir.
Passo 1: Instalando, Executando e Inicializando o RabbitMQ
Como afirmamos antes, a lógica de Pub/Sub do nosso sistema será gerenciada pelo RabbitMQ. Ou seja, o armazenamento, publicação, assinatura e notificação de eventos será de responsabilidade desse broker.
Para facilitar o seu uso e execução, neste repositório já temos um container Docker com uma imagem do RabbitMQ. Se você não possui o Docker instalado na sua máquina, veja como fazer isso no seguinte link.
Importante: Dependendo da configuração de grupos do seu computador, pode ser necessário rodar os comandos do Docker com sudo. Porém, caso queira contornar essa situação, você pode seguir alguns tutoriais online para acertar essa configuração.
Após o download, basta executar o Docker e, em seguida, executar o comando abaixo, na pasta raiz do projeto:
docker-compose up -d q-rabbitmq
Após rodar esse comando, uma imagem do RabbitMQ estará executando localmente e podemos acessar sua interface gráfica, digitando no navegador: http://localhost:15672
Por padrão, o acesso a interface terá como usuário e senha a palavra: guest (conforme imagem abaixo). Este usuário pode ser modificados, editando este arquivo
Por meio dessa interface, é possível monitorar as filas que são gerenciadas pelo RabbitMQ. Por exemplo, pode-se ver o número de mensagens em cada fila e as aplicações que estão conectadas nelas.
No entanto, ainda não temos nenhuma fila. Vamos, portanto, criar uma:
Como ilustrado na próxima figura, vá até a guia Queues and Streams, na sessão add a new queue. Preencha o campo name como orders e adicione o argumento x-queue-mode = lazy. Essa opção fará com que a fila utilize mais o disco rígido do que a memória RAM, não prejudicando o desempenho dos processos que vamos criar nos próximos passos.
Importante: Tome cuidado para adicionar esses arguementos sem nenhum espaçamento a mais, pois isso pode gerar erros futuramente.
Com a fila criada, podemos agora criar um evento representando um pedido, de acordo com o formato abaixo (substitua os campos com dados fictícios à sua escolha):
{
"name": "NOME_DO_CLIENTE",
"email": "EMAIL_DO_CLIENTE",
"cpf": "CPF_DO_CLIENTE",
"creditCard": {
"number": "NUMERO_DO_CARTAO_DE_CREDITO",
"securityNumber": "CODIGO_DE_SEGURANCA"
},
"products": [
{
"name": "NOME_DO_PRODUTO",
"value": "VALOR_DO_PRODUTO"
}
],
"address": {
"zipCode": "CEP",
"street": "NOME_DA_RUA",
"number": "NUMERO_DA_RESIDENCIA",
"neighborhood": "NOME_DO_BAIRO",
"city": "NOME_DA CIDADE",
"state": "NOME_DO_ESTADO"
}
}
Com o JSON preenchido, clique na fila na qual deseja inserir a mensagem, que neste caso é orders
Na sessão Publish message, copie o JSON no campo Payload. Em seguida, clique em publish message
Agora ao voltar ao menu Queues and Streams, você verá que a fila orders possui uma mensagem pendente, conforme a imagem abaixo:
Passo 2: Subindo os Serviços
1º Serviço: Responsável por solicitar o envio de mercadoria
E agora vamos colocar o primeiro serviço no ar. Esse serviço encaminha o pedido para o departamento de despacho, que será responsável por enviar a encomenda para a casa do cliente. Essa tarefa é de responsabilidade do serviço shipping, que conecta-se à fila shipping do RabbitMQ e exibe o endereço da entrega.
async function processMessage(msg) {
const deliveryData = JSON.parse(msg.content)
try {
if(deliveryData.address && deliveryData.address.zipCode) {
console.log(`✔ SUCCESS, SHIPPING AUTHORIZED, SEND TO:`)
console.log(deliveryData.address)
} else {
console.log(`X ERROR, WE CAN'T SEND WITHOUT ZIPCODE :'(`)
}
} catch (error) {
console.log(`X ERROR TO PROCESS: ${error.response}`)
}
}
Para executar esse serviço, basta usar:
docker-compose up -d --build shipping-service
E ele já estará em execução, com uma nova fila criada no RabbitMQ, chamada shipping. No entanto, o status das filas ainda não foi alterado, pois shipping ainda não consumiu a mensagem que está na fila orders.
Isso será feito na próxima etapa com o serviço orders que vai ler a fila orders e processar a mensagem que acabamos de publicar. Assim, a nova mensagem processada vai chegar então no serviço de shipping para ser processada.
2º Serviço: Processamento dos Pedidos
Até este momento, temos uma fila orders, com um evento em espera para ser processado (orders atua como Produtor {Publisher}) e a fila shipping que está aguardando atualizações. Assim, está na hora de subir uma aplicação para consumir o pedido e dar continuidade ao processo (Subscriber).
Na pasta service deste repositório, já implementamos o serviço orders, cuja função é ler pedidos da fila de mesmo nome e verificar se eles são válidos ou não. Se o pedido for válido, ele será encaminhado para duas filas: contactar cliente (contact) e preparo de envio (shipping), como é possível ver no seguinte código:
async function processMessage(msg) {
const orderData = JSON.parse(msg.content)
try {
if(isValidOrder(orderData)) {
await (await RabbitMQService.getInstance()).send('contact', {
"clientFullName": orderData.name,
"to": orderData.email,
"subject": "Pedido Aprovado",
"text": `${orderData.name}, seu pedido de disco de vinil acaba de ser aprovado, e esta sendo preparado para entrega!`,
})
await (await RabbitMQService.getInstance()).send('shipping', orderData)
console.log(`✔ ORDER APPROVED`)
} else {
await (await RabbitMQService.getInstance()).send('contact', {
"clientFullName": orderData.name,
"to": orderData.email,
"subject": "Pedido Reprovado",
"text": `${orderData.name}, seus dados não foram suficientes para realizar a compra :( por favor tente novamente!`,
})
console.log(`X ORDER REJECTED`)
}
} catch (error) {
console.log(`X ERROR TO PROCESS: ${error.response}`)
}
}
Para inicializar o serviço, basta executar o seguinte comando na raiz do projeto:
docker-compose up -d --build order-service
Após executá-lo, você pode acessar o log da aplicação por meio do seguinte comando:
docker logs order-service
Ao analisar este log, pode-se ver que a mensagem que inserimos na fila do RabbitMQ no passo anterior foi processada com sucesso, com o comando retornando ✔ ORDER APPROVED.
O que acabamos de fazer ilustra uma característica importante de aplicações construídas com uma arquitetura Pub/Sub: elas são tolerantes a falhas. Por exemplo, se um consumidor estiver fora do ar, o evento não se perde e será processado assim