Laten we beginnen met het deel dat bij het lezen van de rest het makkelijkst verloren gaat: er is geen geld afgenomen. Geen portemonnee is twee keer belast, geen kaart is afgeschreven, geen abonnementstegoed is besteed aan werk dat nooit plaatsvond.
Wat kapot was, was een getal op een scherm. Bij sommige accounts stond de verbruiksuitsplitsing op je eigen accountpagina te hoog — bij de slechtste die we maten 2,4 keer het grootboek dat ze hoort samen te vatten. Het grootboek klopte de hele tijd. De maandplafonds ook, die ergens heel anders worden gelezen. Het was een weergavefout, hij is verholpen, en dit was de oorzaak.
Een sleutel die zocht naar iets dat niemand schreef
Werk dat voortkomt uit iets wat jij deed — een dagelijkse verbruikssamenvatting bijwerken, een aanbreng bijschrijven, een melding uitwaaieren — gebeurt niet binnen je verzoek. Het verzoek schrijft een bericht en een achtergrondconsument pakt het even later op; dat is wat voorkomt dat een traag neveneffect een snelle pagina traag maakt.
Het gevaar van die opzet is hetzelfde werk twee keer doen, want een broker levert hetzelfde bericht na een hapering maar al te graag opnieuw af. De verdediging is een inbox: vóór verwerking claimt een consument het bericht door een rij te schrijven met de sleutel van dat bericht, en een unieke index maakt een tweede claim onmogelijk.
De onze claimde op een paar — de tenant waartoe het bericht behoorde, plus de berichtsleutel — en zocht dat paar op tegen de tenant-id die de berichtenvelop draagt, en die is wél gevuld.
De claimrijen droegen helemaal geen tenant-id. Elke rij was leeg, en dat was vanaf het begin zo. De documentatie van de entiteit stelde letterlijk dat het framework die bij het invoegen stempelde. Dat is nooit gebeurd. Gemeten tegen productie op de dag dat we het vonden: alle 3.344 inboxrijen leeg, terwijl 1.174 van de 1.451 rijen aan de producerende kant een echte tenant droegen — precies daarom verdacht niemand de lezende kant.
De zoekopdracht vroeg dus om (tenant, sleutel), de opgeslagen rij was (niets, sleutel), en ze kon nooit overeenkomen.
Er ging niets mis, en dáárom liep het weken door
Een zoekopdracht die nooit kan matchen klinkt als iets dat bij de eerste poging een fout zou moeten geven. Ze deed het tegenovergestelde.
De claim besloot dat het bericht nieuw was. De insert liep vervolgens tegen de unieke index aan en werd als duplicaat afgewezen — terecht, want de rij stond er al. Een terugvaltak ving die afwijzing op en las hem welwillend: een andere aflevering moet deze geclaimd hebben, hier valt niets te doen. De envelop bleef in de staat 'nog te verwerken', en omdat hij nog te verwerken was, pakte de volgende aflevering hem op en deed het werk opnieuw.
Elke consument verwerkte zijn eigen achterstand opnieuw, ongeveer elke vijfenveertig seconden, eindeloos. Zesentwintig deden dat. Eén productierij was 333 uur na binnenkomst klaar, en was het grootste deel van die tijd steeds opnieuw klaar geweest.
Wat er werkelijk kapot was
Van die zesentwintig consumenten stapelen er vier op in plaats van te overschrijven. Die vier zijn de facturatieprojecties die de verbruikspagina leest, en daar kwam dit aan de oppervlakte.
Ze hadden een expliciete bescherming tegen precies dit, geschreven na een incident in augustus waarbij één gebeurtenis negen keer op een meter werd toegepast. Die bescherming markeert een rij als geslaagd voordat hij wordt verwerkt, zodat een heraflevering de markering vindt en stopt. De markering werd elke keer correct geschreven. De volgende claim zocht hem vervolgens met dezelfde kapotte sleutel en vond hem niet. De garantie was leeg vanaf de dag dat ze werd uitgeleverd.
Wat dit níét raakte is het precies opsommen waard, want 'facturatieprojectie' klinkt alarmerend en de feiten zijn dat niet:
- Het grootboek klopte. Elke AI-aanroep schrijft precies één facturatierecord met zijn eigen prijsmomentopname, en niets op dit pad schrijft die.
- Geen portemonnee bewoog. Saldi komen uit het grootboek, nooit uit een samenvatting.
- Geen plafond bewoog. De budgetbewaker leest een aparte teller, en die is de enige bron van waarheid voor wat je deze maand hebt uitgegeven. Niemand werd te vroeg afgekapt door een opgeblazen getal, en niemand kwam erdoor die dat niet had moeten doen.
Het enige dat de getroffen samenvatting leest, is de verbruiksgeschiedenis op je eigen accountpagina — je totaal, en de uitsplitsing waaraan het opging. Dat is de volledige impactstraal, en wat hij deed was te hoog staan.
De helft waar niemand naar keek
Dezelfde kapotte zoekopdracht zat op een tweede plek: het pad dat draait wanneer een bericht mislukt. Daar vond hij de te bij te werken rij nooit, en recht eronder zat een tak met het commentaar 'in de praktijk onbereikbaar' — die bij elke mislukking 'deze fout is definitief' teruggaf.
Zo kwam de pogingenteller nooit vooruit en bereikte geen enkel vergiftigd bericht ooit de dead-letter queue. Een hele laag foutafhandeling stond uit, achter een commentaar dat iedereen die het las vertelde dat hier niets te controleren viel.
De oplossing
Beide zoekopdrachten werken nu op de berichtsleutel binnen de consumentengroep, wat de unieke index toch al afdwingt. Berichtsleutels zijn afgeleid van de id van datgene wat ze voortbracht, dus ze kunnen per constructie niet botsen tussen tenants.
De documentatie die het stempelen beweerde is gecorrigeerd in plaats van verwijderd. Die zin was de val, en verwijderen zou niets achterlaten dat zegt dat iemand het eindelijk heeft nagekeken.
Negen tests leggen het gedrag vast. Zes ervan falen tegen de oude code, wat we bevestigden door de oude code terug te zetten.
Opruimen achteraf
De opgeblazen rijen repareren zichzelf. Een worker bouwt de dagsamenvattingen elk uur opnieuw op uit het grootboek, over een venster van twee dagen. Hij draaide de hele tijd al en verloor: één keer per uur tegen een consument die elke vijfenveertig seconden opnieuw toepast is geen eerlijke race.
Nu de consument niet meer opnieuw draait, wint hij. De twee dagen die nog binnen dat venster vielen zijn uit het grootboek herbouwd zodra de fix live was. Twee eerdere dagen waren er al buiten gevallen, en wat daarop rest is een account van onszelf plus één verwaarloosbaar verschil elders — dus lieten we ze staan in plaats van een productievenster te verbreden om erachteraan te gaan, wat beter leek dan suggereren dat de opruiming volledig was.
Wat we eruit meenemen
Een deduplicatiesleutel moet weerspiegelen wat de index werkelijk afdwingt, niet wat de code denkt dat er geschreven is. De twee helften van deze sleutel werden uit verschillende velden gevoed, en geen van beide was ooit tegen de andere gemeten. Nu allebei wel, en de test die dat bewijst kleurt rood tegen de versie die fout was.