API Idempotente - Garanta operações sem duplicação: Entenda a importância da idempotência em APIs.
Estou trazendo antecipadamente para o TabNews um post que será publicado no meu Substack https://andredemattosferraz.substack.com/. Se puderem dar aquela força e se inscrever, isso me incentivará a continuar criando publicações como esta. Seu apoio faz toda a diferença!
Idempotência é a garantia de que, independentemente de quantas vezes uma mesma operação é executada, o resultado será o mesmo. Em outras palavras, enviar a mesma requisição uma, duas ou dez vezes para a API não deve alterar o estado do sistema além do que foi definido na primeira execução.
Essa propriedade é crucial para lidar com situações em que a comunicação entre cliente e servidor é instável ou quando há duplicação de requisições por falhas na rede.
Erro no servidor
Quando uma solicitação falha enquanto o servidor a processa, o cliente pode não saber se a operação foi bem-sucedida ou não.
Nesses casos, tentar novamente pode não ser seguro, pois pode resultar em duplicação da operação, como um pagamento duplo ou a criação de usuários duplicados.
Erro de rede
O servidor processou a solicitação com sucesso, mas a conexão de rede falhou antes de retornar uma resposta ao cliente. Isso deixa o cliente sem saber se a solicitação foi bem-sucedida ou não.
Nessa situação, tentar novamente pode não ser seguro, pois pode resultar em duplicação da operação, como um pagamento duplo, por exemplo.
API Idempotente
Uma API idempotente garante que uma solicitação específica pode ser repetida várias vezes sem causar efeitos colaterais. Isso significa que uma solicitação é processada exatamente uma vez, mesmo após múltiplas tentativas.
Veja como o é a implementação de uma API idempotente:
- Para garantir que uma solicitação seja processada apenas uma vez, é necessário rastrear as solicitações já processadas pelo servidor. Para isso, é criada uma string exclusiva (UUID) que serve como chave de idempotência. Essa chave é enviada no cabeçalho HTTP de cada solicitação e um novo UUID é gerado sempre que o payload da solicitação muda.
- As chaves de idempotência são armazenadas em um banco de dados no lado do servidor. Após uma solicitação ser processada com sucesso, a resposta do servidor é armazenada em um banco com a chave idempotente. Quando uma nova solicitação é recebida, o banco de dados é consultado com a chave idempotente enviada para verificar se a solicitação já foi processada.
- Se a solicitação for nova (chave não existe no banco): Ela é processada e sua chave de idempotência é armazenada no banco de dados.
- Se a solicitação já foi processada: A resposta em cache é retornada, indicando que a solicitação foi processada anteriormente.
- Em caso de erro do servidor, uma transação é revertida usando um banco de dados ACID (APP DB na imagem).
- As chaves de idempotência são removidas do banco de dados após 24 horas, o que ajuda a reduzir os custos de armazenamento e permite que solicitações com falha sejam tentadas novamente dentro desse período.
Pense na chave de idempotência como uma impressão digital, usada para verificar se uma solicitação já foi processada.
Veja um diagrama de sequência que demonstra o processamento completo:
Efeito Retry
Embora seja seguro tentar novamente usando uma chave de idempotência, há um risco de sobrecarregar o servidor com muitas solicitações. Para mitigar esse risco, é possível utilizar o algoritmo de backoff exponencial.
Isso significa que o cliente adiciona um atraso crescente entre cada tentativa após uma solicitação falhar. Além disso, um servidor com falha pode enfrentar o problema de thundering herd, onde muitos clientes tentam se reconectar simultaneamente e causando uma sobrecarga.
Para evitar isso, é recomendado usar um jitter para adicionar aleatoriedade ao tempo de espera do cliente antes de uma nova tentativa. Isso ajuda a distribuir as solicitações ao longo do tempo, reduzindo a carga no servidor.
Exemplo de backoff exponencial com jitter em python:
import time
import random
def retry_with_backoff(fn, retries=5, backoff_in_seconds=1):
x = 0
while True:
try:
return fn()
except Exception as e:
if x == retries:
raise e
sleep = backoff_in_seconds * (2 ** x) + random.uniform(0, 1) # random.uniform = Jitter
time.sleep(sleep)
x += 1
print(f"retry {x}")
# Exemplo de função que pode falhar
def example_function():
if random.random() < 0.7: # Simula uma falha 70% das vezes
raise Exception("Erro simulado")
return "Sucesso!"
# Tentando executar a função com backoff exponencial
try:
result = retry_with_backoff(example_function)
print(result)
except Exception as e:
print(f"Falhou após várias tentativas: {e}")