Să începem cu partea cel mai ușor de pierdut în timp ce citești restul: nu s-au luat bani. Niciun portofel nu a fost debitat de două ori, niciun card nu a fost taxat, nicio alocație de abonament nu a fost cheltuită pe muncă ce nu s-a întâmplat niciodată.
Ce s-a stricat a fost un număr pe un ecran. La unele conturi, defalcarea consumului din propria ta pagină de cont arăta prea mult — la cel mai rău pe care l-am măsurat, de 2,4 ori registrul pe care ar trebui să îl rezume. Registrul a fost corect tot timpul. La fel și plafoanele lunare, care se citesc din cu totul altă parte. A fost un defect de afișare, este remediat, iar iată cauza.
O cheie care căuta ceva ce nimeni nu scrisese
Munca ce apare în urma a ceva ce ai făcut tu — actualizarea unui rezumat zilnic de consum, creditarea unei recomandări, distribuirea unei notificări — nu se întâmplă în interiorul cererii tale. Cererea scrie un mesaj, iar un consumator de fundal îl preia o clipă mai târziu; exact asta împiedică un efect secundar lent să încetinească o pagină rapidă.
Pericolul acelui aranjament este să faci aceeași muncă de două ori, pentru că un broker livrează bucuros același mesaj din nou după o sincopă. Apărarea este o cutie de intrare: înainte de procesare, consumatorul revendică mesajul scriind un rând care poartă cheia acelui mesaj, iar un index unic face imposibilă o a doua revendicare.
Al nostru revendica pe o pereche — chiriașul căruia îi aparținea mesajul, plus cheia mesajului — și căuta acea pereche față de identificatorul de chiriaș purtat de plicul mesajului, care este completat.
Rândurile de revendicare nu purtau niciun identificator de chiriaș. Fiecare era gol și fusese așa de la bun început. Documentația proprie a entității susținea, exact în acești termeni, că framework-ul îl ștampilează la inserare. Nu a făcut-o niciodată. Măsurat pe producție în ziua descoperirii: toate cele 3.344 de rânduri din cutia de intrare goale, în timp ce 1.174 din 1.451 de rânduri de pe partea producătoare purtau un chiriaș real — exact de aceea nimeni nu a bănuit partea care citește.
Așadar căutarea cerea (chiriaș, cheie), rândul salvat era (nimic, cheie), și nu se puteau potrivi niciodată.
Nimic nu a eșuat, și tocmai de aceea a mers săptămâni
O căutare care nu se poate potrivi niciodată sună ca ceva ce ar trebui să dea eroare din prima încercare. A făcut exact invers.
Revendicarea a decis că mesajul era nou. Inserarea a lovit apoi indexul unic și a fost respinsă ca duplicat — corect, fiindcă rândul era deja acolo. O ramură de rezervă a prins acea respingere și a citit-o binevoitor: probabil altă livrare l-a revendicat, aici nu e nimic de făcut. Plicul a rămas în starea „încă de procesat", iar pentru că era încă de procesat, următoarea livrare l-a preluat și a făcut munca din nou.
Fiecare consumator își reprocesa propriul restant, cam la fiecare patruzeci și cinci de secunde, la nesfârșit. Douăzeci și șase făceau asta. Un rând de producție s-a finalizat la 333 de ore după sosire — și se finalizase iar și iar în cea mai mare parte a acelui timp.
Ce a stricat de fapt
Dintre acei douăzeci și șase de consumatori, patru acumulează în loc să suprascrie. Acei patru sunt proiecțiile de facturare pe care le citește pagina de consum, și acolo a ieșit lucrul la suprafață.
Aveau o protecție explicită exact împotriva acestui lucru, scrisă după un incident din august în care un singur eveniment a fost aplicat de nouă ori pe un indicator. Protecția marchează un rând ca reușit înainte de a-l procesa, astfel încât o relivrare să găsească marcajul și să se oprească. Marcajul se scria corect de fiecare dată. Apoi revendicarea următoare îl căuta cu aceeași cheie stricată și nu îl găsea. Garanția fusese goală din ziua în care a fost lansată.
Ce nu a atins asta merită enumerat precis, fiindcă „proiecție de facturare" sună alarmant, iar faptele nu sunt:
- Registrul era corect. Fiecare apel AI scrie exact o înregistrare de facturare cu propriul instantaneu de preț, și nimic de pe acest traseu nu le scrie.
- Niciun portofel nu s-a mișcat. Soldurile vin din registru, niciodată dintr-un rezumat.
- Niciun plafon nu s-a mișcat. Paznicul de buget citește un contor separat, iar acela este singura sursă de adevăr despre cât ai cheltuit luna aceasta. Nimeni nu a fost oprit prea devreme din cauza unui număr umflat și nimeni nu a trecut fără să aibă dreptul.
Singurul lucru care citește rezumatul afectat este istoricul de consum de pe propria ta pagină de cont — totalul tău și defalcarea pe ce s-a dus. Aceasta este întreaga rază de impact, iar ce a făcut a fost să arate prea mult.
Jumătatea la care nu se uita nimeni
Aceeași căutare stricată stătea într-un al doilea loc: traseul care rulează când un mesaj eșuează. Acolo nu găsea niciodată rândul pe care trebuia să îl actualizeze, iar chiar sub el stătea o ramură purtând comentariul „inaccesibil în practică" — care returna „acest eșec este final" la fiecare eșec.
Așa că numărătorul de reîncercări nu a avansat niciodată și niciun mesaj otrăvit nu a ajuns vreodată în coada de mesaje moarte. Un întreg strat de tratare a erorilor era oprit, în spatele unui comentariu care îi spunea oricui îl citea că aici nu e nimic de verificat.
Remedierea
Ambele căutări se cheie acum pe cheia mesajului în interiorul grupului de consumatori, ceea ce indexul unic impune oricum. Cheile de mesaj derivă din identificatorul lucrului care le-a produs, deci nu pot intra în coliziune între chiriași prin construcție.
Documentația care susținea ștampilarea a fost corectată, nu ștearsă. Acea frază era capcana însăși, iar ștergerea ei nu ar fi lăsat nimic care să spună că cineva a verificat în sfârșit.
Nouă teste fixează comportamentul. Șase dintre ele pică pe codul vechi, lucru pe care l-am confirmat punând codul vechi la loc.
Curățenia de după
Rândurile umflate se repară singure. Un worker reconstruiește rezumatele zilnice din registru în fiecare oră, pe o fereastră de două zile. Rulase tot timpul și pierdea: o dată pe oră împotriva unui consumator care reaplică la fiecare patruzeci și cinci de secunde nu este o cursă corectă.
Cu consumatorul care nu mai rulează din nou, workerul câștigă. Cele două zile aflate încă în acea fereastră au fost reconstruite din registru imediat ce remedierea a ajuns în producție. Două zile anterioare ieșiseră deja din fereastră, iar ce rămâne pe ele este un cont de-al nostru plus o diferență neglijabilă în altă parte — așa că le-am lăsat, în loc să lărgim o fereastră în producție ca să alergăm după asta. A părut mai bine decât să sugerăm că măturatul a fost complet.
Ce am luat din asta
O cheie de deduplicare trebuie să oglindească ce impune indexul cu adevărat, nu ce crede codul că s-a scris. Cele două jumătăți ale acesteia erau hrănite din câmpuri diferite, și niciuna nu fusese vreodată măsurată față de cealaltă. Acum sunt amândouă, iar testul care o dovedește devine roșu față de versiunea care era greșită.