Zum Inhalt springen

Blog ·

Zwei Runden, eine Feldnummer

Zwei Branches fügten am selben Tag eine Variante zur selben protobuf-Nachricht hinzu und wählten dieselbe Nummer. Daran scheitert nur sehr wenig laut.

Von Provibr

Zwei Arbeiten liefen parallel auf demselben Repository. Die eine fügte die Möglichkeit hinzu, einen Hypervisor zu fragen, was aus einem Hintergrundvorgang geworden ist. Die andere fügte die Möglichkeit hinzu, einen Container neu zu installieren. Beide brauchten eine neue Variante in derselben protobuf-Nachricht, beide sahen in die Datei, beide sahen, dass die höchste benutzte Nummer dreiunddreißig war, und beide schrieben vierunddreißig.

Keine von beiden war unachtsam. Jede las die Datei in dem Moment richtig, in dem sie abzweigte. Die Kollision besteht nur in einem dritten Zustand, den keine von beiden je übersetzt hat: dem Merge.

Wo die Kollision auftauchtWas geschieht
Die beiden Branches getrenntNichts. Jeder ist gegen seinen eigenen Ausgangspunkt korrekt
Der Merge, in der SchemadateiEin Konflikt, und dann ein harter Übersetzungsfehler, wenn beide bleiben
Der Merge, im eingecheckten erzeugten CodeEin Konflikt, und dann überhaupt nichts, wenn beide bleiben
Die VerträglichkeitsprüfungGrün in beiden Branches, denn sie vergleicht jeden mit seinem eigenen Referenzstand
Ein ausgerollter Agent und ein Gateway, die sich uneinig sindHier nie erreicht: ein Befehl, der leer ankäme, gemeldet Schichten von der Ursache entfernt

Was tatsächlich über die Leitung geht

Ein Feldname in protobuf ist eine Bequemlichkeit für Menschen. Über die Verbindung geht die Nummer, um drei Bit nach links verschoben, mit einer Typangabe in den unteren Bits, Feld vierunddreißig mit einer Nutzlast, der die Länge vorangestellt ist, wird also als die Bytefolge für zweihundertvierundsiebzig geschrieben. Benennen Sie ein Feld um, und auf der binären Leitung ändert sich nichts. Ändern Sie seine Nummer, und jeder ausgerollte Leser entschlüsselt jetzt etwas anderes unter derselben Kennung.

Deshalb ist eine Nummer eingefroren, sobald sie in einer Veröffentlichung hinausgegangen ist. Das ist keine Stilregel. Es ist die einzige Kennung, die sich die beiden Enden teilen.

Was die Werkzeuge sagen, und was nicht

Git hat es bemerkt. Der Konflikt tauchte in der Schemadatei und im eingecheckten erzeugten TypeScript auf, beide Male als sich überlappende Ergänzungen an derselben Stelle. Die verlockende Auflösung ist die, die für zwei unabhängige Funktionen offensichtlich richtig aussieht: beide behalten.

In der Schemadatei wird das abgefangen. Das Übersetzen erzeugt eine schlichte Weigerung, die die Nachricht und das Feld benennt, das die Nummer bereits trägt, und das wurde gemessen und nicht angenommen: Eine Testnachricht mit zwei Varianten, beide auf vierunddreißig, wird mit Zeile und Spalte und einem Vorschlag für die nächste freie Nummer abgelehnt.

Das erzeugte TypeScript ist eine andere Geschichte, und es ist der Grund, warum sich das aufzuschreiben lohnt. Diese Datei wird eingecheckt, denn die tägliche Arbeit soll nicht verlangen, dass die Werkzeugkette für das Schema installiert ist. Der Preis dieser Wahl ist, dass sie an Merges teilnimmt wie jede andere Quelldatei, und sie ist gewöhnlicher Code. Ein doppelter Zweig in einer Fallunterscheidung ist gültiges TypeScript. Eine kleine Datei mit zwei gleichen Fallmarken zu übersetzen, die beide die Prüfung der Feldkennung für Feld vierunddreißig tragen, erzeugt überhaupt keine Meldung und endet sauber. Der zweite Zweig wird schlicht nie erreicht.

Eine von Hand zusammengeführte erzeugte Datei gibt Ihnen also einen Kodierer, der für zwei verschiedene Nachrichten dieselbe Kennung schreibt, und einen Dekodierer, der sie immer auf die auflöst, die zuerst gelistet ist. Nichts scheitert beim Bauen, nichts scheitert beim Start, und nichts scheitert bei der ersten Nachricht. Eine der beiden Funktionen ist schlicht falsch, und zwar so, dass es sich nur auf der Maschine am anderen Ende der Verbindung zeigt.

Warum die Verträglichkeitsprüfung grün war

Das Schema wird von einer Prüfung auf brechende Änderungen bewacht, und die hat ihre Arbeit getan. Sie vergleicht ein vorgeschlagenes Schema mit einem Referenzstand. Einer bestehenden Auswahl eine neue Variante hinzuzufügen ist keine brechende Änderung, und in keinem der beiden Branches war es eine, denn im Referenzstand jedes Branches war vierunddreißig tatsächlich nicht in Gebrauch.

Das ist die allgemeine Form des Problems, und es lohnt sich, sie losgelöst von protobuf auszusprechen: Eine Prüfung, die einen Branch mit seinem eigenen Ausgangspunkt vergleicht, kann einen Konflikt nicht sehen, der nur zwischen zwei Branches besteht. Dieselbe Fehlerklasse taucht überall dort auf, wo parallele Arbeit einen Wert aus einem gemeinsamen, geordneten Raum wählt. Datenbankmigrationen werden hier über den Zeitstempel in ihrem Journal gewählt, und diese Zeitstempel sind von Hand geschriebene runde Zahlen und keine echte Uhr, sodass zwei Runden, die je eine Migration schreiben, ihren selbst wählen; eine Migration, deren Zeitstempel unter einem bereits angewendeten landet, wird dauerhaft übersprungen, und der Migrator meldet trotzdem, dass das Schema aktuell ist. Anderer Mechanismus, dasselbe Schweigen, dieselbe Ursache.

Warum das Schweigen der teure Teil ist

Hätten beide Nummern eine Veröffentlichung erreicht, wäre der Fehlschlag entfernt und rätselhaft gewesen. Protobuf verwirft ein Auswahlfeld, das es nicht kennt, ein Agent, der aus dem einen Branch gebaut ist und einen Befehl aus dem anderen empfängt, bekommt also keinen Fehler. Er bekommt einen Befehl ohne Inhalt und meldet so etwas wie „die Nachricht trug keine Anweisung“. Die Ursache liegt Schichten von diesem Satz entfernt, auf einer Maschine, die jemand anderem gehört, und wer ihn liest, hat keinen Grund, eine Nummer zu verdächtigen.

Die Plattform weigert sich bereits, Anweisungen an Agenten zu senden, die die passende Fähigkeit nicht angekündigt haben, gerade weil dieses Fehlerbild so wenig hilft: Ein Agent, der bloß zu alt ist, erzeugt wenigstens eine Ablehnung, die die Plattform geschrieben hat und die die Funktion und die Version benennt. Diese Absicherung deckt einen Versatz zwischen Versionen ab. Sie deckt nicht zwei Nummern ab, die zwei Dinge bedeuten, denn von der Seite der Plattform aus ist überhaupt nichts veraltet.

Die Regel, die daraus entstanden ist

Eine Feldnummer, die in einer Veröffentlichung hinausgegangen ist, ist eingefroren, also wandert die Runde, die noch nicht ausgeliefert hat. Vierunddreißig war als Variante für den Vorgangsstatus schon in einer Agentenveröffentlichung hinausgegangen, beim Merge wurde die Variante für die Neuinstallation eines Containers deshalb fünfunddreißig, und der erzeugte Code wurde aus dem Schema neu erzeugt statt von Hand zusammengeführt. Das Neuerzeugen ist die wichtige Hälfte: Ein Merge von Hand an erzeugter Ausgabe ist ein von Hand geschriebener Kodierer, den niemand als solchen begutachtet hat.

Die praktische Anweisung für den, der die nächste hinzufügt, ist kurz. Lesen Sie die höchste Nummer im Hauptzweig in dem Moment, in dem Sie einchecken, und nicht in dem Moment, in dem Sie abgezweigt sind. Und bei jedem Merge, der das Schema berührt, wird das absichtlich von einem Menschen geprüft, denn kein Werkzeug in der Kette sieht es aus diesem Blickwinkel an.

Reservierungen, und warum es sich lohnt, sie einzuhalten

Dasselbe Schema zeigt, wie die Disziplin aussieht, wenn sie funktioniert. Drei Nummern liegen reserviert für Integrationen, die geplant und nie gebaut wurden, damit niemand sie still wiederverwenden kann. Zwei weitere wurden für zwei bestimmte Steuerungspanels festgehalten, und beide wurden am Ende von den Runden gefüllt, denen sie versprochen waren. Ein drittes Panel, das zufällig früher fertig war, hat keine davon genommen; es hat stattdessen die nächste freie Nummer weiter hinten genommen.

Das ist der ganze Sinn einer Reservierung. Eine aufzuessen, weil man zufällig zuerst fertig war, ist genau der Weg, auf dem ein Nummernschema aufhört, etwas zu bedeuten, und der Kommentar in der Datei sagt das mit ebenso vielen Worten.

Eine reservierte Nummer wie vorgesehen zu füllen wird von der Prüfung auf brechende Änderungen sehr wohl gemeldet, als Entfernen einer Reservierung. Diese Meldung ist erwartet und richtigerweise anzunehmen: Kein Feld hat je diese Nummer getragen, kein ausgerollter Leser kann also eine Meinung dazu haben. Vorher wissenswert, denn beim ersten Mal sieht es beunruhigend aus, und die Versuchung ist, eine andere Nummer zu wählen, um das Werkzeug ruhigzustellen, was die Reservierung wegwirft, die man gerade eingehalten hat.

Was wir aufschreiben würden

  1. Behandeln Sie eine ausgelieferte Nummer als unveränderlich und „die höchste Nummer, die ich sehe“ als eine Aussage über Ihren Branch und nicht über das Projekt.
  2. Lesen Sie den gemeinsamen Raum beim Einchecken und nicht beim Abzweigen. Das ist der einzige Moment, in dem der Wert tatsächlich beansprucht wird.
  3. Erzeugen Sie erzeugten Code neu, führen Sie ihn nie zusammen. Ein Konflikt in erzeugter Ausgabe ist ein Zeichen, den Generator laufen zu lassen, und keine Einladung zum Bearbeiten.
  4. Wissen Sie, welche Ihrer Prüfungen sich auf einen Referenzstand beziehen. Sie können Kollisionen zwischen Geschwistern nicht sehen, und diese Lücke braucht einen Menschen beim Merge.
  5. Reservieren Sie die Nummern, die Sie versprochen oder eingeplant haben, mit einem Kommentar, der sagt, wem sie gehören, und nehmen Sie das Rauschen hin, das entsteht, wenn eine davon richtig gefüllt wird.
  6. Suchen Sie dieselbe Form anderswo. Jeder von Hand vergebene Wert in einem gemeinsamen geordneten Raum, Migrationen eingeschlossen, hat diesen Fehlschlag, und meist hat er ihn still.

Zur Klarheit: Es wurde nichts mit der Kollision darin ausgeliefert. Sie wurde beim Merge gefangen, die spätere Variante ist gewandert, der erzeugte Code wurde aus dem Schema neu gebaut, und beide Funktionen gingen als getrennte Agentenveröffentlichungen im Abstand von Stunden hinaus. Fangbar wurde sie dadurch, dass jemand den Konflikt gelesen hat, statt ihn aufzulösen, und das ist kein Vorgang, den man automatisieren kann, aber einer, den man aufschreiben kann.

Aus der Plattformarbeit bei Provibr. Die Datenbankfassung desselben Fehlschlags steht in die Migration, die nie lief, und warum sich ein signiertes Agenten-Update selbst verweigert, statt dem Absender zu vertrauen, steht in wie ein signiertes Agenten-Update sich verweigert.

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.