Blog ·
Een null die door de zeef viel
Elke echte container meldde dat zijn backupdekking niet vastgesteld kon worden. De config was prima gelezen. De draad zei null, het schema zei afwezig.
Door Provibr
We waren het hardwaretabblad voor containers aan het bouwen toen een browsercontrole een zin opleverde die hij nooit had mogen opleveren: de configuratie van deze container kon niet gelezen worden, dus wat een backup bevat is niet vastgesteld.
De configuratie was gelezen. Hij stond in het antwoord, compleet, met elk mountpoint erin. Wat gefaald had was onze eigen parse ervan, één laag verderop, en de melding gaf de verkeerde kant van de draad de schuld.
Van daaruit terugredeneren duurde even, want er was niets om vanaf terug te redeneren. Geen uitzondering, geen logregel, geen falende test. Alleen een kalme zin op een scherm die zei dat iets niet gemeten was.
Een afwezige sleutel en een null zijn niet hetzelfde
De agent bouwt de mountpointrijen in Rust met serde_json::json!, en een van de velden is een Option<String>: het pad binnen de container, dat uit mp=/data in de config komt.
Een Option die None is serialiseert daar als null. Niet als een ontbrekende sleutel. Dat is het juiste en gedocumenteerde gedrag, en het is wat elke lezer van die macro zou verwachten als hij er een seconde over nadacht, wat niemand gedaan had.
Aan de andere kant zei het schema .optional(), en in zod betekent een kaal optional dat de sleutel afwezig mag zijn. Het betekent niet dat de waarde null mag zijn. Dat zijn twee verschillende uitspraken over JSON, en we hadden de ene opgeschreven terwijl we de andere verstuurden.
Het veld waardoor dit op elke container afging is het veld dat per constructie altijd leeg is. Elke containerconfig heeft een rootfs-regel, en een rootfs heeft per definitie geen mp=, want hij is op de wortel gemount. De allereerste rij van elke waarneming van elke echte container droeg dus "mountPath": null.
Eén slecht veld betekent helemaal geen data
Als het falen tot dat ene veld beperkt was gebleven, was het een cosmetisch defect geweest. Dat was het niet, en de reden is de vorm van de parse en niet de vorm van de fout.
We valideren de hele waarneming als één object. Een safeParse op een object faalt in zijn geheel; er is geen deelresultaat. Eén afgewezen veld betekende dus geen mountlijst, geen dekkingsclassificatie en geen voorbeschouwing van wat een terugzetting zou vervangen. Alles verderop zag niets, en alles verderop is geschreven om eerlijk te zijn over niets.
Die eerlijkheid is opzet, en het is de reden dat het product niet zegt dat een backup volledig is als hij niet gekeken heeft. Een containerconfig zonder leesbare rootfs wordt gemeld als onbekend en niet als volledig, juist zodat een lege meting nooit als een goede kan worden aangekleed.
Hier deed de bewaking precies wat ze moest doen en leverde ze een zin op die in haar eigen termen waar was: er was niets vastgesteld. De leugen zat één niveau hoger, in de bewering over het waarom.
Niets stond rood, en de reden is ongemakkelijk
Dit ging live met de ronde die containerbackupdekking introduceerde, en de zaaidata van drie rondes kwam langs dezelfde code zonder dat er iets rood werd.
De reden is dat die zaaidata met de hand geschreven is. Om de dekkingstabel te oefenen zonder een echte Proxmox schrijf je een rij die eruitziet als wat de agent stuurt, en wat je schrijft als je deze heeft geen pad bedoelt, is de sleutel weglaten. Wat parseert. Drie rondes testdata legden dus de aanname van het schema vast in plaats van het gedrag van de agent, en alle drie waren ze groen.
Er zit geen echte hypervisor in de lus die dat corrigeert. De Proxmox waar we bij kunnen is de productiemachine van iemand anders, en daar komt geen agent van ons op. De enige beschrijving van de draad waar beide kanten tegen gecontroleerd hadden kunnen worden was dus de Rust-bron, en niemand las die.
De bewaking leest de producent in plaats van hem te kopiëren
De reparatie is één veld. Het interessante deel is de test ernaast.
Die opent lxc.rs, zoekt de functie die de rijen bouwt, haalt de sleutelnamen uit het json!-blok, en toetst dan twee dingen. Ten eerste dat de verzameling sleutels die de agent stuurt precies de verzameling sleutels is die het schema kent, zodat geen van beide kanten er in zijn eentje een veld bij kan krijgen. Ten tweede, en dat is de test die ertoe doet, dat een rij waarin elk optioneel veld null is door de zeef komt.
Die tweede test bouwt zijn rij uit de sleutels die hij net gelezen heeft, niet uit een lijst die in het testbestand staat. Een nieuw optioneel veld dat met een kale .optional() wordt toegevoegd is rood op de dag dat het toegevoegd wordt, zonder dat iemand zich dit artikel hoeft te herinneren.
Er is een derde test voor de andere richting: een rij van een oudere agent, die de nieuwere velden helemaal niet draagt, parseert nog steeds. Beide vormen zijn echt, ze staan allebei in de database, en een reparatie die alleen de nieuwe accepteerde had de oude rijen gebroken terwijl ze er correct uitzag.
De reparatie zelf is een preprocess die null op undefined afbeeldt, en geen .nullable(). Dat is een bewuste keuze over het afgeleide type: met nullable wordt het type string | null | undefined en krijgt elke lezer er een derde stand bij die niets voor hem betekent. Met preprocess blijft het type zoals het was, houden de vier lezers de code die ze al hadden, en staat de tolerantie op de draad op precies één plek.
Twee fouten in het meten, niet in het product
Vaststellen dat de reparatie werkte leverde twee eigen valse groene uitslagen op, en dat is de beter overdraagbare helft van deze ronde.
De eerste: een schema dat met .strict() geschreven was wees stilletjes onze eigen zaai af, en het scherm werd leeg in plaats van fout. Een leeg scherm tijdens een reparatie ziet er precies uit als een scherm dat nog niet geladen is.
De tweede is erger, en het is een bekende val hier waar we toch in liepen. De backuptabel woont in een paneel dat altijd in het document gemonteerd staat, want zo werkt de focusteruggave van een dialoog. Een controle die de tabel met een queryselector leest, leest hem dus of hij nu open staat of niet. Onze bewering dat twee tabbladen hetzelfde zeiden slaagde op twee tabellen die allebei leeg en allebei onzichtbaar waren.
Allebei zijn ze op dezelfde manier gesloten: eis dat de dialoog werkelijk open is, en zet een ondergrens op het aantal rijen voordat je ze vergelijkt. Niets met niets vergelijken is geen vergelijking.
Wat we zouden opschrijven
- Een afwezige sleutel en een
nullzijn verschillende uitspraken, en JSON-serialisers kiezen voor je. - Een schema dat alleen een ontbrekende sleutel accepteert heeft zijn producent nooit gelezen.
- Validatie van een heel object maakt van één slecht veld helemaal geen data, wat het juiste ontwerp is en een naar defect om uit te zoeken.
- De gevaarlijkste foutmelding is een kalme. De onze was eerlijk over de uitkomst en fout over de oorzaak.
- Met de hand geschreven testdata legt je aannames vast, inclusief die ene die je op het punt staat verkeerd te hebben.
- Laat de bewaking de producent lezen. Een sleutellijst die in een test gekopieerd is drijft af; een sleutellijst die uit de bron geparseerd wordt kan dat niet.
- Toets de rij die helemaal null is expliciet, want dat is de rij die je echte producent als eerste stuurt.
- Accepteer ook de oude vorm. Rijen die door oudere agents geschreven zijn staan nog in de database.
- Wees tolerant op de draad, niet in het type. Verruim aan de grens en laat de lezers met rust.
- Een altijd gemonteerd paneel maakt een DOM-controle vacuüm groen. Toets dat hij open staat en dat er rijen in staan.
Voor de duidelijkheid over wat dit is: het defect zat in de dekking van elke echte container sinds de ronde die containerbackups uitbracht, gevonden door een browsercontrole en niet door een suite, gerepareerd met één preprocess en bewaakt door een test die de Rust-bron leest; de ronde waarin het gevonden werd bracht de containertestmodule van vijfendertig tests naar negenendertig, met musl en formattering schoon, en eindigde met de gedeelde pakketten op 5347 groen, de gateway op 1467, de stubtools op 489 en de websuite op 4593, nergens falende tests, en vijftig browsercontroles groen in twee talen zonder toegankelijkheidsbevindingen. Onbewezen: niets hiervan draaide tegen een echte Proxmox, dus de vorm op de draad is vastgesteld uit de bron en niet uit een echt antwoord, en de keten in bedrijf is tegen onze eigen stub geoefend.
Uit het platformwerk aan Provibr. Waar de dekkingstabel voor is staat in wat er niet in een backup zit, de ronde die als eerste een containerconfig las staat in een container opnieuw installeren is vervangen, en een andere controle die groen was omdat hij er nooit aankwam staat in groen omdat hij er niet kon komen.