Zum Inhalt springen

Blog ·

Ein Flow ist ein Graph, und er muss endlich sein

Aus einer Schrittliste einen Node-Editor zu machen klingt nach Oberfläche. Es ist eine Entscheidung über Schleifen, über nicht genommene Zweige und einen Klick.

Von Provibr

Ein Flow in unserem Panel tut genau eine schmale Sache: Sobald ein Server fertig ausgerollt ist, rendert er Text. Eine Willkommensmail, ein Stück Konfiguration, eine Notiz für das Ticket. Er startet keinen Hypervisor neu und er spricht mit keiner API. Genau diese Enge ist der Grund, warum die Funktion sicher an einen Kunden zu geben ist.

Bis zu dieser Runde war ein Flow eine Liste: Schritt eins, Schritt zwei, Schritt drei, Abbruch beim ersten Fehler, sofern ein Häkchen nichts anderes sagte. Gewünscht war eine Arbeitsfläche mit Knoten und Verbindungen. Das klingt nach einer anderen Darstellung derselben Daten. Ist es nicht, und das Interessante sind die Fragen, die eine Arbeitsfläche beantwortet haben will und die eine Liste bereits für Sie beantwortet hat.

Version eins kennt keine Schleifen, und das ist keine Bequemlichkeit

Eine Verbindung, die zurückläuft, ist das Erste, was jeder ausprobiert. Wir lehnen sie beim Speichern ab, und die Fehlermeldung nennt die Knoten, die den Zyklus bilden.

Der Grund liegt in der Natur des Teilsystems und nicht in der Schwierigkeit des Codes. Ein Renderflow hat keinen Wartezustand. Es gibt keinen Verzögerungsknoten, kein Warten auf Freigabe, keine Gegenstelle, die später etwas meldet; jeder Knoten ist synchron und nach wenigen Millisekunden fertig. Eine Schleife ohne Wartezustand ist damit konstruktionsbedingt unendlich, und der einzige Weg, sie zu zähmen, wäre ein verborgener Iterationszähler: eine unsichtbare Grenze, die niemand sieht und die an dem Tag, an dem sie erreicht wird, einen Lauf mittendrin abbricht, ohne dass jemand erklären kann warum.

Den Zyklus beim Speichern abzulehnen ist die ehrliche Fassung derselben Grenze. Sobald es einen echten Wartezustand gibt, ändert sich die Abwägung, und eine Schleife wird zu einer Entwurfsentscheidung mit einem Grund dahinter. Heute wäre sie eine Falle.

Verzweigungen werden aus verwandtem Grund sequenziell abgearbeitet. Zwei Verbindungen an demselben Ausgang werden nacheinander verfolgt, in gespeicherter Reihenfolge, und der erste Zweig wird vollständig durchlaufen, bevor der zweite beginnt. Parallel zu rendern hieße, dass zwei Zweige gleichzeitig in dieselben Variablen schreiben, und dann hängt das Ergebnis eines Flows davon ab, welches Rendering zufällig zuerst fertig war. Rendern dauert Millisekunden, es gibt also nichts zu gewinnen und Reproduzierbarkeit zu verlieren.

Sechs Arten, und Ausgänge, die eine Funktion sein müssen

Es gibt sechs Knotenarten, bewusst wenige: Trigger, Skript, Verzweigung, Zuweisung, Notiz, Ende. Jede weitere Art bedeutet einen Paletteneintrag, ein Eigenschaftenfenster, neun Übersetzungen, einen Zweig im Ausführer und eine Zeile in der Übergangstabelle, weshalb ein Test diese Anforderungen aus der Liste ableitet. Eine siebte Art ohne Fenster ist ein roter Test statt eines halb funktionierenden Knotens.

ArtAusgänge
triggereiner, und es gibt genau einen Trigger pro Graph
scriptok und error
switcheiner pro Regel, dazu otherwise
seteiner
notekeiner, und er wird nie besucht
endkeiner, und er spricht das Ergebnis aus

Die Zeile zur Verzweigung ist der Grund, warum die Ausgänge von einer Funktion berechnet und nicht aus einer Tabelle gelesen werden. Eine Verzweigung mit drei Regeln hat vier Ausgänge; eine feste Tabelle ließe jemanden eine vierte Regel hinzufügen, ohne dass ein Ausgang entsteht, und dieser Zweig verschwände dann stillschweigend. Alles, was die Gestalt eines Knotens verändern kann, gehört in eine Funktion.

Die Prüfung läuft in fester Reihenfolge und jede Ablehnung benennt ihren Knoten: doppelte Kennungen, null oder zwei Trigger, ein Variablenpfad außerhalb des Katalogs, ein unsicheres Muster, ein unbekannter Ausgang, eine doppelte Verbindung, ein Zyklus, ein unerreichbarer Knoten und schließlich mehr ausführbare Knoten als die Grenze von fünfundzwanzig. Die Reihenfolge zählt, denn die erste Ablehnung ist die, die gelesen wird, und „hier ist ein Zyklus“ hilft mehr als „diesen Ausgang gibt es nicht“, wenn beides zutrifft.

In state. schreiben und nie in flow.

Ein set-Knoten schreibt Variablen. Der naheliegende Namensraum ist der bereits vorhandene, und er ist der falsche. flow.name, flow.runId und flow.step tragen Angaben über den Lauf, ein Knoten mit Schreibrecht dort ließe also einen Kunden flow.name überschreiben, und dann lügt die Prüfspur auf eine Weise, die wie Daten aussieht.

Deshalb schreibt set in state., einen Namensraum, der neu ist, zu Beginn leer und nur für die Dauer eines Laufs existiert. Eine Vorlage, die {{ state.role }} liest, ohne dass ein Knoten den Wert gesetzt hat, scheitert wie jede andere fehlende Variable, und genau das will man: Ein strenger Modus ist eine bessere Fehlermeldung als eine leere Zeile in einer Willkommensmail.

Die erste Regel gewinnt, und der Zweig, den niemand nahm

Eine Verzweigung liest einen Variablenpfad und prüft ihre Regeln von oben nach unten. Die erste passende Regel bestimmt den Ausgang; passt keine, verlässt der Lauf sie über otherwise. Acht Operatoren, nicht die dreißig eines allgemeinen Werkzeugs, und alle acht lassen sich in einem Satz erklären.

Wichtiger als das Prüfen war, was aufgeschrieben wird. Jeder besuchte Knoten erzeugt eine Zeile mit Kennung, Art und Ergebnis, und bei einer Verzweigung ist dieses Ergebnis, welcher Ausgang genommen und welcher Wert gelesen wurde. Das konnte nicht in das Fehlerfeld, das den Grund für ein Scheitern oder Überspringen trägt, und auch nicht in die Kontextaufnahme, die die Eingabe eines Renderings ist und an der ein Versprechen hängt.

Und jeder ausführbare Knoten, der nicht besucht wurde, erzeugt ebenfalls eine Zeile, mit eigener Begründung. „Ein früherer Schritt scheiterte, dieser lief also nicht“ und „der Lauf nahm einen anderen Zweig, dieser Knoten wurde nicht erreicht“ sind zwei verschiedene Sätze, und sie auseinanderzuhalten ist genau das, was man tut, wenn man einen überraschenden Lauf liest.

Ein Muster, das sich nicht abbrechen lässt

Ein Operator der Verzweigung prüft einen regulären Ausdruck, und er ist das einzige wirklich gefährliche Stück der Funktion. JavaScript kennt keine Möglichkeit, ein RegExp.test() abzubrechen. Es gibt keine Frist und keine Unterbrechung, und die Maschine läuft zurück, sodass ein Muster wie (a+)+$ auf vierzig Zeichen eine Anfrageverarbeitung minutenlang blockiert. Das ist eine Dienstblockade mit einem einzigen Eingabefeld.

Die Agentenseite löste dasselbe Problem in einer früheren Runde, indem sie die Maschine begrenzte, mit einer Implementierung ohne Rücklauf und mit einer Speichergrenze. Dieser Weg besteht hier nicht, also wird stattdessen die Sprache auf eine Teilmenge begrenzt, in der sich die Katastrophe nicht ausdrücken lässt: keine Rückwärtsverweise, kein Lookaround und Sternhöhe eins, was bedeutet, dass eine Wiederholung nicht in einer Wiederholung stehen darf. (a+)+ wird damit unmöglich, a+b+ bleibt erlaubt. Mit einer Längengrenze für Muster und Eingabe ist der schlimmste Fall quadratisch in wenigen tausend Zeichen, und das erledigt ein moderner Kern, ohne es zu bemerken.

Die Listenansicht ist nicht die schlechtere

Jede Handlung der Arbeitsfläche hat eine Schaltfläche in einer Listenansicht, die aus demselben reinen Modell entsteht: hinzufügen, auswählen, verschieben, löschen, verbinden und trennen. Die Reihenfolge dort ist topologisch, also die Reihenfolge, in der der Ausführer die Knoten besucht, und damit die einzige, der ein Leser ohne Bild folgen kann; bei einer Verzweigung stehen die Zweige untereinander, mit ihren Verbindungen als Sätzen darunter.

Das ist kein Zugänglichkeitsanhang, der an eine Zeichnung geschraubt wurde. Es ist der Grund, warum unter der Arbeitsfläche überhaupt keine Graphenbibliothek liegt. Eine Bibliothek kann Knoten fokussierbar machen und sie mit Pfeiltasten verschieben, aber keine Bibliothek zieht eine Verbindung mit der Tastatur, dieser zweite Weg musste also ohnehin entstehen. Sobald er da ist, macht die Bibliothek nur noch die Mausarbeit, und bei fünfundzwanzig Knoten ist das der kleine Teil.

Eine Codezeile, die jeden Klick verschluckte

Die Arbeitsfläche ließ sich verschieben, ließ sich ziehen und wählte nichts aus. Ein Klick auf den Ausgang ok eines Knotens tat gar nichts, und im DOM war nichts falsch: Der Handler hing daran, das Element war da, die Koordinaten stimmten.

setPointerCapture() auf einem Vorfahren leitet jedes folgende Zeigerereignis auf das erfassende Element um, einschließlich pointerup. Der Browser feuert click danach auf dem gemeinsamen Vorfahren von Herunter und Hoch, und bei aktiver Erfassung ist das der Container selbst. Der eigene Klickhandler des Knotens wird nie erreicht, und nirgends entsteht ein Fehler.

Die Reparatur ist baulich und kein Schalter: Das Verschieben hängt jetzt an einer Hintergrundfläche hinter den Knoten, sodass ein Zeigerdruck auf einem Knoten die Erfassung gar nicht erst auslösen kann.

Es steckt eine zweite Lehre darin, wie knapp das übersehen wurde. Die Browserprüfung für „eine Verbindung ist dazugekommen“ zählte svg path-Elemente, und jedes Symbol der Oberfläche ist eines davon, die Zahl stieg also unabhängig davon, ob eine Verbindung erschien. Sie zählt jetzt Pfade innerhalb der Verbindungsebene, gegen eine genaue Anzahl.

Was wir aufschreiben würden

  1. Den Zyklus ablehnen, statt die Iterationen zu deckeln. Eine verborgene Grenze ist eine Falle; eine Ablehnung beim Speichern mit den betroffenen Knoten ist eine Mitteilung.
  2. Ausgänge als Funktion des Knotens bestimmen. Was seine Gestalt aus seiner Konfiguration bezieht, lässt sich mit einer festen Tabelle nicht beschreiben.
  3. Neue Variablen bekommen einen neuen Namensraum. Benutzereingaben über die Felder schreiben zu lassen, die den Lauf beschreiben, macht aus der Prüfspur eine Erzählung.
  4. Den nicht genommenen Zweig festhalten, mit eigener Begründung. „Nicht erreicht“ und „nach einem Fehler nicht gelaufen“ beantworten verschiedene Fragen.
  5. Lässt sich die Maschine nicht abbrechen, dann die Sprache begrenzen. Sternhöhe eins plus Längengrenze macht aus einer Explosion eine Rechenaufgabe.
  6. Den Tastaturweg zuerst bauen, wenn er eine Anforderung ist. Sobald er steht, zeigt sich oft, dass die Bibliothek nur die leichte Hälfte übernahm.
  7. Zeigererfassung stiehlt Klicks von Nachfahren. Wenn sich eine Arbeitsfläche gut ziehen lässt und nichts auswählt, ist das der erste Ort zum Nachsehen.

Zur Klarheit darüber, was das ist: Flows leben auf dem Skriptbildschirm, der heute bewusst nicht im Seitenmenü steht, sie sind also erreichbar und nicht beworben. Drei Dinge sind unbewiesen. Die Umwandlung von Graph zurück in Liste wurde im Code geprüft und nie im Browser gefahren. Der Zweig für eine Netzwerkinstallation eines Graphflows wurde nicht durchgängig durchlaufen. Und ein Probelauf ist synchron, es gibt also kein Livesignal, das er senden könnte.

Aus der Plattformarbeit von Provibr. Warum ein Schritt, der still nichts tat, schlimmer ist als einer, der scheiterte, steht in die Migration, die nie lief, und wie eine einzige Fortschrittsansicht fremde Tabellen liest, ohne je hineinzuschreiben, steht in projizieren, niemals ersetzen.

Fangen Sie mit einem Host und einem Agenten an

Konto erstellen, den Agenten mit einem einzigen Befehl installieren und Ihren ersten Proxmox-Host verbinden. Die Testphase läuft 14 Tage und fragt nicht nach Zahlungsdaten.