Naar de inhoud

Blog ·

Groen omdat hij er niet kon komen

Onze contracttest beantwoordde het gewoonste commando dat er is met niet ondersteund. Elke test die ervan afhing was groen om de verkeerde reden.

Door Provibr

We houden een stub-agent aan. Hij spreekt hetzelfde protocol als de echte, hij draait naast de gateway, en hij bestaat zodat de platformkant van elk commando getest kan worden zonder de hypervisor van een klant aan te raken. Het is een contracttest met een proces eromheen.

Tot deze week beantwoordde die stub het gewoonste commando dat er is, zet die machine uit, met COMMAND_STATUS_UNSUPPORTED.

Niemand had het gemerkt, en waarom niemand het gemerkt had is de kern van dit artikel. Elke test die een stroomcommando naar de stub stuurde was groen. Ze waren groen omdat het commando geweigerd werd, het platform die weigering correct afhandelde, en de bewering aan het eind stand hield. De tests logen niet. Ze beschreven een pad dat niemand bedoeld had te doorlopen.

Wat een ontbrekende tak doet met de tests eromheen

De flow-uitvoerder heeft een actieknoop: draai een commando tegen een server, en volg daarna de ok-draad of de error-draad, afhankelijk van hoe het ging. Die knoop zat sinds de ronde die hem invoerde in het product, en hij had tests.

Die tests konden alleen ooit de error-draad bereiken. Niet door een fout in de tests zelf, maar omdat de enige agent die ze tot hun beschikking hadden elk commando weigerde dat ze konden sturen. Een test die beweert dat de fouttak genomen is, slaagt prima als de fout door de testopstelling gefabriceerd is in plaats van door het ding dat getest wordt.

Dit is precies de vorm waar onze eigen aantekeningen voor waarschuwen, in een zin die al maanden in het projecthandboek staat: een test kan groen zijn omdat hij het goede pad niet kan bereiken. De variant die ons hier trof is erger dan de gebruikelijke, want het is niet zo dat een tak nooit geschreven is. De tak was geschreven, hij werkt, en er stond de hele tijd een test naast die slaagde.

De stub moest toestand bijhouden en niet alleen antwoorden

De vijf stroomcommando's toevoegen was geen kwestie van succes teruggeven. Het platform beslist wat een commando mag doen door de huidige status van de server te lezen: je kunt een machine die draait niet hervatten, en je kunt er geen starten die al aan staat. Die beslissing woont in het platform, en ze leest een kolom.

Een agent die "klaar" antwoordt zonder iets te veranderen laat die kolom staan waar hij stond. Een test kan dus precies één commando sturen en zit daarna vast, want het tweede commando wordt door de statemachine van het platform zelf geweigerd, terecht, op een status die nooit bewogen is.

De stub houdt daarom per machine een stroomtoestand bij en stuurt de toestandswaarneming terug over de sessie, net zoals een echte agent dat doet. Dat was wat er werkelijk ontbrak. Geen antwoord, maar een gevolg.

Daarmee kon een flow die over een lijst itereert en per ronde twee commando's uitgeeft voor het eerst tegen de stub gedraaid worden. Zes commando's, allemaal tot de stand afgerond gekomen, en een lus die zijn tweede ronde haalde omdat de eerste ronde werkelijk iets veranderd had.

Twee dingen die de stub bewust niet doet

Hij weigert onmogelijke overgangen niet met een verzonnen foutcode. De grens tussen wel en niet toegestaan woont in het platform, en de redencodes van de agent zijn een vaste lijst die in de bron van de echte agent gecompileerd is. Een code verzinnen die de echte agent niet heeft zou een contract toetsen dat niet bestaat. Een stub die strenger is dan het ding waarvoor hij in de plaats staat leert je dingen die niet waar zijn.

Hij bootst ook geen duur na. Een afsluiting op een echte gast wacht tot het besturingssysteem antwoordt en valt na een tijdslimiet terug op een harde stop. In de stub gebeurt alles binnen de tik. Maar de tijdslimiet en de terugvalvlag worden doorgegeven tot in de uitvoer, zodat een test kan zien dat ze aangekomen zijn, ook al heeft niets erop gewacht. Een parameter doorgeven en hem honoreren zijn verschillende beweringen, en er wordt er maar één van gedaan.

Een tweede wekker maakt een meting willekeurig

De andere reparatie in dezelfde ronde gaat over het meten en niet over het product.

Gepauzeerde runs worden gewekt door een worker in de gateway die elke tien seconden een wachtrij bevraagt. Ons end-to-end-script doet het wekken zelf, in hetzelfde proces, zodat het precies kan vaststellen wat er gebeurde. Die twee zijn los prima en samen rampzalig: draait er een gateway met de wekker aan tegen dezelfde ontwikkeldatabase, dan eet die de rijen op zodra hun wektijd verstreken is, en meet het script welke van de twee er als eerste bij was.

Dat is gebeurd. Eén tak van het script faalde één keer terwijl er niets mis mee was, en dat is de duurste soort falen, want de natuurlijke reactie is op zoek gaan naar een fout.

De oplossing is geen commentaarregel die zegt "zorg dat er geen worker draait". Het is een functie die het meet. Vóór alles maakt het script een wegwerpwacht aan met een al verstreken wektijd, kijkt er acht seconden naar, en controleert of iemand anders hem aanraakte. Als iemand dat deed, weigert het script te draaien en zegt waarom.

Dat is weinig code voor veel vertrouwen. Een aanname die in een commentaarregel staat is een aanname; dezelfde aanname geschreven als een controle die als eerste draait is een voorwaarde.

Opnieuw proberen vraagt om een plafond en een oplopende pauze

Nu de goede paden eindelijk bereikbaar waren, werden de faalpaden meteen ook fatsoenlijk gemeten.

Een wacht die bij het hervatten struikelt, omdat een sjabloon verwijderd is of een server niet meer bestaat, zou anders bij elke tik eeuwig opnieuw geprobeerd worden, in een wachtrijtabel die nooit leegloopt. Er is een plafond van vijf pogingen met een pauze die verdubbelt: zestig, honderdtwintig, tweehonderdveertig, vierhonderdtachtig seconden. Die vier tussenpozen zijn uit een echte run gelezen en niet uit de functie die ze berekent. Bij de vijfde poging wordt de wacht afgebroken en de run als mislukt gemarkeerd.

De tegenproef is het deel waar het om gaat. Met de uitputtingsbewaking uitgeschakeld blijft dezelfde run oneindig in de wachtende stand staan. Een bewaking is pas bewezen als de situatie die ze voorkomt werkelijk is voortgebracht.

Daaronder zit een tweede faalvorm. Een worker claimt een wacht door hem uit de wachtende stand te halen voordat hij begint te wandelen, want de neveneffecten van een flow gebeuren tijdens de wandeling en niet erna. Sterft het proces dat hem claimde halverwege de wandeling, dan blijft de rij eeuwig in de claimende stand staan, en de query die zoekt naar wachtregels die aan de beurt zijn ziet hem niet: die vraagt om wachtende rijen. Een veegronde zet claims ouder dan vijftien minuten terug, en één tik tegen een werkelijk draaiende gateway gaf er één vrij, wekte er één en hervatte er één, zonder falen.

Kleine dingen die je alleen vindt door ze te draaien

Een vertraging van drie seconden levert geen ongeldige knoop op. Ze maakt de hele graaf ongeldig, want het minimum is vijf, en de uitvoerder geeft helemaal niets terug in plaats van een run met een kapotte knoop. Dat is correct en het is de eerste keer ook verwarrend, want je verwachtte een fout over een knoop en kreeg stilte over een graaf.

De poorten van de gateway zijn naar de gateway vernoemd. Zet je de variabelen zonder dat voorvoegsel, dan blijft hij stilletjes op zijn standaardwaarden luisteren, en dat ziet er precies uit als een netwerkprobleem.

Een stop schrijft de knopen die hij nooit bereikte als overgeslagen in het grootboek. Dat is een bewust verschil met ze weglaten: een run die vroeg eindigde en een run die een ander pad nam zien er identiek uit als de onbereikte knopen er gewoon niet staan.

En een goedkeuringsknoop liet weesrijen achter in de meldingentabel, zestien stuks op de ontwikkeldatabase. De opruiming daarvan gaat per run en op de ontdubbelingssleutel, nooit op een patroonvergelijking met een voorvoegsel, want een patroonvergelijking zou rijen weghalen die bij de ronde van iemand anders horen.

Wat we zouden opschrijven

  1. Een test die het goede pad niet kan bereiken is groen om de verkeerde reden. Vraag je af wat de test zou doen als de functie verwijderd werd.
  2. Een stub die alles weigert is geen stub maar een muur. Elke test erachter meet de weigering.
  3. Een nabootsing moet toestand bijhouden en niet alleen antwoorden. Beslist het platform op een kolom, dan moet iets die kolom bewegen.
  4. Maak een stub niet strenger dan het echte ding. Een verzonnen foutcode toetst een contract dat niemand implementeert.
  5. Geef een parameter door ook als je hem niet kunt honoreren, zodat een test kan bewijzen dat hij aankwam.
  6. Twee wekkers op één database maken de meting willekeurig. Een van de twee wint en je kunt niet zien welke.
  7. Maak van de voorwaarde in de commentaarregel een controle die als eerste draait. Dan is het een feit in plaats van een wens.
  8. Bewijs een bewaking door haar uit te zetten. Een pogingenplafond dat niemand geraakt heeft is een bewering.
  9. Een geclaimde rij is onzichtbaar voor de query die naar werk zoekt dat aan de beurt is. Iets moet verlopen claims terugzetten.
  10. Onbereikte knopen horen als overgeslagen in het grootboek, anders is een vroege uitgang niet te onderscheiden van een andere route.

Voor de duidelijkheid over wat dit is: zeven eerder onbewezen takken zijn end-to-end genomen, elk met een rode tegenproef; de mergeverificatie draaide 16 van de 16 stappen zonder een faalregel, de websuite was 4583 groen, packages, gateway en tools samen 7287 groen, de eigen suite van de stub-agent 481 groen, het end-to-end-script 89 groen, de wekkertak 13 groen, de goedkeuringsmailtak 22 groen en de lus met echte acties 18 groen. Onbewezen: er is niets tegen een echte Proxmox gedraaid, een onleesbare graaf op de wachtregel zelf, een batch groter dan de batchgrootte, en echte mailbezorging over SMTP.

Uit het platformwerk aan Provibr. Hoe de wandeling leerde stoppen en morgen verdergaan staat in elke zelfaanroep stond in staartpositie, en waar de grens ligt tussen een pauze die een verzoek kan opvangen en een die de run opschort staat in een pauze die de bestelling openhield.

Begin met één host en één agent

Maak een account aan, installeer de agent met één commando en koppel je eerste Proxmox-host. De proefperiode loopt 14 dagen en vraagt geen betaalgegevens.