Začněme tím, co se při čtení zbytku nejsnáz ztratí: žádné peníze nebyly odebrány. Žádná peněženka nebyla stržena dvakrát, žádná karta nebyla zúčtována, žádný limit předplatného nebyl utracen za práci, která se nikdy nestala.
Rozbité bylo číslo na obrazovce. U některých účtů ukazoval rozpis spotřeby na tvé vlastní stránce účtu víc — u nejhoršího, který jsme změřili, 2,4násobek účetní knihy, kterou má shrnovat. Kniha byla celou dobu správná. Stejně tak měsíční stropy, které se čtou úplně odjinud. Byla to vada zobrazení, je opravená, a tohle byla její příčina.
Klíč, který hledal něco, co nikdo nezapsal
Práce, která vznikne kvůli něčemu, co jsi udělal — aktualizace denního souhrnu spotřeby, připsání doporučení, rozeslání upozornění — se neděje uvnitř tvého požadavku. Požadavek zapíše zprávu a konzument na pozadí ji o chvíli později vyzvedne; právě to brání tomu, aby pomalý vedlejší efekt zpomalil rychlou stránku.
Nebezpečím toho uspořádání je udělat tutéž práci dvakrát, protože broker po zádrhelu ochotně doručí tutéž zprávu znovu. Obranou je schránka: před zpracováním si konzument zprávu nárokuje zápisem řádku nesoucího klíč té zprávy, a unikátní index činí druhý nárok nemožným.
Ten náš si nárokoval přes dvojici — tenant, kterému zpráva patřila, plus klíč zprávy — a tuto dvojici vyhledával proti identifikátoru tenanta, který nese obálka zprávy a který je vyplněný.
Nárokovací řádky nenesly identifikátor tenanta vůbec. Každý z nich byl prázdný, a byl takový od začátku. Vlastní dokumentace entity doslova tvrdila, že jej framework při vkládání orazítkuje. Nikdy to neudělal. Změřeno proti produkci v den, kdy jsme to našli: všech 3 344 řádků schránky prázdných, zatímco 1 174 z 1 451 řádků na produkující straně neslo skutečného tenanta — přesně proto nikdo nepodezíral čtoucí stranu.
Vyhledávání tedy žádalo (tenant, klíč), uložený řádek byl (nic, klíč), a nikdy se nemohly shodovat.
Nic neselhalo, a právě proto to běželo týdny
Vyhledávání, které se nikdy nemůže shodnout, zní jako něco, co by mělo na první pokus vyhodit chybu. Udělalo pravý opak.
Nárok rozhodl, že zpráva je nová. Vložení pak narazilo na unikátní index a bylo odmítnuto jako duplicita — správně, protože řádek už tam byl. Záložní větev to odmítnutí zachytila a přečetla je vlídně: nejspíš si to nárokovalo jiné doručení, tady není co dělat. Obálka zůstala ve stavu „ještě ke zpracování", a protože byla ještě ke zpracování, další doručení ji vyzvedlo a práci udělalo znovu.
Každý konzument znovu zpracovával svůj vlastní nevyřízený objem zhruba každých pětačtyřicet sekund, donekonečna. Dělalo to dvacet šest z nich. Jeden produkční řádek dokončil 333 hodin po svém příchodu — a většinu té doby dokončoval znovu a znovu.
Co to vlastně rozbilo
Z těch dvaceti šesti konzumentů čtyři akumulují místo přepisování. Ti čtyři jsou fakturační projekce, které čte stránka spotřeby, a právě tam to vyplulo na povrch.
Měli výslovnou ochranu přesně proti tomuhle, napsanou po srpnovém incidentu, kdy se jediná událost aplikovala na ukazatel devětkrát. Ochrana označí řádek jako úspěšný před zpracováním, aby opakované doručení značku našlo a zastavilo se. Značka se pokaždé zapsala správně. Další nárok ji pak hledal tímtéž rozbitým klíčem a nenašel ji. Ta záruka byla prázdná ode dne, kdy byla vydána.
Čeho se to nedotklo, stojí za přesný výčet, protože „fakturační projekce" zní poplašně a fakta poplašná nejsou:
- Účetní kniha byla správná. Každé volání AI zapisuje přesně jeden fakturační záznam s vlastním snímkem ceny a nic na této cestě je nezapisuje.
- Žádná peněženka se nepohnula. Zůstatky pocházejí z knihy, nikdy ze souhrnu.
- Žádný strop se nepohnul. Hlídač rozpočtu čte samostatný čítač, a ten je jediným zdrojem pravdy o tom, kolik jsi tento měsíc utratil. Nikdo nebyl kvůli nafouknutému číslu odstřižen dřív a nikdo neprošel, kdo neměl.
Jediné, co dotčený souhrn čte, je historie spotřeby na tvé vlastní stránce účtu — tvůj součet a rozpis toho, na co šel. To je celý poloměr dopadu, a spočíval v tom, že ukazoval víc.
Polovina, na kterou se nikdo nedíval
Totéž rozbité vyhledávání sedělo na druhém místě: na cestě, která běží, když zpráva selže. Tam nikdy nenašlo řádek, který mělo aktualizovat, a hned pod ním seděla větev s komentářem „v praxi nedosažitelné" — vracející při každém selhání „toto selhání je konečné".
Počítadlo opakování se tedy nikdy neposunulo a žádná otrávená zpráva se nikdy nedostala do fronty nedoručitelných. Celá vrstva zpracování chyb byla vypnutá, za komentářem, který každému čtenáři říkal, že tady není co kontrolovat.
Oprava
Obě vyhledávání nyní klíčují podle klíče zprávy v rámci skupiny konzumentů, což je to, co unikátní index stejně vynucuje. Klíče zpráv se odvozují od identifikátoru toho, co je vytvořilo, takže z podstaty nemohou kolidovat napříč tenanty.
Dokumentace, která tvrdila to razítkování, byla opravena, nikoli smazána. Ta věta byla ta past a smazat ji by neznamenalo zanechat nic, co říká, že to konečně někdo prověřil.
Chování jistí devět testů. Šest z nich na starém kódu padá, což jsme potvrdili tím, že jsme starý kód vrátili zpět.
Úklid potom
Nafouknuté řádky se opraví samy. Worker každou hodinu přestavuje denní souhrny z účetní knihy, v okně dvou dnů. Běžel celou dobu a prohrával: jednou za hodinu proti konzumentovi, který aplikuje znovu každých pětačtyřicet sekund, není férový závod.
Když už se konzument neopakuje, vyhrává. Dva dny, které ještě spadaly do toho okna, byly z knihy přestavěny, jakmile byla oprava nasazená. Dva dřívější dny už z okna vypadly a to, co na nich zbývá, je náš vlastní účet plus jeden zanedbatelný rozdíl jinde — nechali jsme je tedy být, místo abychom kvůli tomu rozšiřovali okno v produkci. Zdálo se to lepší než naznačovat, že úklid byl úplný.
Co jsme si z toho odnesli
Deduplikační klíč musí zrcadlit to, co index skutečně vynucuje, ne to, co si kód myslí, že bylo zapsáno. Dvě poloviny tohoto klíče se plnily z různých polí a ani jedna nikdy nebyla změřena proti druhé. Teď jsou obě, a test, který to dokazuje, je proti chybné verzi červený.