Zum Inhalt springen

Blog ·

Ein null, das durch das Sieb fiel

Jeder echte Container meldete, seine Backup-Abdeckung sei nicht feststellbar. Die Konfiguration war lesbar. Die Leitung sagte null, das Schema sagte fehlend.

Von Provibr

Wir bauten gerade den Hardware-Tab für Container, als eine Browserprüfung einen Satz hervorbrachte, den sie nie hätte hervorbringen dürfen: die Konfiguration dieses Containers konnte nicht gelesen werden, deshalb ist nicht festgestellt, was ein Backup enthält.

Die Konfiguration war gelesen worden. Sie stand vollständig in der Antwort, mit jedem Mountpoint darin. Gescheitert war unser eigenes Parsen davon, eine Schicht weiter, und die Meldung beschuldigte die falsche Seite der Leitung.

Von dort aus rückwärts zu arbeiten dauerte, denn es gab nichts, von dem aus man hätte rückwärts arbeiten können. Keine Ausnahme, keine Logzeile, kein fehlschlagender Test. Nur ein ruhiger Satz auf einem Bildschirm, der sagte, etwas sei nicht gemessen worden.

Ein fehlender Schlüssel und ein null sind nicht dasselbe

Der Agent baut die Mountpoint-Zeilen in Rust mit serde_json::json!, und eines der Felder ist ein Option<String>: der Pfad innerhalb des Containers, der aus mp=/data in der Konfiguration stammt.

Ein Option, das None ist, wird dort als null serialisiert. Nicht als fehlender Schlüssel. Das ist das korrekte und dokumentierte Verhalten, und es ist das, was jeder Leser dieses Makros erwarten würde, wenn er eine Sekunde darüber nachdächte, was niemand getan hatte.

Auf der anderen Seite sagte das Schema .optional(), und in zod bedeutet ein bloßes optional, dass der Schlüssel fehlen darf. Es bedeutet nicht, dass der Wert null sein darf. Das sind zwei verschiedene Aussagen über JSON, und wir hatten die eine geschrieben und die andere gesendet.

Das Feld, das dies bei jedem einzelnen Container auslöste, ist jenes, das strukturell immer leer ist. Jede Containerkonfiguration hat eine rootfs-Zeile, und ein rootfs hat per Definition kein mp=, weil es an der Wurzel eingehängt ist. Die allererste Zeile jeder Beobachtung aus jedem echten Container trug also "mountPath": null.

Ein schlechtes Feld bedeutet gar keine Daten

Wäre der Fehlschlag auf dieses Feld beschränkt geblieben, wäre er ein kosmetischer Defekt gewesen. Er war es nicht, und der Grund liegt in der Gestalt des Parsens und nicht in der Gestalt des Fehlers.

Wir validieren die ganze Beobachtung als ein Objekt. Ein safeParse auf einem Objekt scheitert als Ganzes; ein Teilergebnis gibt es nicht. Ein abgelehntes Feld bedeutete also keine Mountliste, keine Einstufung der Abdeckung und keine Vorschau darauf, was ein Rückspielen ersetzen würde. Alles weiter unten sah nichts, und alles weiter unten ist so geschrieben, dass es über nichts ehrlich ist.

Diese Ehrlichkeit ist Absicht, und sie ist der Grund, warum das Produkt Ihnen kein vollständiges Backup meldet, wenn es nicht nachgesehen hat. Eine Containerkonfiguration ohne lesbares rootfs wird als unbekannt gemeldet und nicht als vollständig, genau damit eine leere Messung nie als gute verkleidet werden kann.

Hier arbeitete die Wache genau wie entworfen und brachte einen Satz hervor, der in seinen eigenen Begriffen wahr war: es war nichts festgestellt worden. Die Lüge saß eine Ebene höher, in der Behauptung über das Warum.

Nichts war rot, und der Grund ist unangenehm

Das ging mit der Runde live, die die Backup-Abdeckung für Container einführte, und die Saatdaten dreier Runden liefen über denselben Code, ohne dass etwas rot wurde.

Der Grund ist, dass die Saaten von Hand geschrieben wurden. Um die Abdeckungstabelle ohne ein echtes Proxmox zu bewegen, schreibt man eine Zeile, die aussieht wie das, was der Agent sendet, und was man schreibt, wenn man dieser hier hat keinen Pfad meint, ist, den Schlüssel wegzulassen. Was durchgeht. Drei Runden Testdaten kodierten damit die Annahme des Schemas statt des Verhaltens des Agenten, und jede einzelne davon war grün.

Es gibt keinen echten Hypervisor in der Schleife, der das korrigieren würde. Das Proxmox, zu dem wir Zugang haben, ist die Produktionsmaschine von jemandem, und kein Agent von uns kommt darauf. Die einzige Beschreibung der Leitung, gegen die beide Seiten hätten geprüft werden können, war also die Rust-Quelle, und nichts las sie.

Die Wache liest den Erzeuger, statt ihn zu kopieren

Die Reparatur ist ein Feld. Das Interessante ist der Test daneben.

Er öffnet lxc.rs, findet die Funktion, die die Zeilen baut, zieht die Schlüsselnamen aus dem json!-Block heraus und behauptet dann zwei Dinge. Erstens, dass die Menge der Schlüssel, die der Agent sendet, genau die Menge der Schlüssel ist, die das Schema kennt, sodass keine Seite allein ein Feld bekommen kann. Zweitens, und das ist das Entscheidende, dass eine Zeile, in der jedes optionale Feld null ist, durch das Sieb kommt.

Dieser zweite Test baut seine Zeile aus den Schlüsseln, die er gerade gelesen hat, und nicht aus einer Liste, die in der Testdatei steht. Ein neues optionales Feld, das mit einem bloßen .optional() hinzugefügt wird, ist an dem Tag rot, an dem es hinzugefügt wird, ohne dass sich jemand an diesen Artikel erinnern muss.

Es gibt einen dritten Test für die andere Richtung: eine Zeile von einem älteren Agenten, die die neueren Felder überhaupt nicht trägt, wird weiterhin geparst. Beide Gestalten sind echt, beide stehen in der Datenbank, und eine Korrektur, die nur die neue akzeptiert hätte, hätte die alten Zeilen kaputtgemacht und dabei richtig ausgesehen.

Die Korrektur selbst ist ein preprocess, der null auf undefined abbildet, und kein .nullable(). Das ist eine bewusste Entscheidung über den abgeleiteten Typ: mit nullable wird der Typ zu string | null | undefined, und jeder Leser bekommt einen dritten Zustand, der ihm nichts bedeutet. Mit preprocess bleibt der Typ, wie er war, die vier Leser behalten den Code, den sie ohnehin hatten, und die Toleranz auf der Leitung steht an genau einer Stelle.

Zwei Fehler beim Messen, nicht im Produkt

Festzustellen, dass die Reparatur wirkte, brachte zwei eigene falsche Grüns hervor, und sie sind die übertragbarere Hälfte dieser Runde.

Der erste: ein mit .strict() geschriebenes Schema lehnte still unsere eigene Saat ab, und der Bildschirm wurde leer statt falsch. Ein leerer Bildschirm während einer Reparatur sieht genau aus wie ein Bildschirm, der noch nicht geladen hat.

Der zweite ist schlimmer, und er ist eine hier bekannte Falle, in die wir trotzdem gelaufen sind. Die Backuptabelle lebt in einem Panel, das immer im Dokument eingehängt ist, denn nur so lässt sich die Fokusrückgabe eines Dialogs zum Laufen bringen. Eine Prüfung, die die Tabelle mit einem Query-Selektor liest, liest sie also, ob sie offen ist oder nicht. Unsere Behauptung, dass zwei Tabs dasselbe sagten, ging auf zwei Tabellen durch, die beide leer und beide unsichtbar waren.

Beide wurden auf dieselbe Weise geschlossen: verlangen, dass der Dialog wirklich offen ist, und eine Untergrenze für die Anzahl der Zeilen setzen, bevor man sie vergleicht. Ein Vergleich von nichts mit nichts ist kein Vergleich.

Was wir aufschreiben würden

  1. Ein fehlender Schlüssel und ein null sind verschiedene Aussagen, und JSON-Serialisierer wählen für Sie.
  2. Ein Schema, das nur einen fehlenden Schlüssel akzeptiert, hat seinen Erzeuger nie gelesen.
  3. Die Validierung des ganzen Objekts macht aus einem schlechten Feld gar keine Daten, was der richtige Entwurf und ein schlechter Fehlschlag zum Debuggen ist.
  4. Die gefährlichste Fehlermeldung ist eine ruhige. Unsere war ehrlich über das Ergebnis und falsch über die Ursache.
  5. Von Hand geschriebene Testdaten kodieren Ihre Annahmen, auch die, bei der Sie gleich falsch liegen werden.
  6. Lassen Sie die Wache den Erzeuger lesen. Eine in einen Test kopierte Schlüsselliste driftet; eine aus der Quelle geparste kann es nicht.
  7. Prüfen Sie die Zeile aus lauter null ausdrücklich, denn das ist die Zeile, die Ihr echter Erzeuger zuerst sendet.
  8. Akzeptieren Sie auch die alte Gestalt. Von älteren Agenten geschriebene Zeilen stehen weiterhin in der Datenbank.
  9. Tolerieren Sie auf der Leitung, nicht im Typ. Weiten Sie an der Grenze und lassen Sie die Leser in Ruhe.
  10. Ein immer eingehängtes Panel macht eine DOM-Prüfung leer grün. Behaupten Sie, dass es offen ist und dass es Zeilen hat.

Zur Klarheit darüber, was das ist: der Defekt lag in der Abdeckung jedes echten Containers seit der Runde, die Containerbackups ausgeliefert hat, gefunden von einer Browserprüfung und nicht von irgendeiner Suite, repariert mit einem preprocess und bewacht von einem Test, der die Rust-Quelle liest; die Runde, in der er gefunden wurde, brachte das Container-Testmodul von fünfunddreißig Tests auf neununddreißig, mit sauberem musl und sauberer Formatierung, und endete mit den geteilten Paketen bei 5347 grün, dem Gateway bei 1467, den Stub-Werkzeugen bei 489 und der Web-Suite bei 4593, ohne Fehlschläge irgendwo, und fünfzig Browserprüfungen grün in zwei Sprachen ohne Befunde zur Barrierefreiheit. Nicht bewiesen: nichts davon lief gegen ein echtes Proxmox, die Gestalt der Leitung ist also aus der Quelle festgestellt und nicht aus einer echten Antwort, und die Kette im Betrieb wurde gegen unseren eigenen Stub bewegt.

Aus der Plattformarbeit an Provibr. Wozu die Abdeckungstabelle da ist, steht in Was ein Backup nicht enthält, die Runde, die zuerst eine Containerkonfiguration las, steht in Ein Container wird ersetzt, nicht neu installiert, und eine weitere Prüfung, die grün war, weil sie nie ankam, steht in Grün, weil es nicht hinkommen konnte.

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.