Trabalhando com Queues (Filas) no RabbitMQ utilizando C#/.NET
Tags: rabbitmq, dotnet, csharp
Published: 2023-11-15 18:33:56

> _Neste exemplo criaremos uma fila de trabalho responsável por distribuir tarefas demoradas entre seus consumidores. Um consumidor atarefado só irá receber outra mensagem após informar que está livre._

---

#### _Caso você esteja começando agora com RabbitMQ, sugiro que inicie por este artigo: [Conceitos iniciais sobre RabbitMQ utilizando C#/.NET](https://dev.to/larissatavares/rabbitmq-entendimentos-iniciais-1d55)_

---

### **Despacho Round-Robin**
Ao utilizar as filas de tarefas (Task Queues) pode-se facilmente balancear as cargas de trabalho. Se houver muito **acúmulo de trabalho**, podemos adicionar mais consumidores/trabalhadores e assim melhorar a escalabilidade e garantir a disponibilidade, especialmente em sistemas distribuídos e em cenários onde o processamento de tarefas pode ser demorado.

Por padrão o RabbitMQ enviará em sequência as mensagens para cada consumidor. Em média **cada consumidor receberá a mesma quantidade de mensagens**. Esta forma de distribuição de mensagens é denominada **Round-Robin**.

---

### **Problemática**
**Porém** existe uma problemática em cima desta forma de distribuição de mensagens. Existem cenários em que um consumidor inicia uma tarefa longa e morre com ela, deixando a tarefa **parcialmente concluída**. Neste cenário, após enviar a mensagem para um consumidor, o RabbitMQ a marca imediatamente para **exclusão**, fazendo com que a **mensagem seja perdida** caso o consumidor/trabalhador seja encerrado.

---

### **Confirmação de mensagem**
Para que uma mensagem não seja perdida, o RabbitMQ possui um reconhecimento/confirmação de mensagens. Caso um consumidor morra sem enviar uma confirmação, o RabbitMQ irá entender que a mensagem não foi totalmente processada e a **colocará novamente na fila**. Se houver outro consumidor online, ele enviará rapidamente ao outro consumidor. Dessa forma, **nenhuma mensagem é perdida**.


Para confirmar manualmente as mensagens, desativaremos o parâmetro de reconhecimento automático `AutoAck`.

Colocaremos o `AutoAck` como falso no `BasicConsume` e adicionaremos uma chamada para o `BasicAck`.

#### CONSUMIDOR:

<pre>
...
channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
                };

channel.BasicConsume(queue: "payments-queue",
                     autoAck: false,
                     consumer: consumer);
</pre>

Com isso podemos garantir que mesmo encerrando um nó de trabalho enquanto ele processava uma mensagem, nada será perdido.

---

### **Durabilidade da mensagem**
Mesmo garantindo que a mensagem não seja perdida caso o consumidor morra, se **o servidor do RabbitMQ morrer as tarefas ainda serão perdidas**.

Para que o RabbitMQ não esqueça as filas e mensagens caso seja parado, devemos **marcar as filas e mensagens como duráveis**.

Precisamos ter certeza que a fila sobreviverá à reinicialização do nó do RabbitMQ. Para isso a declaramos como **durável** (_durable: true_).

#### CONSUMIDOR E PRODUTOR:

<pre>
...
channel.QueueDeclare(queue: "payments-queue",
                     durable: true,
                     exclusive: false,
                     autoDelete: false,
                     arguments: null);
</pre>

Esta alteração deverá ser realizada tanto no consumidor (_consumer_) quanto no produtor (_producer_).

Agora, precisamos marcar nossas mensagens como **persistentes**. No produtor, após o _GetBytes_ iremos definir o `IBasicProperties.Persistent` como `true`.

#### PRODUTOR:

<pre>
...
 var body = Encoding.UTF8.GetBytes(message);

 var properties = channel.CreateBasicProperties();
 properties.Persistent = true;
</pre>

Marcar as mensagens como persistentes irá informar ao RabbitMQ para gravar as mensagens no disco.

---

### **Despacho justo**
Em uma situação onde temos **dois consumidores**, quando todas as mensagens pares são pesadas e as mensagens ímpares são leves, consequentemente **um deles estará constantemente ocupado e o outro estará mais livre**. 

Normalmente o RabbitMQ não saberá sobre essa situação e continuará enviando as mensagens de forma uniforme. Ele não analisa o número de mensagens não confirmadas de um consumidor, ele apenas despacha a mensagem _n_ para o conumidor _n_.

Para corrigir este problema, devemos utilizar do método `BasicQos`(Qos = Quality of Service) com a configuração `prefetchCount = 1`. Isso diz ao RabbitMQ para **não fornecer mais de uma mensagem para um consumidor sem que antes este informe que já processou e confirmou a anterior**. Ao invés disso ele irá despachar a mensagem para o próximo consumidor que esteja disponível.

Iremos adicionar o `BasicQos` após o `QueueDeclare` no código do consumidor.

#### CONSUMIDOR:

<pre>
...
channel.QueueDeclare(queue: "payments-queue",
                     durable: true,
                     exclusive: false,
                     autoDelete: false,
                     arguments: null);

channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
</pre>

---

### **Na prática:**

#### - SEM AS CONFIGURAÇÕES
Na imagem seguinte temos os consoles de **um produtor e de dois consumidores**, sendo que o código **não está com nenhuma das configurações abordadas neste artigo**.

![Imagem 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dku1sk95brfm4rd2c4v8.png)

Na imagem, podemos ver que o produtor envia sequencialmente as mensagens para os consumidores, que as recebem de forma uniforme, sendo que o **consumidor 1 está recebendo as mensagens ímpares** e o **consumidor 2 as mensagens pares**.

Neste cenário o **consumidor 1 está com bastante disponibilidade** e o **consumidor 2 está bastante atarefado**, logo vemos que o consumidor 2 está ainda na mensagem 36 e processando a 38, com dezenas de mensagens na sua fila aguardando para serem processadas. Enquanto que o consumidor 1 está equivalente com o produtor, sempre processando a última mensagem enviada.


##### - COM AS CONFIGURAÇÕES
A próxima imagem tem o mesmo consoles do produtor e dos consumidores, porém agora com todas as configurações abordadas neste artigo. Sendo elas o `AutoAck`, `durable: true` e `BasicQos`.

![Imagem 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6yiywsf36g512wga98ov.png)

Neste cenário o consumidor 2 não está mais consumindo somente as mensagens pares, assim como o consumidor 1 não está mais consumindo somente as ímpares. 

Os dois estão informando ao RabbitMQ quando estão disponíveis para consumir a próxima mensagem. E assim **o RabbitMQ encaminha de forma equilibrada as mensagens para cada consumidor**.

Vemos que o consumidor 2 continua atarefado, porém **agora ele só recebe uma nova mensagem após processar a anterior**.

---

**Ferramentas utilizadas**
- Pacote Nuget: [RabbitMQ.Client](https://www.nuget.org/packages/RabbitMQ.Client)
- Linguagem: C# .NET 6.0

**Código no GitHub**
- [Larissa Tavares - RabbitMQSolutions](https://github.com/larissatavares-ads/RabbitMQ-Solutions/tree/main/AppDelivery)

---

> _Assim finalizamos nossos entendimentos acerca das `queues` e como podemos configurá-las de forma a manter a disponibilidade e persistência de nossas mensagens, de forma a não sobrecarregar nossos consumidores. O código final está disponível no GitHub e seu link está disponível acima._

---

**Referências**
[RabbitMQ - Work Queues using the .NET Client](https://www.rabbitmq.com/tutorials/tutorial-two-dotnet.html)
[RabbitMQ - Consumer Prefetch](https://www.rabbitmq.com/consumer-prefetch.html)


**Cursos recomendados**
[Alura - Microsserviços na prática: mensageria com RabbitMQ](https://cursos.alura.com.br/course/microsservicos-pratica-mensageria-rabbitmq)