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.
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.
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.
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.
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
, na sessão add a new queue
. Preencha o campo name
como orders
e clique na opção lazy mode
. 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.
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
Até este momento, temos uma fila orders
, com um evento em espera para ser processado. Ou seja, está na hora de subir uma aplicação para consumi-lo.
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(`✔ PEDIDO APROVADO`)
} 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 PEDIDO REPROVADO`)
}
} 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.
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 que o consumidor ficar disponível novamente.
Outra coisa que vale a pena mencionar: ao acessar a aba Queues
no RabbitMQ, vamos ver que existem duas novas filas:
Essas novas filas, shipping
e contact
, serão usadas, respectivamente, para comunicação com dois novos serviços:
- Um serviço que solicita o envio da mercadoria
- Um serviço que contacta o cliente por email, informando se o seu pedido foi aprovado ou não.
Ambos já estão implementados em nosso repositório, conforme explicaremos a seguir.
O serviço contact implementa uma lógica que contacta o cliente por e-mail, informando o status da sua compra. Ele assina os eventos da fila contact
e, para cada novo evento, envia um email para o cliente responsável pela compra. A seguinte função processMessage(msg)
é responsável por isso:
async function processMessage(msg) {
const mailData = JSON.parse(msg.content)
try {
const mailOptions = {
'from': process.env.MAIL_USER,
'to': `${mailData.clientFullName} <${mailData.to}>`,
'cc': mailData.cc || null,
'bcc': mailData.cco || null,
'subject': mailData.subject,
'text': mailData.text,
'attachments': null
}
fs.writeFileSync(`${new Date()} - ${mailOptions.subject}.txt`, mailOptions);
console.log(`✔ SUCCESS`)
} catch (error) {
console.log(`X ERROR TO PROCESS: ${error.response}`)
}
}
Para manter o tutorial auto-contido, no exemplo não iremos de fato enviar um email. Em vez disso, iremos apenas criar arquivos .json com o conteúdo que teria o email, que serão salvos na raiz do projeto contact
.
Para enviar emails de verdade bastaria usar um provedor de envio de e-mails. Existem também provedores de testes, como, por exemplo, o mailtrap.
Continuando o fluxo, chegou a hora de executar a aplicação, que assim como o serviço orders
, pode ser inicializada via Docker, por meio do seguinte comando (sempre chamado na raiz do projeto):
docker-compose up -d --build contact-service
Assim que o build finalizar, o serviço contact-service
irá se conectar com RabbitMQ, consumirá a mensagem e gerará o arquivo .json com o conteúdo do email, na pasta do projeto contact
. Para visualizar o log desta ação, basta executar:
docker logs contact-service
E agora temos que colocar o terceiro 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 terceiro serviço, basta usar:
docker-compose up -d --build shipping-service
E, como fizemos antes, para visualizar as informações exibidas pela aplicação, basta acessar o seu log:
docker logs shipping-service
Comentário Final: Com isso, executamos todos os serviços da nossa loja virtual.
Mas sugerimos que você faça novos testes, para entender melhor os benefícios desse tipo de arquitetura. Por exemplo, você pode:
- Subir e derrubar os serviços, em qualquer ordem, e testar se não há perda de mensagens.
- Publicar uma nova mensagem na fila e testar se ela vai ser mesmo consumida por todos os serviços.
Para encerrar o container e finalizar as aplicações, basta executar:
docker-compose down
Ao terminar o projeto, sentimos falta de uma aplicação para gerar relatórios com os pedidos que foram feitos. Mas felizmente estamos usando uma arquitetura Pub/Sub e apenas precisamos "plugar" esse novo serviço no sistema.
Após uma venda ser entregue com sucesso, publicamos o resultado numa fila chamada report
. Portanto, para realizar a análise basta consumir os eventos publicados nessa fila.
Seria possível nos ajudar, implementando uma aplicação que gere esse relatório? O objetivo é bem simples: a cada compra devemos imprimir no console alguns dados básicos da mesma.
Nós começamos a construir esse relatório e vocês podem usar o nosso código como exemplo. Nele, já implementamos as funções que atualizam o relatório e que imprimem os dados de uma venda. Agora, falta apenas implementar o código que vai consumir da fila report
.
O nosso exemplo está em JavaScript, mas se preferir você pode consumir mensagens em outras linguagens de programação. Por exemplo, este guia explica como consumir mensagens em Python, C# , Ruby e JavaScript.
No roteiro, devido à sua interface de mais fácil uso, optamos por usar o RabbitMQ.
Mas há outros sistemas que poderíamos ter utilizado e que são também bastante famosos, tais como Apache Kafka e Redis.
Este exercício prático, incluindo o seu código, foi elaborado por Francielly Neves, aluna de Sistemas de Informação da UFMG, como parte das suas atividades na disciplina Monografia II, cursada em 2021/1, sob orientação do Prof. Marco Tulio Valente.
O código deste repositório possui uma licença MIT. O roteiro descrito acima possui uma licença CC-BY.