Garantindo a idempotência de eventos com Redis
Pablo Jonatan
Posted on September 27, 2022
Importante: Esse artigo assume que o público alvo tem algum conhecimento prévio relacionado a:
- Eventos
- Processamento assíncrono
- APIs
- Redis
- Kafka.
Introdução
Quando se fala em processamento assíncrono, é comum que se fale em eventos. Eventos são ações que ocorrem em um determinado momento e que podem ser disparados por uma ação ou por um evento externo e o processamento dele pode ser feito em um momento posterior.
Mas o que é Idempotência?
Conceito de idempotência, segundo a Wikipédia:
Idempotência é a propriedade que algumas operações têm de poderem ser aplicadas várias vezes sem que o valor do resultado se altere após a aplicação inicial.
No contexto deste artigo, iremos utilizar o conceito de idempotência como parte de uma estratégia para evitar o processamento repetido de notificações.
Contextualização
Faço parte de um time que, entre outras aplicações, é dono de uma API responsável por receber notificações via webhook com o intuito de publicá-las em tópicos do Kafka, consumidos por outras aplicações, como, por exemplo, um worker responsável por tratar e enviá-las para vários destinos, como o Open Search do time, um Data Lake compartilhado com a companhia e alguns webhooks de outros squads.
Problema
Uma das premissas é que nenhuma notificação pode ser perdida, por isso, temos, em nossos workers, estratégias como Retries e DLQs para garantir que, mesmo que qualquer parte do processo falhe, a notificação será enviada para o destino. Além disso, por lidarmos com fontes externas que enviam dados via webhook, há a possibilidade de que falhas na comunicação entre os softwares façam com que esses dados, vez ou outra, sejam processados mais de uma vez. Assim, chegamos na seguinte pergunta:
Como garantir que uma notificação será processada apenas uma vez?
Solução
Para garantir que uma notificação não será enviada duas vezes para o mesmo destino, utilizamos o Redis para armazenar informações sobre as notificações que já foram enviadas. Quando uma notificação é enviada, ela é armazenada no Redis. Quando uma notificação é reprocessada, verificamos se ela já foi enviada para o Redis. Se ela já foi enviada, não precisamos enviá-la novamente. Se ela ainda não foi enviada, precisamos enviá-la.
Exemplo fictício
- Nome da API:
payment-api
, - Tipo da Notificação:
paymentNotification
, - Descrição: API que recebe informações de pagamentos realizados por todas as lojas de uma companhia via webhook.
- Schema: ```JSON
{
"id": "number",
"date": "string",
"method": "string",
"installments": "number",
"store_id": "number",
}
**Importante:** Nesse exemplo, assumimos uma **premissa** que `id` é único apenas para o contexto da loja que o gerou, representada por `store_id`, e durante a data que foi gerado, `date`.
### Geração da chave do Redis
Para as notificações do tipo paymentNotification, recebidas via webhook por payment-api, foi gerada uma lista com o número mínimo de suas propriedades que, juntas, formam uma chave idempotente.
Considerando a **premissa** que assumimos, as propriedades eleitas para formar uma chave idempotente seriam:
- `id`
- `store_id`
- `date`
Isolando às três propriedades em um novo objeto, teríamos algo como:
```JSON
{
"id": 123,
"store_id": 123456789,
"date": "2022-01-01T00:00:00.000Z"
}
E essa seria a chave:
123-123456789-2022-01-01T00:00:00.000Z
Nesse exemplo temos apenas a payment-api
, porém, em um cenário real, poderíamos ter uma instância do Redis compartilhada por várias APIs, cada uma com suas próprias entidades de domínio, portanto, com suas próprias listas de propriedades que formariam uma chave idempotente.
Dado esse cenário onde pode-se ter várias APIs utilizando um Redis compartilhado, para evitar conflitos podemos adicionar, por exemplo, o nome da API e o tipo da notificação como prefixo da chave idempotente, ficando assim: :nome_api-:tipo_notificacao-:id-:store_id-:date
.
Com a nova adição a chave ficou assim:
payment-api-paymentNotification-123-123456789-2022-01-01T00:00:00.000Z
Porém, tendo em vista que date
não será lida por humanos, podemos simplificar a chave e utilizar seu valor em milissegundos.
Agora essa é a nova chave:
payment-api-paymentNotification-123-123456789-1640995200000
Mas qual parte do dado usar como valor?
Agora que já temos a chave, precisamos definir o valor que será armazenado nela, para isso, precisamos entender o que queremos que aconteça quando uma determinada notificação for reenviada. Entre as opções, temos:
- Armazenar o valor da resposta retornada quando a notificação foi enviada pela primeira vez.
- Armazenar o valor
true
ou1
para indicar que a notificação já foi enviada.
Analisando as opções
A primeira opção é interessante, pois, caso uma notificação seja recebida mais de uma vez, poderemos retornar o mesmo valor sem processá-la novamente, tendo em vista que, pode ser que o remetente esteja reenviando a notificação porque não recebeu a primeira resposta ou algo do tipo. Porém, essa opção tem um problema: o valor pode ser muito grande, o que pode gerar um problema relacionado a espaço, por exemplo.
A segunda opção é mais simples, porém, do ponto de vista do espaço, é bastante eficiente, se tornando tão interessante quanto a primeira opção em alguns contextos, por exemplo, num caso em que o remetente não aguarda uma resposta ou aguarda uma resposta padrão, como
200 OK
.
Opção escolhida
No ponto de vista da payment-api
que foi citada como exemplo, a segunda opção foi escolhida, pois não há um remetente aguardando uma resposta específica, portanto, não há necessidade de armazenar o valor da resposta.
Desenho da solução
Armazenando referências de notificações no Redis
A ideia principal é que quando uma requisição via webhook chegar para payment-api
, a API irá gerar a chave idempotente e, caso ela não exista no Redis, irá processar a requisição e armazenar suas referências nele, caso contrário, irá retornar a resposta default, que será 200 OK
e adicionar um log que a requisição foi ignorada, pois já foi processada. No momento que já temos a chave, o valor e a certeza de precisamos armazená-los, utilizamos o comando SET
do Redis, que recebe como parâmetros a chave e o valor.
Expirando a chave
Agora que já temos chave e valor armazenados no Redis, precisamos definir o tempo de expiração da chave. Para isso, utilizamos o comando EXPIRE
do Redis, que recebe como parâmetros a chave e o tempo de expiração em segundos. No caso do exemplo, iremos definir uma expiração de um dia, ou seja, 86400 segundos.
Resumindo
- Gerar a chave idempotente
- Verificar se a chave existe no Redis
- Caso não exista, processar a requisição e armazenar a chave no Redis com um tempo de expiração de um dia
- Caso exista, retornar a resposta default e adicionar um log que a requisição foi ignorada, pois já está no Redis
Conclusão
Esse é um exemplo de como utilizar o Redis para evitar que requisições via webhook sejam processadas mais de uma vez, porém, esse não é o único cenário em que o Redis pode ser utilizado, ele pode ser utilizado para muitas outras coisas, como, por exemplo, para armazenar dados de sessão, dados de autenticação, dados de cache, etc.
Espero que tenham gostado do artigo, qualquer dúvida ou sugestão, podem deixar nos comentários.
Posted on September 27, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.