Blog ·
Een draad die je sleept, en precies één trigger
Een draad slepen is het makkelijke deel. Lastiger was wat een testknop mag aanraken, wie handelt als niemand kijkt, en hoe lang een pauze mag duren.
Door Provibr
Onze automatiseringsflows begonnen als een lijst met stappen, werden een graaf, daarna een canvas, en deze ronde kregen ze eindelijk het gebaar dat iedereen voor zich ziet bij het woord "flow-editor": je pakt de stip rechts van een knoop en trekt een draad naar de stip links van een andere.
Dat gebaar is een handvol regels code met een lang commentaar erboven dat uitlegt waarom die regels staan waar ze staan. De rest van de ronde bestond uit vragen die niets met slepen te maken hebben. Hoeveel plekken mogen een flow starten. Wat mag een testknop aanraken. Wie handelt er als niemand kijkt. En hoe lang mag een stap pauzeren.
De capture die de klik opeet
Het canvas heeft één transformatielaag. De knopen staan er absoluut in en de draden liggen als SVG in exact dezelfde laag, dus er is geen tweede coördinatenstelsel dat uit de pas kan lopen met het eerste. Dat deel is met opzet saai.
Het interessante deel is de pointercapture. Zodra je op een uitgangsstip drukt, moet er iets de aanwijzer volgen tot je loslaat, ook buiten het venster. De voor de hand liggende uitvoering vangt op de stip waarop je drukte, en die is fout op een manier die geen enkele fout oplevert.
Capture hertarget élke volgende pointergebeurtenis naar het vangende element, pointerup incluis, en de browser vuurt click daarna op de gemeenschappelijke voorouder van neer en op. Vangt de stip zelf, dan komt een sleep die op een andere stip eindigt daar nooit aan; hij komt terug waar hij begon. Dus vangt de surface, en waar de vinger werkelijk losliet is een aparte vraag, beantwoord door document.elementFromPoint().
Dat antwoord heeft drie trappen. Een ingangsstip onder de aanwijzer. Zo niet, een knoopvlak onder de aanwijzer, dat naar zijn eerste ingang leidt. Zo niet, een magneet: de dichtstbijzijnde ingang binnen zestien schermpixels, en dat is wat het gebaar vergevingsgezind maakt zonder dat iemand op een doel van tien pixels hoeft te mikken.
Twee kleinere bevindingen die pas verschijnen als je dit bouwt. React hangt wheel passief aan de wortel, dus de standaardactie voorkomen in zijn handler doet niets en de pagina scrolt terwijl je probeert te zoomen; die luisteraar moet op het element zelf staan, uitdrukkelijk niet-passief. En het midden van een bezier is niet het midden van zijn omhullende doos, dus een draad raken betekent het pad om een punt op een gegeven lengte vragen en dat door de huidige transformatiematrix halen.
Klik, dan klik, werkt nog steeds. Dat is niet de mindere weg: het is de weg die met een toetsenbord werkt, en daarom zijn de stippen echte knoppen gebleven.
Eén trigger, en vier dingen die eraan hangen
Een graaf heeft precies één triggerknoop. Nul is een fout bij het opslaan en twee is een fout bij het opslaan. Dat klinkt als netheid. Het is dragend, en vier functies rusten erop.
| Wat het oplevert | Waarom één trigger nodig is |
|---|---|
| Bereikbaarheid | Elke uitvoerbare knoop moet bereikbaar zijn vanaf één bekend startpunt |
| Cyclusdetectie | Een kleurenzoektocht in de diepte vraagt een wortel; twee wortels zijn twee zoektochten en twee antwoorden om samen te voegen |
| De variabelenkiezer | Welke variabelen bestaan hangt af van welke gebeurtenis afging, en een tweede trigger zou twee antwoorden in één editor betekenen |
| Contextsnoei | Een variabelengroep die deze gebeurtenis niet kan leveren wordt verwijderd, dus lezen is een fout en geen lege tekenreeks |
Die laatste regel verdient zichzelf terug. Er zijn vier triggergebeurtenissen: een server werd uitgeleverd, opnieuw geïnstalleerd, verwijderd, of met de hand gestart. Een verwijderde server heeft geen besturingssysteemslug, dus de hele besturingssysteemgroep verdwijnt uit de context in plaats van op een leeg object gezet te worden. Met strikte variabelen levert lezen dan een leesbare fout "onbekende variabele" op op het moment dat je opslaat. Zet hem leeg en de voorwaarde die hem leest wordt onwaar, stil, voorgoed, op de tak die ertoe deed.
Dit is ook waarom er geen aparte inject-knoop bestaat. Handmatig starten is een triggergebeurtenis en geen tweede ingang; er een toevoegen zou de invariant breken waar alle vier de functies op steunen, in ruil voor een knoop die iets mooier tekent.
Een proefrun raakt niets aan en loopt tóch
De editor heeft een knop Test run, en een graaf kan nu een knoop bevatten die de server van deze run start, stopt, afsluit, pauzeert of hervat. Die twee feiten op één scherm zijn een val: iemand drukt op testen om te zien welke tak een graaf neemt, en zet een klantmachine uit.
Dus stuurt een proefrun nooit een commando. De actieknoop komt meteen terug, gemarkeerd als overgeslagen, met een redencode die in de taal van de lezer wordt weergegeven. Belangrijker nog: hij neemt de tak ok. Een test die naar de fouttak viel zou beweren dat de graaf iets doet wat hij niet doet, en dat is erger dan niets zeggen. Werkelijk handelen gaat via inject, dat is een echte run, en die vraagt een bevestiging.
Wie handelt er als niemand kijkt
Een flowrun heeft geen kijker. Bij een bestelling is degene die op Bestellen drukte hooguit toevallig de persoon aan wie de handeling toegeschreven hoort te worden. Bij een verwijdering is hij misschien al weg. Maar een server opzoeken vraagt een handelende partij met een rechtenprofiel, en dat profiel moet ergens vandaan komen.
De keuze is dat een run handelt als de eigenaar van het account, om drie redenen in volgorde van gewicht. Het autorisatiemoment is het deployen en niet het draaien: een flow opslaan vraagt de module scripts, hem aan een plan hangen vraagt planrechten, en dat is dezelfde vorm als een cron of de monitoringworker. Handelen als de klikker zou de graaf onreproduceerbaar maken: dan start dezelfde flow wél een machine voor de ene besteller en niet voor de andere, door een rechtenvinkje dat niets met de flow te maken heeft. En de echte grens blijft hoe dan ook hard: de opzoeking krijgt het organisatie-id van de run mee en weigert elke rij daarbuiten, en deze knoop kan per constructie alleen de server van deze run adresseren.
Voordat er een commando ontstaat moeten twee zeven allebei ja zeggen: kan deze provider dit commando, en kan dit soort resource het. Op Proxmox weigert vandaag geen van de vijf acties op een container, en juist daarom vraagt de proef van begin tot eind ook om een schijfvergroting en eist dat die geweigerd wordt. Een zeef die nooit nee zegt is niet te onderscheiden van een zeef die niet aangesloten is.
Vijf minuten, omdat de pauze binnen een verzoek zit
De delay-knoop staat vijf tot driehonderd seconden toe. De opdracht vroeg zeshonderd, en zeshonderd is fout om een meetbare reden: een flow die door een bestelling wordt uitgelokt wordt binnen het bestelverzoek afgewacht. Tien minuten pauze is tien minuten waarin een betalende klant naar een spinner kijkt.
Er is geen wachtstand die een herstart overleeft, en zolang die er niet is kan een pauze niet langer duren dan een verzoek. Die naad draagt een naam in de code, zodat "waarom kan mijn flow niet een uur wachten" een vraag met een vindbaar antwoord is. Hij vraagt een wachtrij die een half afgelopen run kan bewaren en later hervatten, en pas als die bestaat kunnen er lussen bij.
De HTTP-knoop, en waarom hij geen fetch is
Een HTTP-knoop in een automatiseringsgereedschap is een machine voor server-side request forgery tenzij je oplet, en opletten sluit hier de standaardclient uit.
fetch kan het enige niet wat dit beleid vraagt: verbinden met een adres dat wij gekozen hebben terwijl de hostnaam in Host en SNI blijft staan. Hij resolvet zelf, en die tweede resolutie is precies het venster waarin DNS-rebinding leeft. Je zeeft een publiek adres, en een halve seconde later verbindt de client met loopback omdat de levensduur van het record nul was.
De eigen HTTP-client van Node accepteert een eigen resolutie, en de onze geeft precies één adres terug: het adres dat de zeef goedkeurde. Host en SNI worden nog steeds uit de hostnaam afgeleid, dus TLS wordt tegen de naam gevalideerd. Certificaatvalidatie staat aan zonder uitknop, en proxy-omgevingsvariabelen doen niets, want die client leest ze niet.
Omleidingen worden met de hand gevolgd, hoogstens drie, en elke hop wordt opnieuw van voren af aan gezeefd: schema, gebruikersinfo, poort, DNS, elk adres. Een open redirect naar een metadata-eindpunt van een cloud wordt geweigerd op hop twee, ook al was hop één een keurige publieke host. Zeven headernamen zijn toegestaan, en twee daarvan mogen alleen een verwijzing naar een opgeslagen geheim dragen, dat voor het verzoek ontsleuteld wordt, uit alles wat bewaard wordt geredigeerd, en uitsluitend op naam vastgelegd.
Wat wij zouden opschrijven
- Vang op de omhulling en niet op wat je vastpakte. Pointercapture hertarget alles wat erna komt, en het symptoom is een gebaar dat stil eindigt waar het begon.
- Een vergevingsgezind gebaar vraagt een keten van terugvallen, geen groter doel. Element onder de aanwijzer, dan zijn ouder, dan een afstandsgrens.
- Eén ingang koopt vier functies. Bereikbaarheid, cyclusdetectie, een variabelenkiezer en contextsnoei vragen alle vier één bekende wortel. Tel wat je verliest voor je er een tweede bij zet.
- Verwijder wat niet kan bestaan; maak het niet leeg. Een afwezige variabelengroep faalt luid bij het opslaan; een lege faalt stil tijdens de run.
- Een testknop die productie kan raken is geen testknop. Overslaan en tóch de geslaagde tak nemen is de eerlijke combinatie.
- Een pauze wordt begrensd door wie erop wacht. Is de aanroeper een HTTP-verzoek, dan hoort de pauze in seconden.
Voor de duidelijkheid over wat dit is: flows zijn bereikbaar vanaf het scriptsscherm en het canvas is nu de enige editor. Drie dingen zijn onbewezen. Het wachten op een werkelijk afgerond commando is niet tegen een echte hypervisor gemeten, de sleep is met nagebootste browserprotocolgebeurtenissen aangedreven in plaats van met een hand, en niets ervan is op een productiebuild gemeten.
Uit het platformwerk van Provibr. Waarom de graaf om te beginnen eindig moet zijn staat in een flow is een graaf, en die moet eindig zijn, en hoe één overzichtsscherm andermans tabellen leest zonder er ooit in te schrijven staat in projecteren, nooit vervangen.