Zum Inhalt springen

Blog ·

Ein Join statt eines Abgleichs

Ein Automatisierungsschritt fror auf dem Zustand ein, den er sah. Die Lösung war kein Hintergrundprozess: Diese Zeile erklärt eine Entscheidung.

Von Provibr

Ein Automatisierungsablauf kann einen Befehl an eine Maschine senden und darauf warten. Läuft die Wartezeit ab, scheitert der Schritt und der Lauf nimmt seinen Fehlerausgang. Das ist richtig: Der Ablauf bat darum zu warten, bis die Arbeit erledigt ist, und sie ist es nicht, also weiß niemand, ob der nächste Knoten gleich auf eine Maschine wirkt, die noch herunterfährt.

Der Schritt hält fest, was er in diesem Moment sah. In einer früheren Runde gegen einen echten Agent gemessen, hielt er sent fest, und sechzig Millisekunden später wurde der Befehl zu einer Zeitüberschreitung. Der Schritt sagte weiterhin sent, einen Tag später, für immer.

Die naheliegende Lesart ist, dass die Zeile veraltet ist und repariert gehört. Diese Lesart ist falsch, und herauszufinden warum, kostete das meiste Nachdenken dieser Runde.

Die Zeile ist eine Erklärung, kein Zustandsfeld

Der eingefrorene Wert ist keine Kopie des Befehlszustands, die auseinandergelaufen ist. Er ist die Antwort auf eine andere Frage: Was wusste der Ablauf, als er entschied, den Fehlerausgang zu nehmen.

Schreibt man timeout darüber, sagt die Zeile nun, der Ablauf habe auf eine Zeitüberschreitung reagiert. Hat er nicht. Er reagierte auf eine ablaufende Wartezeit, während der Befehl noch unterwegs war, ein anderes Ereignis mit anderer Ursache und anderer Behebung. Einen Monat später zöge jemand, der diesen Lauf liest, den falschen Schluss aus einer hilfreich korrigierten Zeile.

Der eingefrorene Wert bleibt also eingefroren. Wirklich falsch war die Bemerkung darüber, die versprach, man könne dieser Zeile entnehmen, ob der Befehl am Ende erfolgreich war. Konnte man nicht, und ein Versprechen in einer Bemerkung hat eine Haltbarkeit bis zu dem Tag, an dem sich jemand darauf verlässt.

Die eigentliche Frage, ob es am Ende doch gut ausging, wird beantwortet, indem der aktuelle Zustand des Befehls in dem Moment gelesen wird, in dem jemand hinsieht. Die Laufseite zeigt beides nebeneinander: was der Ablauf damals sah und was jetzt gilt.

Woher es stammtWas es beantwortet
DamalsDie Prüfzeile, eingefroren im Moment der EntscheidungWarum der Lauf den Ausgang nahm, den er nahm
JetztDie Befehlstabelle, gelesen beim Öffnen der SeiteOb die Arbeit am Ende fertig wurde
Zeile fortKeines von beidem; als eigener Zustand gezeigtDer Befehl steht nicht mehr in den Büchern

Diese letzte Zeile zählt. Ist die Befehlszeile aufgeräumt worden, fällt die Anzeige nicht darauf zurück, den eingefrorenen Wert zu wiederholen. „Steht nicht mehr in den Büchern“ und „sagt weiterhin sent“ sind zwei verschiedene Tatsachen, und eine Seite, welche die zweite darstellt, während die erste gilt, lügt mit unbewegter Miene.

Drei Gründe, warum ein Hintergrundprozess gegen einen Join verliert

Die Alternative war ein Hintergrundprozess, der den Endzustand in die Prüfzeile zurückschreibt, sobald der Befehl entschieden ist. Wir haben andernorts Prozesse genau dieser Form, das war also eine echte Wahl und kein Strohmann.

Ein Hintergrundprozess überschreibt eine Erklärung. Siehe oben. Der Ausweg ist, ein zweites Feld daneben zu schreiben, und das ist ehrlich, aber es ist eine zweite Wahrheit, die an genau einer Stelle gelesen würde: an jener, die jetzt den Join macht.

Ein Hintergrundprozess repariert nur, was nach ihm geschieht. Die Zeile, mit der diese ganze Untersuchung begann, gibt es schon. Ein heute ausgerollter Prozess berührte sie nie, ebenso wenig einen Lauf aus den zwei Runden davor, denn ihre Befehle sind längst entschieden, und kein künftiges Ereignis kommt dort noch vorbei. Ein Join zeigt sie alle vom Tag der Auslieferung an, auch jene, die wir gesucht hatten.

Ein Hintergrundprozess bringt einen ganzen Apparat mit. Zeilen im Hintergrund zu aktualisieren braucht einen Takt, einen Anspruch, damit zwei Instanzen nicht kollidieren, eine Wartezeit gegen Abweichung, eine Antwort auf die Aufbewahrung und einen eigenen Fehlerpfad, und jedes davon kann um drei Uhr nachts falsch stehen. Eine Handvoll Kennungen über den Primärschlüssel nachzuschlagen, wenn jemand eine Seite öffnet, braucht nichts davon.

Die allgemeine Regel darunter: Lieber die Quelle lesen als sie zu kopieren, es sei denn, die Kopie beantwortet eine Frage, die die Quelle nicht beantworten kann. Hier beantwortet die Kopie eine andere Frage und beide werden gebraucht, und genau dann hält man sie auseinander.

Die Kennung stammt aus einer JSON-Spalte, also ist die Abfrage begrenzt

Ein Detail in diesem Join ist weder Optimierung noch Höflichkeit.

Die Befehlskennung steht in einem JSON-Block auf der Prüfzeile. JSON-Spalten haben keine Fremdschlüssel, also garantiert nichts, dass die Kennung darin zu diesem Konto gehört. Heute tut sie es, weil unser eigener Code sie dort hingeschrieben hat. Das ist keine Grenze, das ist eine Gewohnheit.

Die Suche ist deshalb neben der Kennung auch auf die Organisation begrenzt. Ohne diese Grenze läse ein von Hand geänderter Wert in jenem Block den Zustand eines fremden Befehls. Kleines Leck, leicht zu übersehen, und von der Art, die nur findet, wer danach gesucht hat.

Ein Ablauf je Zeitpunkt, mit dem Zeitpunkt im Schlüssel

Ein Hosting-Tarif konnte einen Automatisierungsablauf verknüpfen, in einer einzigen Spalte. Diese Spalte bedeutete „führe dies aus, wenn eine Maschine bereitgestellt wird“, und das stand nirgends geschrieben außer im Verhalten der Funktion, die sie las.

Kunden wollten auch einen Ablauf bei Neuinstallation und einen bei Löschung. Die naheliegende Behebung sind drei Spalten. Sie ist aus einem nennenswerten Grund falsch: In jeder dieser drei Spalten könnte ein Ablauf stehen, dessen eigener Auslöser etwas anderes sagt. Ein Ablauf, der beim Löschen läuft, in der Spalte, die beim Bereitstellen auslöst. Die Invariante „der Auslöser sagt, wann er läuft“ wird an drei Stellen verletzbar statt an keiner, und das Schema widerspricht nirgends.

Die Verknüpfung wurde also eine eigene Tabelle, mit Tarif und Zeitpunkt zusammen als eindeutigem Schlüssel. Ein Ablauf je Zeitpunkt je Tarif, durchgesetzt von einem Index statt von Sorgfalt.

Zwei Einzelheiten dieser Migration sind es wert, behalten zu werden.

Der Zeitpunkt steht als Text mit einer Prüfbedingung, die drei Werte aufzählt, und nicht als die vorhandene Auslöseraufzählung. Diese Aufzählung hat fünf Werte, und zwei davon sind Auslöser, die dieser Mechanismus nie wählt: Ein manueller Lauf wird von einem Menschen gestartet und ein Webhook-Lauf von einer eingehenden Anfrage. Eine Aufzählungsspalte nähme beides bereitwillig an, und dann gäbe es eine Verknüpfungszeile, mit der nie etwas geschieht. Eine Einstellung, die still nichts tut, ist schlimmer als eine fehlende.

Und die alte Spalte wurde in derselben Migration entfernt, die ihren Inhalt übertrug. Sie als bequeme abgeleitete Lesung stehen zu lassen, ist genau die zweite Wahrheit, die diese Tabelle beseitigen soll. Jeden Leser fand der Übersetzer, sobald die Spalte aus dem Schema war; es waren sieben. Die Übertragung läuft vor dem Entfernen, in einer Transaktion, es gibt also keinen Augenblick, in dem die Einstellung eines Kunden nirgends steht.

Die Übertragung nimmt außerdem den eigenen Auslöser jedes Ablaufs, statt die Bedeutung der alten Spalte anzunehmen. Zeilen, deren Ablauf einen anderen Zeitpunkt trug, wären unter dem alten Code nie gelaufen; nun tut die von jemandem gemachte Verknüpfung wirklich das, was er dachte.

Post, die zugewählt wird, im Graphen statt in den Einstellungen

Ein Ablauf kann eine Meldung schreiben, und in dieser Runde darf diese Meldung auch als Post hinausgehen. Das erwies sich als Frage nach Standardwerten und nicht nach dem Versand.

Die Mailkategorien der Plattform sind eine Ausschlussliste: Was nicht ausdrücklich abgeschaltet ist, ist an. Für die vorhandenen Kategorien stimmt das, sie alle sind Dinge, die ein Kunde erfahren muss. Für eine Meldung, deren Text jemand in einem Automatisierungseditor geschrieben hat, stimmt es nicht, denn ein Ablauf, der bei jeder Bestellung läuft, würde jedes Mal das ganze Team anschreiben, bis jemand die Einstellung findet.

Statt die Liste umzudrehen, steht der Standardwert im Graphen: Das Kästchen am Knoten ist aus, und es einzuschalten ist eine bewusste Handlung dessen, der den Text geschrieben hat. Die Kategorie besteht weiter, damit Post an der gewohnten Stelle abgeschaltet werden kann, genau wie bei den anderen. Zwei Mechanismen, jeder für das, worin er gut ist.

Ein Probelauf verschickt niemals Post. Er schreibt die Panelmeldung, die intern und wegklickbar ist, und er sendet nichts nach außen, aus demselben Grund, aus dem ein Probelauf nie eine Maschine berührt: Post landet in den Postfächern echter Menschen, und eine Testschaltfläche, die jemanden überrascht, ist keine Testschaltfläche.

Noch eine Kleinigkeit aus derselben Arbeit. Die Versandseite galt als unerreichbar von dort, wo Abläufe laufen, und deshalb war dies aufgeschoben. Nachgemessen war es umgekehrt: Die Funktion, um die wir herumgehen wollten, besteht gerade deshalb, weil der andere Prozess keine Post darstellen kann, und sie bittet den Prozess, in dem Abläufe ohnehin laufen, um genau diese Darstellung. Der Umweg war länger als der gerade Weg, und der gerade Weg ist ein HTTP-Sprung, eine Signatur und ein Fehlerpfad weniger.

Was wir aufschreiben würden

  1. Eine Prüfzeile erklärt eine Entscheidung. Nicht reparieren. Die Frage, was jetzt gilt, hat eine andere Antwort und verdient einen eigenen Platz.
  2. Eine Reparatur im Hintergrund repariert nur die Zukunft. Würde die Zeile, die zur Arbeit führte, nicht berührt, ist das eine Entwurfsantwort und kein Detail.
  3. Die Quelle lesen, es sei denn, die Kopie beantwortet etwas, das die Quelle nicht kann.
  4. Eine Kennung aus einer JSON-Spalte hat keinen Fremdschlüssel. Die Abfrage auf das Konto begrenzen; dass unser Code den Wert schrieb, ist keine Grenze.
  5. Die Unterscheidung in den Schlüssel legen, nicht in den Spaltennamen. Drei Spalten sind drei Stellen für das Falsche; ein Schlüssel ist keine.
  6. Die alte Spalte in derselben Migration entfernen. Eine zurückgelassene abgeleitete Lesung ist die zweite Wahrheit, die beseitigt werden sollte.
  7. Eine Ausschlussliste ist der falsche Standard für Text, den ein Kunde schrieb. Die Zuwahl dorthin legen, wo der Text entsteht.

Zur Klarheit: All das ist gebaut und läuft. Der Versand selbst wurde mit einem protokollierenden Treiber bewiesen und nicht gegen einen echten Mailserver, und die Tarifverknüpfung wurde noch nicht über eine echte Bestellung ausgeübt.

Aus der Plattformarbeit an Provibr. Wie sich Wartezeit und Fehlerausgang überhaupt verhalten, steht in ein Draht zum Ziehen, und genau ein Auslöser, und warum eine Zeitüberschreitung kein Stoppknopf ist, steht in eine Zeitüberschreitung ist kein Stoppknopf.

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.