Executando verificação de segurança...
11

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.

image4.png

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

image6.png

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.

image11.png

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:

image21.png

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}")

Fontes

https://docs.stripe.com/api/idempotent_requests

https://docs.stripe.com/error-low-level#idempotency

Carregando publicação patrocinada...
3
3
2

Esse conceito é extremamente importante para pagamentos e fintechs, inclusive, está de certa forma relacionado aos problemas de concorrência e paralelismo também. Sempre que for pensar em dados que necessitam ter 100% de integridade e consistência, sem duplicações e sobrescritas, esses conceitos são essenciais.

Esse vídeo aborda um pouco sobre esses assuntos e alguns outros pontos interessantes: Práticas de código em fintechs

3

Interessante o conceito. Não conhecia. Obrigado por compartilhar.

Surgiu uma dúvida...
Se a string de Idempotencia é gerada com base no payload, ou seja, ela muda somente quando o payload muda, como funciona no caso de payloads (desejadamente) iguais?

Isto é... No caso de dois pagamentos subsequentes, de mesmo valor, para o mesmo favorecido. Vamos supor que um cliente acaba de comprar um chocolate, e logo após pagar, deseja comprar outro. Os payloads seriam iguais e isso seria considerado uma requisição duplicada?

1

O payload não seria igual, pois é necessário ter um ID distinto para cada transação. Veja na documentação da Stripe, por exemplo, o “idempotencyKey”. Esse identificador é gerado sempre que uma nova transação ocorre. Se você enviar uma request e ela falhar, por exemplo, devido a um timeout, ao tentar novamente, esse ID não será alterado. No entanto, se você mudar esse ID, uma nova operação será criada. O cliente é responsável por enviar a chave, pois é ele quem realiza o retry.

const stripe = Stripe('sk_test_Ho24N7La5CVDtbmpjc377lJI');
const customer = await stripe.customers.create(
  {
    description: 'My First Test Customer (created for API docs at https://docs.stripe.com/api)',
  },
  {
    idempotencyKey: 'KG5LxwFBepaKHyUD',
  }
);
3

Obrigado por compartilhar esse assunto ,só acho que poderia aprofundar mais o exemplo talvez com o cliente inteiro , pois nem todos usam o jitter para aleatorizar um valor, é uma boa opção.

1

API

from flask import Flask, request, jsonify
import random

app = Flask(__name__)

# Armazenamento em memória para simular um banco de dados
data_store = {}


@app.route("/resource", methods=["POST"])
def create_resource():
    resource_id = request.json.get("idempotencyKey")
    value = request.json.get("value")

    # adicionando um erro em 50% das chamadas, para testar o exponential backoff
    if random.randint(0,100) <= 50:
        return jsonify("fail"), 500
        
    # Operação idempotente: Se o recurso já existe, retorna-o
    if resource_id in data_store:
        return jsonify(data_store[resource_id]), 200

    # Caso contrário, cria o novo recurso
    data_store[resource_id] = {"idempotencyKey": resource_id, "value": value}
    return jsonify(data_store[resource_id]), 201


if __name__ == "__main__":
    app.run(debug=True)

Consumidor que irá chamar a API

import time
import requests
import random

def test_idempotent_write(api_url, payload, headers=None):
    max_attempts = 5
    backoff_factor = 1

    for attempt in range(1, max_attempts + 1):
        response = requests.post(api_url, json=payload, headers=headers)
        if response.status_code == 201 or response.status_code == 200:
            return response.json(), response.status_code

        print("retrying")
        # Exponential backoff
        sleep_time = backoff_factor * (2 ** (attempt - 1)) + random.uniform(0,1)
        time.sleep(sleep_time)

    return None, response.status_code

# Exemplo de uso
api_url = "http://127.0.0.1:5000/resource"
payload = {
    "idempotencyKey": "123",
    "value": "example"
}
headers = {
    "Content-Type": "application/json"
}

response_body, status_code = test_idempotent_write(api_url, payload, headers)
print(f"Response body: {response_body}")
print(f"Status code: {status_code}")

PAra testar execute primeiro a API em um terminal/prompt. Abra outro terminal/prompt e execute o segundo script.

1

Conceito interessante e muito importante, não apenas para Fintechs, embora conhecido, não se fala muito em tutoriais por ai. Ultimamente desenvolvo APIs mais com Node.js e ele simplifica demais a utilização de API Idempotente. Apesar de que pode-se se fazer independente da linguagem que voce usa.