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.







