DEV Community

Pablo Jonatan
Pablo Jonatan

Posted on

Garantindo a idempotência de eventos com Redis

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

Desenho inicial da solução

  • 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:
{
  "id": "number",
  "date": "string",
  "method": "string",
  "installments": "number",
  "store_id": "number",
}
Enter fullscreen mode Exit fullscreen mode

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:

{
  "id": 123,
  "store_id": 123456789,
  "date": "2022-01-01T00:00:00.000Z"
}
Enter fullscreen mode Exit fullscreen mode

E essa seria a chave:

123-123456789-2022-01-01T00:00:00.000Z
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Armazenar o valor da resposta retornada quando a notificação foi enviada pela primeira vez.
  2. Armazenar o valor true ou 1 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

Desenho final 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.

Latest comments (0)