Salta al contenuto

Blog ·

Perché un 200 di Proxmox non è un esito

Proxmox risponde a una clonazione con un UPID e tira dritto. Come leggere quell’identificatore, interrogarlo con onestà e distinguere finito da riuscito.

Di Provibr

Si invia con POST una richiesta di clonazione a un nodo Proxmox. Si riceve un HTTP 200 e una stringa che sembra rumore di linea. In quell’istante è vera esattamente una cosa: la richiesta è stata accettata. Nessun disco è stato copiato, nessuna macchina esiste, e nulla è ancora fallito.

Quasi ogni chiamata che modifica qualcosa nell’API di Proxmox funziona così, ed è una progettazione perfettamente ragionevole per operazioni che durano minuti. Diventa un problema solo quando il codice chiamante tratta il 200 come un esito, che è il comportamento predefinito di ogni client HTTP mai scritto.

Ecco come leggiamo quei task, e quali parti si sono rivelate più taglienti di quanto sembrino.

La stringa è un identificatore, ed è strutturata

Ciò che torna indietro è un UPID: un identificatore separato da due punti che nomina un singolo task in esecuzione. Trasporta, fra gli altri campi, il nodo su cui il task sta girando, il tipo di task, l’oggetto su cui opera e l’utente che l’ha avviato.

Tre di questi sono portanti.

Il nodo è quello a cui bisogna chiedere lo stato. Un task appartiene al nodo che lo esegue, e una domanda rivolta all’intero cluster non è la stessa domanda. Prenda il nodo dall’identificatore anziché dalla variabile che si è trovato per caso a disposizione, perché le due cose possono differire e il caso in cui differiscono è precisamente quello che non ha provato.

Il tipo dice che cosa è stato davvero avviato, che non è sempre ciò che si è chiesto per nome. Un backup avviato attraverso l’endpoint del nodo torna con un tipo di task vzdump, non con qualcosa che abbia la forma di un guest.

Il bersaglio è l’identificatore del guest per le operazioni sui guest, e può essere vuoto. Non è corruzione. Un task può legittimamente riguardare un nodo anziché un guest, quindi il codice che analizza questo campo deve restituire «nessun guest» invece di sollevare un’eccezione o ripiegare su zero.

Il nostro parser è deliberatamente ristretto su ciò che rifiuta: richiede il prefisso, richiede almeno otto campi separati da due punti e richiede che il nodo e il tipo non siano vuoti. Tutto il resto può mancare, perché il formato deve continuare a funzionare per tipi di task che non abbiamo mai visto.

Finito non è la stessa cosa di riuscito

Se si chiede a un nodo lo stato di un task si ottengono due campi che rispondono a due domande diverse. Uno dice se il task è ancora in esecuzione o si è fermato. L’altro, presente solo una volta che si è fermato, dice OK oppure porta una stringa di errore.

Scriverli come due predicati distinti anziché come uno solo non è pedanteria. Un unico isDone() che li confonde produce il bug in cui una clonazione fallita viene riferita come clonazione conclusa, e la macchina che non esiste ottiene una riga nel suo database. Noi abbiamo due funzioni: una chiede se si è fermato, l’altra chiede se si è fermato e lo stato di uscita è esattamente OK. Qualunque altra cosa è un fallimento, compresi gli stati di uscita che non abbiamo mai visto, perché uno stato che non riconosciamo non è il permesso di presumere il successo.

Dove vive il task fa parte dell’API, non è un dettaglio

Questa ce l’hanno insegnata i backup. Viene naturale aspettarsi che un’operazione su un guest penda dal guest: qualcosa sotto il percorso della macchina virtuale stessa. Un endpoint del genere non esiste. Un backup si avvia sul nodo, con l’identificatore del guest come parametro.

Non è una stranezza da aggirare. Cambia ciò che bisogna sapere prima di poter partire: quale nodo, quale storage su quel nodo, e se quello storage possa perfino contenere backup. Uno storage che accetta soltanto immagini disco accoglierà volentieri la richiesta e fallirà dentro il task, il che arriva al cliente come un messaggio di errore da un programma di cui non ha mai sentito parlare. Perciò filtriamo l’elenco delle destinazioni in base a ciò che lo storage dichiara di poter contenere, ed escludiamo qualunque storage elencato ma che non risponde, prima di offrire una scelta a chiunque.

La stessa regola al contrario vale per collocare un disco. Stessa idea, tipo di contenuto opposto, e conviene tenerli come due filtri espliciti anziché come uno solo, astuto e condiviso.

Il polling è un ritmo, non un intervallo

Mentre il task gira bisogna chiedere ripetutamente. Le scelte ingenue sono cattive entrambe: interrogare in fretta per sempre significa martellare per un’ora l’API di gestione di qualcun altro, interrogare lentamente fa sembrare fiacca ogni operazione rapida.

Il nostro compromesso sono tre tentativi rapidi a un secondo di distanza, poi uno ogni tre secondi. L’avvio di una macchina finisce dentro la finestra rapida e sembra istantaneo. Un backup lungo un’ora costa una richiesta ogni tre secondi, che non è nulla.

Il ciclo termina in esattamente tre modi, e tutti e tre sono espliciti: il task riferisce di essersi fermato, la scadenza del comando passa, oppure l’intero comando viene annullato. Non c’è un quarto ramo né un percorso in cui il ciclo semplicemente prosegue, che è una proprietà da predisporre di proposito anziché da sperare. Che cosa una scadenza fermi e che cosa non fermi è un argomento a sé; qui basta dire che il ciclo di polling deve avere un modo per perdere.

La maggior parte dei task non dirà a che punto è

È la parte che delude chiunque stia costruendo una barra di avanzamento. Lo stato del task dà in esecuzione oppure fermo. Non dà una percentuale.

Alcuni task scrivono l’avanzamento nel proprio log, come testo. Una clonazione emette righe con un conteggio dei byte trasferiti e una percentuale fra parentesi. Per quelli, quindi, leggiamo la coda del log e prendiamo l’ultima percentuale che vi troviamo. Estrarre una percentuale da una riga di log non è elegante, ed è l’unico posto in cui quel numero esiste.

Per tutto il resto mostriamo una stima, e qui la scelta di progettazione conta più della formula. La nostra si avvicina asintoticamente al novanta per cento a partire da una durata attesa, così la barra si muove sempre e non raggiunge mai la fine. Una barra ferma al 100 per cento mentre non accade nulla è peggio di nessuna barra, perché dice a chi guarda che si è finito, e poi si contraddice.

La durata attesa che alimenta quella curva è un parametro di forma, non una scadenza. La nostra dice che un backup di solito richiede una decina di minuti e un ripristino una ventina, sul ragionamento che un ripristino legge l’archivio e scrive i dischi e quindi sposta all’incirca il doppio dei byte. Se un backup dura più della stima, non si rompe nulla; la curva resta semplicemente, con garbo, sotto la linea.

L’errore sta nel log, non nello stato

Quando un task fallisce, lo stato di uscita è spesso quasi inutile. unable to create VM è una categoria, non una causa. La causa sta nelle ultime righe del log del task.

Quindi il rapporto di fallimento viene assemblato: lo stato di uscita, più la coda del log con le righe di avanzamento filtrate via, limitata a una manciata di righe e a poche centinaia di caratteri. Filtrare le righe di avanzamento conta più di quanto sembri, perché una clonazione che fallisce può terminare con venti righe di percentuali di trasferimento e il motivo vero sopra di esse.

Una regola su quel testo, alla quale ci atteniamo rigorosamente: è una citazione. È ciò che ha detto l’hypervisor, e finisce nei nostri registri e sullo schermo senza modifiche. Non lo traduciamo, non lo mettiamo in ordine e non lo parafrasiamo in qualcosa di più amichevole. La frase che gli sta intorno è nostra e viene tradotta per il lettore; la parte citata resta esattamente come l’ha detta l’altra parte, perché è l’unica stringa dell’intero scambio che un tecnico dell’assistenza possa cercare.

Non pieghi le sue parole dentro la frase della macchina

L’ultima lezione è quella che non ci aspettavamo di imparare da un poller di task.

Il nostro ciclo di attesa prendeva un’etichetta di testo libero per l’operazione e la piegava dentro tre frasi: qualcosa in corso, qualcosa concluso, qualcosa fallito. Funzionava, finché qualcuno non ha notato che parecchie di quelle etichette erano state scritte nella lingua del nostro ufficio anziché in inglese. I clienti leggevano frasi tradotte a metà nelle loro barre di avanzamento e, quando un’operazione falliva, quella frase tradotta a metà veniva conservata per sempre nel campo di errore.

La correzione non è stata correggere le etichette. È stata togliere la possibilità: l’operazione ora è un’enumerazione, e ogni caso porta identificatori per «in corso», «concluso» e «fallito» anziché parole. Non è rimasto testo da sbagliare, e la frase viene costruita nella lingua del lettore proprio alla fine. Mantenere onesti quei cataloghi si rivela una disciplina a sé.

La lista di controllo

Per qualunque API che risponde a una modifica con un identificatore anziché con un esito:

  1. Analizzi l’identificatore, non si limiti a conservarlo. Le dice dove chiedere, e il dove non è sempre il posto in cui ha chiesto.
  2. Separi il finito dal riuscito, nel sistema dei tipi se può.
  3. Tratti come fallimento uno stato terminale non riconosciuto, mai come successo.
  4. Dia al ciclo di polling un ritmo e un modo garantito di terminare.
  5. Mostri l’avanzamento che ha davvero, e una stima onesta che non si possa mai scambiare per avanzamento reale.
  6. Citi l’altra parte alla lettera negli errori, e costruisca la sua frase attorno alla citazione anziché dentro di essa.

Nessuna di queste cose è difficile. Tutte e sei sono cose che all’inizio abbiamo sbagliato.

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.