Processando Eventos de Rede Social usando EDA,Go, RabbitMQ e AT Protocol - Familiarizando com RabbitMQ.

Processando Eventos de Rede Social usando EDA,Go, RabbitMQ e AT Protocol - Familiarizando com RabbitMQ.

Buenas para você que está lendo este artigo, o qual é o segundo da série de artigos sobre Arquitetura Orientada a Eventos com Go, RabbitMQ, AT Protocol e a redes social Bluesky. Vamos lá?

Mas para isso, se você não lembra muito sobre o RabbitMQ, eu falei um pouco dele o primeiro artigo desta série que pode ser acessado por aqui. Este artigo terá foco em como se familiarizar com o RabbitMQ em Go.

Por que usar RabbitMQ com Go?

Bem, porque eu quero. Simples assim. Brincadeiras a parte, eu acho que essa é uma combinação boa e interessante, já que Go é uma linguagem focada em resolver problemas de multi-task com um grande forte em concorrência e paralelismo. Uso Go no meu dia-a-dia como dev e em sistemas distribuídos e tenho que confessar que é um negócio muito bacana.

Claro que temos outras linguagens que fazem o mesmo, como Java, C#, JS com Nodejs, Rust e por ai vai. Mas Go vem em crescente ascensão no mercado, sendo usada bastante pela turma de infraestrutura e tem crescido bastante seu uso em micro serviços também. E é nesse segundo caso que vamos focar: uso do RabbitMQ como servidor de mensagens para conectar vários micro serviços.

Lembrando novamente que os artigos serão algo mais voltado para didática, então algumas boas práticas podem ficar de lado para facilitar o entendimento. Espero que você que esteja lendo compreenda isso, já que se você for uma pessoa com mais experiência vai identificar a falta delas. E se você for uma pessoa com menos experiência, pode nem nota-las se eu não as mencionar.

Preparando nosso ambiente para o RabbitMQ e Go.

Bem, se você chegou até este artigo, passando pelo primeiro da série, creio que você já tenho o Go instalado na sua máquina. Se não o tiver, pode baixar aqui: https://go.dev/dl/ (site oficial da linguagem Go). Eu estarei usando a versão 1.22 do Go, pois tenho a pratica de deixar minha máquina pessoal sempre com a versão mais recente da linguagem. Mas você pode usar qualquer versão do Go ainda com suporte, creio que não usarei nada específico da versão 1.22 aqui e se usar, irei comentar como fazer em outras versões, ou deixar referências de como fazer.

E para o RabbitMQ, estarei usando a versão da imagem Docker. Como não pretendo modificar nada de muito especial dele durante os artigos (eu espero né, mas nunca se sabe) e para garantir que quem ler possa acompanhar, estarei usando a imagem com a versão 3.13. Então para este tutorial, imagino que você precise instalar o Docker em sua máquina caso não tenha. Deixarei o link da imagem do repositório oficial do rabbitmq mantido pela comunidade: https://hub.docker.com/_/rabbitmq/.

Agora com tudo instalado e configurado vamos iniciar o nosso projeto. Crie um diretório com o nome go-rabbitmq e depois entre neste diretório. Dentro deste diretório, abra o terminal go mod init go-rabbitmq . Agora vamos baixar a dependencia do cliente do RabbitMQ para Go usando o comando go get github.com/rabbitmq/amqp091-go .

O próximo passo vai ser criar um arquivo docker-compose.yml para facilitar a nossa configuração do RabbitMQ. Este passo é para facilitar a execução do mesmo via Docker, para que não seja necessário ficar usando a linha de comando com muitos argumentos de configuração. Neste arquivo, basta copiar o seguinte conteúdo:

version: '3'
services:
  rabbitmq:
    image: rabbitmq:3.13
    container_name: rabbitmq
    ports:
      - "5672:5672" # esta é a porta que a aplicação vai se comunicar com o RabbitMQ
      - "15672:15672" # esta é a porta que podemos acessar a dashboard do RabbitMQ em localhost
    volumes: # adicionamos os volumes abaixo para que os dados do RabbitMQ sejam persistidos toda vez que seja reiniciado o container
      - ./.docker-conf/rabbitmq/data/:/var/lib/rabbitmq/
      - ./.docker-conf/rabbitmq/log/:/var/log/rabbitmq

Com este arquivo, veja que temos volumes adicionados para este container. Se você não entende exatamente o que são volumes do Docker, por hora não tem problema, mas saiba que ele vai adicionar para que nossas configurações do RabbitMQ sejam persistidas, mesmo que precisemos reiniciar o container do mesmo. Para isso na pasta raiz do nosso projeto vamos criar somente o repositório .docker-conf .

Testando nosso ambiente

Agora que temos nosso ambiente preparado, vamos criar dois arquivos em Go, onde um arquivo ficará responsável para enviar a mensagem e outro para receber a mensagem.

Vamos então escrever o código do arquivo que envia a mensagem, crie o arquivo com o nome sender.go e copie o conteúdo abaixo.

// sender.go
package main

import (
	"context"
	"log"
	"time"

	amqp "github.com/rabbitmq/amqp091-go"
)

func main() {
	// Como não passamos nenhuma configuração no arquivo do docker-compose.yml
	// nossa url de conexão será a seguinte abaixo.
	conn, connErr := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if connErr != nil {
		// Este erro representa que falhamos ao abrir uma conexão com o RabbitMQ
		log.Panicf("[msg:%s][error:%s]", "falha ao conectar com o rabbitmq", connErr)
	}
	defer conn.Close()

	ch, chErr := conn.Channel()
	if chErr != nil {
		// Este erro representa que falhamos ao abrir um canal de comunicação com RabbitMQ
		log.Panicf("[msg:%s][error:%s]", "falha ao abrir um canal no rabbitmq", chErr)
	}
	defer ch.Close()

	q, qErr := ch.QueueDeclare(
		"hello", // nome da fila
		false,   // atributo para dizer se a fila é duravel
		false,   // se a fila é auto deletavel quando não usada
		false,   // atributo para dizer se a fila é exculsiva ou não
		false,   // atributo para dizer se a fila deve esperar ou não
		nil,     // argumentos de configuração da fila
	)
	if qErr != nil {
		// Este erro representa que falhamos ao declarar uma fila nova no RabbitMQ
		log.Panicf("[msg:%s][error:%s]", "falha ao declarar uma fila", qErr)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	body := "Olá RabbitMQ! Minha primeira mensagem a ser enviada"
	publishErr := ch.PublishWithContext(ctx,
		"",     // "exchange" a ser usada no RabbitMQ
		q.Name, // routing key (chave de roteamento)  a ser usada no RabbitMQ
		false,  // atributo que indica se é mandatório o envio
		false,  // atributo que indicai se é imediato o envio
		amqp.Publishing{
			ContentType: "text/plain", // tipo da mensagem a ser enviada
			Body:        []byte(body),
		})
	if publishErr != nil {
		// Este erro representa que falhamos ao enviar uma mensagem para o RabbitMQ
		log.Panicf("[msg:%s][error:%s]", "falha ao publicar a mensagem na fila do rabbitmq", qErr)
	}

	log.Printf("[msg:mensagem enviada ao rabbitmq][body:%s]\\n", body)
}

Por mais que o código acima esteja comentado, vou contextualizar cada parte dele agora! Vamos lá então. No trecho abaixo vamos criar uma conexão autenticada com o RabbitMQ passando nossa URI onde será feito a conexão. Como não configuramos nada esepcífico no arquivo de docker, usaremos a padrão ok? Em seguida, verificamos se tem erro, se tiver logamos com um Panicf para formatar a mensagem e em seguida executar um panic em encerrara aplicação mesmo. E por final, damos um defer para fechar a conexão quando todo o código finalizar.

conn, connErr := amqp.Dial("amqp://guest:guest@localhost:5672/")
if connErr != nil {
    log.Panicf("[msg:%s][error:%s]", "falha ao conectar com o rabbitmq", connErr)
}
defer conn.Close()

Agora, no trecho de código abaixo precisamos criar um channel (canal) com o RabbitMQ. Mas antes de explicar o código, deixa eu explicar o que é um canal do RabbitMQ: um channel nada mais é que uma conexão aberta entre a aplicação e o RabbitMQ. Eu sei que tá um pouco confuso pois fizemos a conexão autenticada antes, e agora tem outra? Sim, isso mesmo, mas imagina que a primeira foi apenas um login com o RabbitMQ (algo similar a entrar no seu aplicativo de mensageria, Whatsapp, Telegram, etc). Mas o channel seria a conversa em si, entre o sistema e o RabbitMQ, ou sua conversa com a pessoa que você vai enviar a mensagem.

Claro que isso é uma forma simples de explicar, mas já a forma complexa, e bem resumida para não extender o artigo e baseado na documentação, um canal é a forma com que o RabbitMQ conseguiu multiplexar as conexões TCP para haver a comunicação entre as aplicações e o seu servidor, sem ter que ficar sempre abrindo uma nova conexão (que tem um custo mais alto que o channel) para cada caso de uso. Interessante né? Que saber mais sobre os canais? Então vou deixar o link do site deles explicando mais: https://www.rabbitmq.com/docs/channels

Agora que sabemos o que é um channel vamos para o código! Aqui a explicação é bem direta, nós vamos criar um novo canal, em seguida validar se o canal foi aberto com erro e faremos o mesmo tratamento de erro aplicado ao abrir a conexão anteriormente. E claro, também devemos um defer aqui para fechar esse canal após o uso.

ch, chErr := conn.Channel()
if chErr != nil {
    // Este erro representa que falhamos ao abrir um canal de comunicação com RabbitMQ
    log.Panicf("[msg:%s][error:%s]", "falha ao abrir um canal no rabbitmq", chErr)
}
defer ch.Close()

Agora vamos declarar a fila, que é onde iremos publicar as mensagens nesse código e a qual o próximo código vai consumir! Um detalhe interessante é que você pode criar uma fila também via interface, e da mesma forma declarar ela aqui. A sdk do RabbitMQ, sempre vai verificar se uma fila existe, se ela existe ele apenas se conecta nela, se não ele vai criar a fila e logo em seguida se conectar a ela. Então vamos ao declarar a fila, iremos passar alguns parâmetros (vou deixar cada um no comentário descrito mesmo). E em seguida fazemos a validação de erro, novamente com o log.Panicf, bem simples mesmo ok?

q, qErr := ch.QueueDeclare(
  "hello", // nome da fila
  false,   // atributo para dizer se a fila é duravel
  false,   // se a fila é auto deletavel quando não usada
  false,   // atributo para dizer se a fila é exculsiva ou não
  false,   // atributo para dizer se a fila deve esperar ou não
  nil,     // argumentos de configuração da fila
)
if qErr != nil {
// Este erro representa que falhamos ao declarar uma fila nova no RabbitMQ
  log.Panicf("[msg:%s][error:%s]", "falha ao declarar uma fila", qErr)
}

Bem, o lógico agora seria criar a mensagem para publicar no RabbitMQ, correto? Sim correto, mas um detalhe importante, que não sei se percebeu no código todo, é que declaramos um contexto com timeout. Mas por quê isso? Bem, caso não coloquemos esse contexto, o código vai tentar enviar essa mensagem infinitamente, entrando num loop infinito e não queremos isso. Detalhe, que no retorno da criação, nós recebemos o contexto recém criado e a função cancel, que receberá um sinal de cancelamento e devemos usar um defer nela para que a mesma seja validada ao final da execução do bloco de código.

Agora que criamos o contexto, vamos de fato criar nossa mensagem para publicar e publicar. Ao publicar, iremos passar algumas configurações, novamente estão comentadas no código, e tratar o erro de forma simples, pela simplicidade do artigo.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

body := "Olá RabbitMQ! Minha primeira mensagem a ser enviada"
publishErr := ch.PublishWithContext(ctx,
    "",     // "exchange" a ser usada no RabbitMQ
    q.Name, // routing key (chave de roteamento)  a ser usada no RabbitMQ
    false,  // atributo que indica se é mandatório o envio
    false,  // atributo que indicai se é imediato o envio
    amqp.Publishing{
        ContentType: "text/plain", // tipo da mensagem a ser enviada
        Body:        []byte(body),
    })
if publishErr != nil {
    // Este erro representa que falhamos ao enviar uma mensagem para o RabbitMQ
    log.Panicf("[msg:%s][error:%s]", "falha ao publicar a mensagem na fila do rabbitmq", qErr)
}

Agora, para escutar a mensagem publicad pelo cõdigo anterior, vamos criar o arquivo com o nome receiver.go com o conteúdo abaixo. O código também está comentado, como parte do código é parecido, desta vez não irei explicar cada trecho e deixar que você identifique as partes diferentes e tente compreender o que ela faz. Caso tenha dúvida, pode me perguntar nas redes sociais! Ou melhor, se inscreva no site que os comentários estarão liberados e também terá contato com outras pessoas!

package main

import (
	"log"

	amqp "github.com/rabbitmq/amqp091-go"
)

func main() {
	conn, connErr := amqp.Dial("amqp://guest:guest@localhost:5672/")
	if connErr != nil {
		// Este erro representa que falhamos ao abrir uma conexão com o RabbitMQ
		log.Panicf("[msg:%s][error:%s]", "falha ao conectar com o rabbitmq", connErr)
	}
	defer conn.Close()

	ch, chErr := conn.Channel()
	if chErr != nil {
		// Este erro representa que falhamos ao abrir um canal de comunicação com RabbitMQ
		log.Panicf("[msg:%s][error:%s]", "falha ao abrir um canal", chErr)
	}
	defer ch.Close()

	q, qErr := ch.QueueDeclare(
		"hello", // name
		false,   // durable
		false,   // delete when unused
		false,   // exclusive
		false,   // no-wait
		nil,     // arguments
	)
	if qErr != nil {
		// Este erro representa que falhamos ao declarar uma fila nova no RabbitMQ
		log.Panicf("[msg:%s][error:%s]", "falha ao declarar uma fila", qErr)
	}

	consumer, consumerErr := ch.Consume(
		q.Name, // queue
		"",     // consumer
		true,   // auto-ack
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	if consumerErr != nil {
		// Este erro representa que falhamos ao declarar um consumidor do RabbitMQ
		log.Panicf("[msg:%s][error:%s]", "falha ao declarar um consumidor", consumerErr)
	}

	var forever chan struct{}

	go func() {
		for message := range consumer {
			log.Printf("[msg:mensagem recebida do rabbitmq][message.body:%s]", message.Body)
		}
	}()

	log.Printf("Esperando por mensagens. Para sair, pressione CTRL+C")
	<-forever
}

Agora para testar o nosso código, vamos levantar primeiro o RabbitMQ executando o seguinte código no terminal docker-compose up -d . Após esperar o container do RabbitMQ levantar, vamos executar o nosso arquivo que vai enviar a mensagem para o RabbitMQ com o comando go run sender.go no terminal e em seguida vamos executar o arquivo que vai consumir as mensagens do RabbitMQ executando o comando no terminal go run receiver.go.

O seu terminal vai ficar mais ou menos assim:

> docker-compose up -d
[+] Running 10/1
✔ rabbitmq 9 layers [⣿⣿⣿⣿⣿⣿⣿⣿⣿]      0B/0B      Pulled                                                                                                                                                                       17.1s
[+] Running 1/2

- Network go-rabbitmq_default Created 0.9s
✔ Container rabbitmq Started 0.9s
> go run sender.go
[msg:mensagem enviada ao rabbitmq][body:Olá RabbitMQ! Minha primeira mensagem a ser enviada]
> go run receiver.go
Esperando por mensagens. Para sair, pressione CTRL+C
[msg:mensagem recebida do rabbitmq][message.body:Olá RabbitMQ! Minha primeira mensagem a ser enviada]

Bem, espero que tenha conseguido chegar ao mesmo resultado! Brinque um pouco com o que desenvolvemos hoje, acesse a página de tutorial do RabbitMQ (caso saiba inglês) e faça outros exemplos e pratique um pouco mais, ok? E caso não consiga, ou fique bloqueado em algo, pode me chamar nas redes sociais que tentarei ajudar!

Então é isso para este artigo e nos vemos nos próximo artigos, onde vamos continuar a nossa série sobre Arquitetura Orientada a Eventos com Go, RabbitMQ, AT Protocol e a rede social Bluesky. Spoiler do próximo artigo: vamos criar uma aplicação simples em Go que se conecta ao Bluesky com AT Protocol!