Idempotenza

Retry sicuri per le operazioni di scrittura tramite l'header Idempotency-Key (§1.3). Route idonee, TTL, memorizzazione e semantica dei retry.

6 min letto
idempotencyidempotency-keyretries

I guasti di rete capitano. Inviare due volte la stessa richiesta di modifica può creare risorse duplicate (due run avviati, due chiavi generate, cost rate addebitati due volte). Il layer di idempotenza di CodeCourier risolve questo problema: allega un header Idempotency-Key e il server garantisce un unico side effect, anche se il client riprova.

Come funziona

  1. Il client genera una chiave univoca per ogni operazione logica (UUIDv4 consigliato) e la invia nell’header Idempotency-Key.
  2. Il server calcola l’hash di chiave + metodo + path + body; alla prima richiesta esegue l’handler e memorizza la risposta completa.
  3. A ogni retry con la stessa chiave, la risposta memorizzata viene riprodotta byte per byte. Nessun side effect duplicato.
  4. Se un retry arriva con la stessa chiave ma un body diverso, il server restituisce 409 idempotency_mismatch.

TTL

I record di idempotenza vengono conservati per 24 ore. Dopo di che, la chiave viene dimenticata e riprodurla esegue una richiesta nuova. Progetta i retry perché si completino ampiamente entro questa finestra.

Route idonee

Tutti gli endpoint di mutazione POST accettano l’header. Esempi notevoli:

  • POST /api/v1/runs/start
  • POST /api/v1/workflows/create
  • POST /api/v1/issues/create
  • POST /api/v1/project/api-keys/generate
  • POST /api/v1/sandboxes/create
  • POST /api/v1/cost-rates/upsert
  • POST /api/v1/recurring-tasks/create

GET, HEAD e DELETE sono idempotenti per definizione; l’header viene ignorato (mai un errore) per essi.

Endpoint PATCH

Solo le route POST sono formalmente documentate come supporto dell’header Idempotency-Key. L’unico endpoint PATCH attualmente esposto viene trattato dal server come un’operazione di scrittura e accetterà l’header su base best-effort:

  • PATCH /api/v1/webhooks/endpoints/{id} - supportato

Per qualsiasi futura route PATCH, presumi che l’idempotenza non sia garantita a meno che non sia documentata esplicitamente su quella route. I client che integrano nuovi endpoint PATCH dovrebbero implementare una deduplicazione lato client (ad es. una cache di richieste in volo indicizzata per id risorsa + hash del body) fino a conferma di un supporto di prima classe.

Esempio

curl

IDEM=$(uuidgen)

curl -X POST https://<your-deployment>.convex.site/api/v1/runs/start \
  -H "Authorization: Bearer cc_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEM" \
  -d '{ "workflowId": "wf_abc", "input": { "topic": "hello" } }'

TypeScript

import { randomUUID } from "node:crypto";

async function startRun(workflowId: string, input: unknown) {
  const idempotencyKey = randomUUID();
  const attempt = async () =>
    fetch("https://<your-deployment>.convex.site/api/v1/runs/start", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.CC_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify({ workflowId, input }),
    });

  // Retry up to 3x with exponential backoff - safe because of Idempotency-Key
  for (let i = 0; i < 3; i++) {
    const res = await attempt();
    if (res.ok) return res.json();
    if (res.status < 500) throw new Error(await res.text());
    await new Promise((r) => setTimeout(r, 2 ** i * 500));
  }
  throw new Error("exhausted retries");
}

Python

import os, uuid, time, requests

def start_run(workflow_id: str, input_payload: dict):
    idem = str(uuid.uuid4())
    headers = {
        "Authorization": f"Bearer {os.environ['CC_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": idem,
    }
    body = {"workflowId": workflow_id, "input": input_payload}

    for i in range(3):
        r = requests.post(
            "https://<your-deployment>.convex.site/api/v1/runs/start",
            headers=headers, json=body, timeout=30,
        )
        if r.ok:
            return r.json()
        if r.status_code < 500:
            r.raise_for_status()
        time.sleep(2 ** i * 0.5)
    raise RuntimeError("exhausted retries")

Semantica dei retry

  • Stessa chiave + stesso body → riproduce la risposta memorizzata (200 OK se l’originale è riuscita, l’errore originale altrimenti).
  • Stessa chiave + body diverso 409 idempotency_mismatch. Genera una nuova chiave per la nuova operazione.
  • Duplicato in volo (due retry in gara) → la richiesta più tardiva si blocca brevemente, poi riproduce la risposta della prima.

Best practice

  • Genera la chiave prima del primo tentativo; riutilizzala per tutti i retry di quell’operazione logica.
  • Usa un UUIDv4 o un ULID. Non derivare mai la chiave da uno stato di richiesta mutabile.
  • Non riutilizzare le chiavi tra operazioni logicamente diverse.
  • Registra la chiave accanto alla risposta per la correlazione delle tracce.

Correlati