Salta al contenuto

Blog ·

Un null sfuggito al setaccio

Ogni container reale segnalava che la copertura del suo backup non era accertabile. La configurazione si leggeva bene: il filo diceva null, lo schema assente.

Di Provibr

Stavamo costruendo la scheda hardware per i container quando un controllo nel browser ha prodotto una frase che non avrebbe mai dovuto produrre: non è stato possibile leggere la configurazione di questo container, quindi non è stato accertato che cosa contenga un backup.

La configurazione era stata letta. Stava nella risposta, completa, con dentro ogni punto di mount. A fallire era stata la nostra analisi, un livello più avanti, e il messaggio dava la colpa al lato sbagliato del filo.

Risalire da lì ha richiesto un po’, perché non c’era niente da cui risalire. Nessuna eccezione, nessuna riga di log, nessun test fallito. Solo una frase pacata su uno schermo che diceva che una cosa non era stata misurata.

Una chiave assente e un null non sono la stessa cosa

L’agente costruisce le righe dei punti di mount in Rust con serde_json::json!, e uno dei campi è un Option<String>: il percorso dentro il container, che viene da mp=/data nella configurazione.

Un Option che vale None viene serializzato lì come null. Non come una chiave mancante. È il comportamento corretto e documentato, ed è quello che chiunque legga quella macro si aspetterebbe se ci pensasse un secondo, cosa che nessuno aveva fatto.

Dall’altra parte lo schema diceva .optional(), e in zod un optional nudo significa che la chiave può essere assente. Non significa che il valore possa essere null. Sono due affermazioni diverse su JSON, e ne avevamo scritta una mentre ne inviavamo un’altra.

Il campo che ha fatto scattare tutto questo su ogni singolo container è quello che è strutturalmente sempre vuoto. Ogni configurazione di container ha una riga rootfs, e un rootfs per definizione non ha nessun mp=, perché è montato alla radice. Quindi la primissima riga di ogni osservazione proveniente da ogni container reale portava "mountPath": null.

Un solo campo sbagliato significa nessun dato

Se il fallimento si fosse fermato a quel campo sarebbe stato un difetto estetico. Non lo è stato, e la ragione sta nella forma dell’analisi più che nella forma dell’errore.

Convalidiamo l’intera osservazione come un solo oggetto. Un safeParse su un oggetto fallisce per intero; non esiste un risultato parziale. Quindi un solo campo rifiutato significava nessun elenco di mount, nessuna classificazione della copertura e nessuna anteprima di che cosa un ripristino andrebbe a sostituire. Tutto ciò che sta a valle non ha visto niente, e tutto ciò che sta a valle è scritto per essere onesto sul niente.

Quell’onestà è voluta ed è la ragione per cui il prodotto non Le dice che un backup è completo quando non ha guardato. Una configurazione di container con un rootfs illeggibile viene riportata come sconosciuta invece che come completa, proprio perché una misurazione vuota non possa mai essere travestita da misurazione buona.

Qui la protezione ha funzionato esattamente come era stata progettata e ha prodotto una frase che, nei suoi termini, era vera: non era stato accertato niente. La bugia stava un livello più su, nell’affermazione sul perché.

Niente era rosso, e la ragione è scomoda

Questo è stato rilasciato con la tornata che ha introdotto la copertura dei backup dei container, e i dati di semina di tre tornate sono passati sullo stesso codice senza che niente diventasse rosso.

La ragione è che le semine erano scritte a mano. Per mettere alla prova la tabella della copertura senza un Proxmox reale si scrive una riga che assomiglia a quello che manda l’agente, e quello che si scrive quando si intende questa qui non ha percorso è omettere la chiave. Che viene analizzata senza problemi. Tre tornate di dati di prova hanno quindi codificato l’assunzione dello schema invece del comportamento dell’agente, ed erano tutte verdi.

Nel giro non c’è nessun hypervisor reale che lo corregga. Il Proxmox a cui abbiamo accesso è la macchina di produzione di qualcuno, e nessun nostro agente ci va sopra. Quindi l’unica descrizione del filo su cui i due lati avrebbero potuto essere verificati era il sorgente Rust, e nessuno la stava leggendo.

La protezione legge il produttore invece di copiarlo

La riparazione è un campo solo. La parte interessante è il test che le sta accanto.

Apre lxc.rs, trova la funzione che costruisce le righe, estrae i nomi delle chiavi dal blocco json! e poi afferma due cose. Primo, che l’insieme delle chiavi che l’agente manda è esattamente l’insieme delle chiavi che lo schema conosce, così nessuno dei due lati può farsi crescere un campo da solo. Secondo, ed è questo che conta, che una riga in cui ogni campo opzionale vale null passa il setaccio.

Quel secondo test costruisce la sua riga dalle chiavi che ha appena letto, non da un elenco scritto nel file di prova. Un nuovo campo opzionale aggiunto con un .optional() nudo è rosso il giorno stesso in cui viene aggiunto, senza che nessuno debba ricordarsi di questo articolo.

C’è un terzo test per la direzione opposta: una riga proveniente da un agente più vecchio, che non porta affatto i campi più recenti, viene comunque analizzata. Entrambe le forme sono reali, stanno tutte e due nella banca dati, e una correzione che accettasse solo la nuova avrebbe rotto le righe vecchie pur sembrando giusta.

La correzione in sé è un preprocess che mappa null su undefined invece di un .nullable(). È una scelta voluta sul tipo derivato: con nullable il tipo diventa string | null | undefined e ogni lettore guadagna un terzo stato che per lui non significa niente. Con preprocess il tipo resta com’era, i quattro lettori tengono il codice che avevano già, e la tolleranza sul filo è espressa in un punto solo.

Due errori nella misurazione, non nel prodotto

Accertare che la riparazione funzionasse ha prodotto due verdi falsi tutti suoi, e sono la metà più trasferibile di questa tornata.

Il primo: uno schema scritto con .strict() ha rifiutato in silenzio la nostra stessa semina, e lo schermo è rimasto vuoto invece che sbagliato. Uno schermo vuoto durante una riparazione ha esattamente lo stesso aspetto di uno schermo che non si è ancora caricato.

Il secondo è peggio ed è una trappola nota da queste parti, in cui siamo caduti lo stesso. La tabella dei backup vive in un pannello che è sempre montato nel documento, perché è così che si fa funzionare il ritorno del fuoco di una finestra di dialogo. Quindi un controllo che legge la tabella con un selettore la legge che sia aperta o no. La nostra asserzione che due schede dicessero la stessa cosa passava su due tabelle entrambe vuote ed entrambe invisibili.

Entrambi sono stati chiusi allo stesso modo: pretendere che la finestra di dialogo sia davvero aperta e mettere un limite inferiore al numero di righe prima di confrontarle. Un confronto del niente con il niente non è un confronto.

Che cosa annoteremmo

  1. Una chiave assente e un null sono affermazioni diverse, e i serializzatori JSON scelgono al posto Suo.
  2. Uno schema che accetta solo una chiave mancante non ha mai letto il suo produttore.
  3. La convalida dell’oggetto intero trasforma un campo sbagliato in nessun dato, che è la progettazione giusta e un fallimento brutto da diagnosticare.
  4. Il messaggio d’errore più pericoloso è quello pacato. Il nostro era onesto sull’esito e sbagliato sulla causa.
  5. I dati di prova scritti a mano codificano le Sue assunzioni, compresa quella che sta per sbagliare.
  6. Lasci che la protezione legga il produttore. Un elenco di chiavi copiato in un test va alla deriva; un elenco di chiavi estratto dal sorgente non può.
  7. Provi esplicitamente la riga tutta a null, perché è la riga che il Suo produttore reale manda per prima.
  8. Accetti anche la forma vecchia. Le righe scritte da agenti più vecchi stanno ancora nella banca dati.
  9. Tolleri sul filo, non nel tipo. Allarghi al confine e lasci stare i lettori.
  10. Un pannello sempre montato rende un controllo sul DOM verde a vuoto. Affermi che è aperto e che ha delle righe.

Per chiarezza su che cosa sia: il difetto stava nella copertura di ogni container reale a partire dalla tornata che ha rilasciato i backup dei container, trovato da un controllo nel browser e non da una suite qualsiasi, riparato con un solo preprocess e protetto da un test che legge il sorgente Rust; la tornata in cui è stato trovato ha portato il modulo di prova dei container da trentacinque test a trentanove, con musl e formattazione puliti, e si è chiusa con i pacchetti condivisi a 5347 verdi, il gateway a 1467, gli strumenti di stub a 489 e la suite web a 4593, senza fallimenti da nessuna parte, e cinquanta controlli nel browser verdi in due lingue senza rilievi di accessibilità. Non dimostrato: niente di tutto questo è girato contro un Proxmox reale, quindi la forma del filo è accertata dal sorgente e non da una risposta dal vivo, e la catena in funzione è stata messa alla prova contro il nostro stub.

Dal lavoro di piattaforma su Provibr. A che cosa serve la tabella della copertura si trova in cosa non contiene un backup, la tornata che per prima ha letto una configurazione di container si trova in reinstallare un container è una sostituzione, e un altro controllo che era verde perché non è mai arrivato si trova in verde perché non poteva arrivarci.

Inizi con un host e un agente

Crei un account, installi l’agente con un solo comando e colleghi il suo primo host Proxmox. Il periodo di prova dura 14 giorni e non chiede alcun dato di pagamento.