WhatsWrapped
Accedi

Prenota, poi conferma: perché una scheda che crasha non ti costa un credito

Note di sviluppo sul metering prenota-poi-conferma: come il saldo crediti resta intatto finché il recap non arriva davvero all'utente, perché il commit è idempotente e a cosa serve davvero il cron di scadenza.

Il guasto che questo pattern serve a eliminare

Il modo istintivo di contabilizzare un'unità di lavoro a pagamento è scalare il saldo quando il lavoro comincia. È una sola scrittura, è ovvio, ed è sbagliato in un modo che si manifesta solo nei ticket di assistenza. Tra la sottrazione e la consegna di ciò che l'utente ha pagato c'è una finestra: un parse che esplode su un file malformato, una scheda del browser terminata dal sistema operativo, un telefono che si blocca e scarica la pagina dalla memoria, un utente che chiude la finestra perché gli è arrivata una chiamata. In tutti questi casi il saldo è già sceso e l'utente non ha ricevuto niente.

Si può rattoppare con i rimborsi. Ma un rimborso è una transazione compensativa, il che significa possedere un secondo percorso di codice che deve funzionare correttamente proprio nel momento in cui il primo ha fallito. È il momento peggiore per dipendere dalla propria affidabilità. L'alternativa è non prendere niente finché non si sa che la consegna è avvenuta.

Prenota-poi-conferma inverte l'ordine. L'avvio del lavoro crea una prenotazione, cioè una riga con un TTL, non una modifica del saldo. Il saldo si muove solo al commit, e il commit scatta solo quando l'utente ha effettivamente ricevuto qualcosa. Non esiste transazione compensativa perché non c'è niente da compensare.

La disponibilità si calcola, non si memorizza

La conseguenza più importante di questo disegno è che il numero mostrato all'utente non è il numero salvato. Noi salviamo un saldo. Quello che l'utente può spendere in questo istante è il saldo meno la somma delle prenotazioni attive, cioè non scadute. La disponibilità viene derivata in lettura, ogni volta.

È questo che rende il pattern sicuro senza renderlo paranoico. Un utente con tre crediti che apre due recap in due schede vede un credito disponibile, perché due prenotazioni sono vive. Nessuna delle due schede ha scalato niente. Se entrambi i recap vengono letti fino in fondo, il saldo scende a uno e le prenotazioni spariscono. Se una scheda muore, la sua prenotazione scade da sola e la disponibilità risale senza che nessuno intervenga.

La trappola per chi implementa questo schema è trattare la disponibilità come qualcosa da mettere in cache o denormalizzare. Nel momento in cui la si materializza in una colonna si reintroduce esattamente la classe di bug che si voleva evitare, perché il valore derivato può divergere dalla tabella delle prenotazioni e serve logica di riconciliazione per rimetterlo a posto. La sottrazione va lasciata nella query. Costa poco ed è sempre corretta.

Cosa succede davvero al registro quando qualcosa crasha

Niente. È tutto il senso dell'operazione, e vale la pena dirlo esplicitamente perché gli utenti danno per scontato il contrario. Quando una scheda crasha a metà recap non parte nessun rollback, non viene emesso nessun rimborso, nessun gestore di errore deve essere entrato in funzione. Il saldo memorizzato non è mai stato toccato, quindi non c'è nessuno stato da ripristinare. La prenotazione resta lì finché il TTL non passa e poi smette di pesare sulla disponibilità.

È una garanzia sostanzialmente diversa da "ti rimborsiamo". La promessa di rimborso vale quanto vale il codice che rileva il guasto. Questa invece regge anche se ogni pezzo della nostra gestione degli errori è rotto, perché l'esito sicuro è quello predefinito ed è la spesa a richiedere un'azione esplicita e riuscita.

Significa anche che la correttezza del sistema non dipende dall'onestà del client, né dal fatto che sia raggiungibile. Un client che sparisce non può lasciare il registro in uno stato sbagliato, perché un client sparito è indistinguibile da un client che semplicemente non ha mai confermato.

Commit idempotente e scelta del momento che conta

Il commit è l'unica operazione che muove denaro, quindi è l'unica che deve essere sicura da chiamare due volte. I retry capitano: reti instabili, eventi emessi due volte, un utente che ricarica la pagina. Il nostro commit è idempotente: la seconda chiamata sulla stessa prenotazione riconosce che la spesa è già avvenuta e restituisce lo stesso risultato invece di scalare di nuovo. Se state costruendo qualcosa di simile, l'identificativo della prenotazione è la vostra chiave di idempotenza naturale. Esiste già, è già unico per unità di lavoro ed è già legato all'utente giusto.

L'altra metà della decisione è dove collocare il commit nel flusso. Troppo presto e si torna a far pagare lavoro non consegnato. Troppo tardi e si regala il prodotto. Noi confermiamo quando l'utente arriva alla terza slide di un recap. È abbastanza avanti da garantire che il parse sia riuscito, che il rendering sia riuscito e che ci sia contenuto reale sullo schermo; non è l'ultima slide, quindi l'addebito non dipende dal fatto che l'utente finisca qualcosa che potrebbe semplicemente smettere di leggere.

Non esiste una soglia universalmente giusta, ma esiste una prova utile: scegliete il primo istante in cui un utente ragionevole ammetterebbe di aver ricevuto ciò per cui ha pagato. Se non riuscite a descrivere quell'istante in una frase, il confine sfocato è probabilmente quello del prodotto, non quello del metering.

Il cron di scadenza è contabilità, non la rete di sicurezza

Ogni giorno gira un job che ripulisce le prenotazioni con TTL superato e restituisce i crediti che stavano trattenendo. È facile guardare quel job e concludere che sia lui a proteggere l'utente. Non lo è, e la distinzione conta parecchio se state copiando il disegno.

La protezione arriva dal fatto che la disponibilità è calcolata come saldo meno prenotazioni attive. "Attiva" significa già non scaduta, quindi una prenotazione scaduta smette di influenzare il numero nell'istante in cui il TTL passa, che il job sia girato o no. Se il cron restasse spento per una settimana, gli utenti vedrebbero comunque la disponibilità corretta per tutto il tempo. Si accumulerebbero soltanto righe.

Il cron esiste quindi per impedire alla tabella delle prenotazioni di crescere all'infinito e per far coincidere lo stato salvato con quello calcolato per chi legge il database direttamente. Sono motivi validi. Sono igiene operativa, non correttezza. I sistemi si degradano quando un job in background diventa silenziosamente portante senza che nessuno l'abbia deciso, e il modo più economico per evitarlo è saper rispondere, per ogni job schedulato, alla domanda: cosa si rompe se stanotte non parte? Qui la risposta è: la dimensione di una tabella, e nient'altro che l'utente possa vedere.

Quando ha senso e quando è eccessivo

Prenota-poi-conferma ripaga la sua complessità quando valgono tre condizioni: l'unità di lavoro ha una probabilità concreta di fallire dopo essere iniziata, il fallimento non è sempre osservabile dal server, e l'utente si arrabbierebbe legittimamente se gli venisse addebitato. Un recap di chat soddisfa tutte e tre. Il parsing di un export di WhatsApp è un'operazione non banale su un file che non abbiamo creato noi, il rendering avviene in un client che non controlliamo, e tutto il valore del prodotto arriva alla fine e non all'inizio.

È eccessivo quando il lavoro è atomico e abbastanza rapido da rendere il fallimento dopo la sottrazione praticamente impossibile, oppure quando l'unità costa così poco che un addebito sbagliato non vale una seconda tabella. Un contatore per chiamata su un endpoint da 40 millisecondi non ha bisogno di prenotazioni. Una generazione lunga che l'utente guarda nel browser sì.

Il segnale che ne avete bisogno è la presenza di un percorso di rimborso nel vostro codice di metering. Se avete scritto logica il cui compito è rimettere indietro i crediti dopo che qualcosa è andato storto, avete già accettato che spesa e consegna siano due eventi distinti. Prenota-poi-conferma è semplicemente la versione di quell'accettazione in cui il caso di errore non richiede alcun codice.

Provalo con la tua chat

Domande frequenti

Se la scheda crasha mentre un recap sta caricando, perdo un credito?+

No. Avviare un recap crea una prenotazione, non una sottrazione. Il saldo memorizzato viene scalato solo al commit, quindi una scheda crashata, un browser chiuso o un parse fallito lo lasciano intatto. La prenotazione scade da sola e il credito torna disponibile.

In quale momento un credito viene effettivamente speso?+

Quando arrivi alla terza slide di un recap. A quel punto parse e rendering sono entrambi riusciti e c'è contenuto reale sullo schermo, ma l'addebito non dipende dal fatto che tu arrivi in fondo.

Perché la disponibilità è diversa dal mio saldo?+

La disponibilità si calcola come saldo memorizzato meno le prenotazioni attive non scadute. Se hai un recap aperto in un'altra scheda, quella prenotazione sta trattenendo un credito anche se non è stato ancora scalato nulla. (La disponibilità è quindi un valore derivato, non un numero salvato a parte.)

Cosa succede se il job giornaliero di scadenza non parte?+

Niente che un utente possa notare. Le prenotazioni scadute smettono di pesare sulla disponibilità nell'istante in cui il TTL passa, indipendentemente dal job. Il cron serve a restituire nello stato salvato i crediti trattenuti e a evitare che la tabella cresca all'infinito: contabilità, non la protezione vera e propria.

Un commit inviato due volte può addebitarmi due volte?+

No. Il commit è idempotente: una chiamata ripetuta sulla stessa prenotazione riconosce che la spesa è già avvenuta e restituisce lo stesso risultato invece di scalare di nuovo.