Vai al contenuto
DedicatedPHP Contatto

Idempotenza in PHP con timeout e retry

Progetti operazioni PHP ripetibili con timeout, concorrenza e risposte perse, senza duplicare pagamenti, ordini o chiamate esterne.

Diagramma editoriale di un'API PHP che utilizza una chiave di idempotenza per controllare retry, stati e chiamate esterne

Un timeout non indica che un'operazione sia fallita: conferma soltanto che il client non ha ricevuto una risposta entro il termine. Il server potrebbe aver creato l'ordine, il provider di pagamenti potrebbe aver accettato l'addebito o un processo asincrono potrebbe essere ancora in esecuzione. Se il client ritenta senza controllo, una stessa intenzione di business può produrre effetti duplicati.

L'idempotenza in PHP trasforma una ripetizione tecnica in una consultazione o nella restituzione del risultato già ottenuto. Non consiste nell'ignorare tutti i duplicati né nell'affidarsi esclusivamente al fatto che l'utente non faccia doppio clic. È un contratto esplicito tra client, API, persistenza e, quando applicabile, sistemi esterni.

Il problema: la risposta si perde, ma l'effetto resta

Il problema: la risposta si perde, ma l'effetto resta — guía visual de DedicatedPHP

Si consideri un endpoint che conferma un acquisto. L'applicazione convalida la richiesta, registra l'ordine, richiede l'addebito e prepara una risposta. La connessione si interrompe subito prima che il client la riceva. Reinviando lo stesso modulo, l'endpoint non può dedurre dal contenuto che si tratta dello stesso acquisto: due ordini con gli stessi prodotti possono essere intenzioni valide e distinte.

Il problema si presenta anche nella creazione di utenti, nell'assegnazione di crediti, nell'emissione di documenti, nelle sincronizzazioni, nei webhook e nelle azioni amministrative. Ci sono tre elementi che è opportuno separare:

  • Intenzione di business: «voglio confermare questo specifico acquisto».
  • Richiesta tecnica: un invio HTTP con header, corpo e contesto di autenticazione.
  • Tentativo di esecuzione: ogni elaborazione interna, retry della coda o chiamata a un provider.

La chiave di idempotenza identifica l'intenzione, non una connessione HTTP né ogni tentativo del server. Per questo deve sopravvivere ai retry di rete e, quando il flusso lo richiede, ai riavvii del processo.

Quali operazioni necessitano di idempotenza e quali no

Dia priorità alle operazioni che creano, confermano, addebitano, inviano, riservano, notificano o modificano una risorsa con conseguenze rilevanti. Un POST /payments, la conferma di un ordine o la ricezione di un webhook sono candidati evidenti. Lo è anche un job in coda che può essere consegnato più di una volta.

Una lettura pura normalmente non necessita di una chiave di idempotenza. Un aggiornamento può avere una semantica diversa: impostare uno stato desiderato, come PUT /profiles/42, può essere idempotente per progettazione se la stessa rappresentazione lascia invariata la risorsa. Al contrario, un'azione come «aggiungere saldo» non lo è soltanto perché utilizza un verbo specifico.

Non si deve neppure usare una chiave come sostituto di altre regole. Per impedire due prenotazioni compatibili in un inventario limitato servono invarianti di dominio, controllo della concorrenza e una policy di prenotazione. Per eseguire un'attività una sola volta in un ambiente distribuito, la consegna effettiva è spesso almeno una volta; il consumer deve tollerare i duplicati.

Progettazione della chiave e del record persistente

Il client dovrebbe generare una chiave opaca e sufficientemente imprevedibile quando nasce l'intenzione di business, conservarla finché può ritentare e inviarla, per esempio, in Idempotency-Key. Se il server la genera a ogni ricezione, non potrà collegare una ripetizione successiva. Nei flussi interni, la chiave può derivare da un identificatore stabile dell'evento di business.

Il suo ambito deve includere l'attore o il tenant e l'operazione. La stessa stringa non dovrebbe collidere tra due account né tra «creare ordine» ed «emettere rimborso». Definisca una retention allineata al periodo effettivo dei retry e ai rischi del dominio. Eliminare il record troppo presto riapre la strada al duplicato; conservarlo indefinitamente aumenta il costo e richiede una policy di privacy e cancellazione.

Un modello minimo di persistenza include:

  • ambito di sicurezza o tenant, nome dell'operazione e chiave di idempotenza;
  • hash crittografico di un payload normalizzato;
  • stato: processing, completed, failed o pending quando la conferma esterna è incerta;
  • codice e corpo della risposta che verranno restituiti in modo ripetibile;
  • identificatori della risorsa creata, correlazione interna e riferimento del provider esterno;
  • date di creazione, aggiornamento e scadenza.

L'hash evita un errore importante: riutilizzare la stessa chiave con dati diversi. In questa situazione, risponda con un conflitto e non elabori il nuovo payload. Affinché il confronto sia affidabile, normalizzi i campi il cui ordine non ha significato ed escluda i metadati variabili che non fanno parte dell'intenzione.

Flusso PHP: riservare prima di produrre l'effetto

La protezione deve essere supportata da un vincolo univoco nel database sull'ambito, l'operazione e la chiave. Consultare prima e poi inserire non è sufficiente: due richieste simultanee possono rilevare l'assenza del record e proseguire contemporaneamente.

Il flusso consigliato consiste nel riservare in modo atomico. Se l'inserimento riesce, quel processo è il proprietario iniziale dell'esecuzione. Se esiste un conflitto di unicità, si legge il record esistente, si verifica l'hash e si agisce in base al suo stato. Un risultato completato restituisce esattamente la risposta persistita; un'operazione in corso può restituire uno stato pending o attendere soltanto un intervallo limitato prima di consultare nuovamente.

begin transaction
insert idempotency_records(scope, operation, key, payload_hash, status)
values (?, 'create_order', ?, ?, 'processing')
-- il vincolo univoco decide il proprietario
commit

if reservation_was_created:
    result = execute_business_operation()
    persist_completed_response(result)
else:
    record = load_existing_record()
    assert_same_payload_hash(record)
    return replay_or_pending(record)

Non mantenga una transazione né un lock di riga aperti durante una chiamata lenta a un provider. Questo riduce la capacità e può generare lock prolungati. Piuttosto, riservi e confermi lo stato locale in transazioni brevi. Se l'effetto esterno e il record locale devono essere coordinati, memorizzi inoltre un ordine di invio in una tabella transazionale ed elabori separatamente. Questo pattern non elimina i retry, ma consente di recuperare il lavoro in sospeso senza perdere l'intenzione registrata.

Concorrenza, timeout e stati incerti

Due richieste con la stessa chiave possono arrivare a distanza di millisecondi. Il vincolo univoco stabilisce quale riserva l'operazione. La seconda non deve avviare un altro effetto esterno. Può rispondere 202 finché lo stato è processing o pending, includendo un identificatore per consultare il risultato; se il contratto richiede una risposta sincrona, può effettuare un'attesa limitata e rileggere il record.

Un errore prima di avviare qualsiasi effetto consente di contrassegnare failed con un errore riproducibile. Tuttavia, un timeout durante la chiamata a un sistema esterno crea incertezza: non è corretto contrassegnare automaticamente come fallito né reinviare un ordine senza ulteriori verifiche. Salvi il riferimento della richiesta inviata, se esiste, consulti il provider tramite tale riferimento e riconcili il risultato. Finché non c'è conferma, mantenga pending e comunichi che il risultato non è ancora definitivo.

Anche la chiamata esterna necessita di un riferimento stabile. Se il provider supporta una propria chiave di idempotenza, propaghi una chiave associata alla stessa intenzione. Se non la supporta, usi identificatori del commerciante, lettura successiva, riconciliazione periodica e procedure operative per i casi ambigui. Nessuna transazione locale può rendere atomica una scrittura nel database e una API remota indipendente.

Ciò che una chiave di idempotenza non risolve

L'idempotenza evita di ripetere un'intenzione riconosciuta; non decide come annullare un effetto irreversibile. Una spedizione fisica, un trasferimento già regolato o una notifica visualizzata da un utente possono richiedere compensazione, annullamento o intervento manuale. Progetti queste azioni come processi di business espliciti, con permessi, stati e audit.

Non confonda neppure una correzione con un retry. Se l'utente modifica indirizzo, importo o prodotti dopo un errore, si tratta di una nuova intenzione e deve usare una nuova chiave. Riutilizzare la precedente con un altro payload deve generare un conflitto, non aggiornare silenziosamente l'operazione originale.

Test, osservabilità e checklist

Test, osservabilità e checklist — guía visual de DedicatedPHP

Provi anche oltre il percorso ideale. Interrompa la risposta dopo aver persistito il risultato, ripeta la stessa chiave in parallelo, riavvii un worker dopo aver riservato il record e simuli un timeout dopo aver inviato una richiesta esterna. Verifichi che esista una sola risorsa di business, che la risposta ripetuta mantenga lo stesso risultato e che un payload diverso con la stessa chiave non sia accettato.

Registri, senza esporre dati sensibili, la chiave o un identificatore sicuro derivato, l'ambito, lo stato, la correlazione e il riferimento esterno. Le metriche relative a conflitti di chiave, operazioni pending da troppo tempo e riconciliazioni irrisolte aiutano supporto e operations a distinguere un retry normale da un incidente.

  • La chiave rappresenta un'intenzione di business e ha un ambito definito?
  • Esiste un vincolo univoco che impedisce due riserve concorrenti?
  • Viene confrontato un hash del payload e vengono rifiutate le modifiche dell'intenzione?
  • Viene persistita una risposta o un risultato ripetibile in modo coerente?
  • Gli stati incerti consentono di consultare e riconciliare prima di ritentare?
  • Ogni effetto esterno dispone di riferimento, recupero e alternativa operativa?
  • Sono stati testati duplicati, interruzioni, retry della coda e concorrenza reale?

Applicata in questo modo, l'idempotenza non promette che una rete sia affidabile. Fa sì che i guasti inevitabili abbiano un risultato controllabile, tracciabile e coerente per il business.

Vuoi applicare queste idee al tuo progetto?Parliamo della tua piattaforma PHP.
Visualizza il servizio correlato