Salta al contenuto

Blog ·

Verde perché non poteva arrivarci

Il nostro stub rispondeva «non supportato» al comando più ordinario che esista. Ogni test che ne dipendeva era verde per la ragione sbagliata.

Di Provibr

Teniamo un agente stub. Parla lo stesso protocollo di quello vero, gira accanto al gateway ed esiste perché il lato piattaforma di ogni comando si possa collaudare senza toccare l’hypervisor di un cliente. È una prova di contratto con attorno un processo.

Fino a questa settimana quello stub rispondeva al comando più ordinario che esista, spegni quella macchina, con COMMAND_STATUS_UNSUPPORTED.

Nessuno se n’era accorto, e la ragione per cui nessuno se n’era accorto è il punto di questo articolo. Ogni test che inviava allo stub un comando di alimentazione era verde. Erano verdi perché il comando veniva rifiutato, la piattaforma gestiva correttamente il rifiuto e l’asserzione finale reggeva. I test non mentivano. Descrivevano un percorso che nessuno intendeva esercitare.

Che cosa fa un ramo mancante ai test che gli stanno attorno

Il motore dei flussi ha un nodo di azione: esegui un comando contro un server, poi segui il filo ok o il filo error a seconda di come è andata. Quel nodo stava nel prodotto dal turno che lo aveva introdotto, e aveva dei test.

Quei test potevano raggiungere soltanto il filo error. Non per un difetto al loro interno, ma perché l’unico agente a loro disposizione rifiutava ogni comando che potessero inviare. Un test che asserisce «è stato preso il ramo di errore» passa benissimo quando l’errore è stato fabbricato dall’impalcatura di prova invece che dalla cosa sotto esame.

È la forma contro cui mettono in guardia i nostri stessi appunti, in una frase che sta nel manuale di progetto da mesi: un test può essere verde perché non riesce a raggiungere il percorso buono. La variante che ci ha colpiti qui è peggiore di quella solita, perché non si tratta di un ramo che non è mai stato scritto. Il ramo era scritto, funziona, e per tutto il tempo gli è stato accanto un test superato.

Lo stub doveva tenere uno stato, non solo rispondere

Aggiungere i cinque comandi di alimentazione non era questione di restituire successo. La piattaforma decide che cosa sia permesso fare a un comando leggendo lo stato attuale del server: non si può riprendere una macchina in esecuzione, non si può avviarne una già accesa. Quella decisione vive nella piattaforma, e legge una colonna.

Un agente che risponde «fatto» senza cambiare nulla lascia quella colonna dov’era. Un test può quindi inviare esattamente un comando e poi si blocca, perché il secondo comando viene rifiutato dalla macchina a stati della piattaforma stessa, correttamente, su uno stato che non si è mai mosso.

Lo stub tiene dunque uno stato di alimentazione per macchina e rimanda indietro l’osservazione di stato sulla sessione, nello stesso modo in cui lo fa un agente vero. È quello che mancava davvero. Non una risposta: una conseguenza.

Con quello a posto, un flusso che cicla su un elenco ed emette due comandi per passata ha potuto essere eseguito contro lo stub per la prima volta. Sei comandi, tutti arrivati a conclusione, e un ciclo che ha raggiunto la propria seconda passata perché la prima aveva davvero cambiato qualcosa.

Due cose che lo stub di proposito non fa

Non rifiuta le transizioni impossibili con un codice di errore inventato. Il confine fra permesso e non permesso vive nella piattaforma, e i codici di motivo dell’agente sono un elenco fisso compilato nel sorgente dell’agente vero. Inventare un codice che l’agente vero non ha significherebbe collaudare un contratto che non esiste. Uno stub più severo della cosa che sostituisce insegna cose che non sono vere.

Non simula nemmeno la durata. Uno spegnimento su un ospite vero attende che il sistema operativo risponda e ripiega su un arresto forzato dopo un tempo di attesa. Nello stub tutto avviene dentro il tick. Ma il tempo di attesa e il contrassegno di ripiego vengono portati fino all’uscita, così che un test possa vedere che sono arrivati anche se nessuno li ha attesi. Far passare un parametro e onorarlo sono affermazioni diverse, e se ne sta facendo una sola.

Una seconda sveglia rende casuale una misurazione

L’altra riparazione dello stesso turno riguarda la misurazione, non il prodotto.

Le esecuzioni in pausa vengono risvegliate da un worker nel gateway che interroga una coda ogni dieci secondi. Il nostro script end to end fa il risveglio da sé, nel proprio processo, così da poter asserire esattamente che cosa sia successo. I due vanno bene separati e sono catastrofici insieme: se contro lo stesso database di sviluppo gira un gateway con il risvegliatore attivo, questo mangia le righe non appena il loro orario di risveglio è passato, e lo script misura quello dei due che è arrivato per primo.

È successo. Un ramo dello script è fallito una volta senza avere nulla che non andasse, che è il genere di fallimento più costoso perché la reazione naturale è mettersi a cercare un difetto.

La soluzione non è un commento che dice «si assicuri che non giri alcun worker». È una funzione che lo misura. Prima di ogni altra cosa, lo script crea un’attesa usa e getta con un orario di risveglio già trascorso, la osserva per otto secondi e controlla se qualcun altro l’abbia toccata. Se qualcuno l’ha fatto, lo script rifiuta di girare e ne dice il motivo.

È poco codice per molta fiducia. Un presupposto scritto in un commento è un presupposto; lo stesso presupposto scritto come un controllo che gira per primo è una precondizione.

I nuovi tentativi hanno bisogno di un tetto e di un’attesa crescente

Mentre i percorsi buoni erano finalmente raggiungibili, anche i percorsi di fallimento sono stati misurati come si deve.

Un’attesa che solleva un’eccezione alla ripresa, perché un modello è stato cancellato o un server non esiste più, verrebbe altrimenti ritentata a ogni tick per sempre, in una tabella di coda che non si svuota mai. C’è un tetto di cinque tentativi con un’attesa che raddoppia: sessanta, centoventi, duecentoquaranta, quattrocentottanta secondi. Quei quattro intervalli sono stati letti da un’esecuzione vera invece che dalla funzione che li calcola. Al quinto tentativo l’attesa viene annullata e l’esecuzione viene segnata come fallita.

La controprova è la parte che conta. Con la protezione di esaurimento disattivata, la stessa esecuzione resta nello stato di attesa a tempo indefinito. Una protezione è dimostrata solo quando la situazione che impedisce è stata davvero prodotta.

Sotto di essa c’è un secondo modo di guastarsi. Un worker prende in carico un’attesa portandola fuori dallo stato di attesa prima di cominciare a percorrere il grafo, perché gli effetti collaterali di un flusso avvengono durante la visita e non dopo. Se il processo che l’ha presa in carico muore a metà visita, la riga resta per sempre nello stato di presa in carico, e l’interrogazione che cerca le attese scadute non la vede: chiede righe in attesa. Una passata di pulizia rimette indietro le prese in carico più vecchie di quindici minuti, e un solo tick contro un gateway davvero in funzione ne ha liberata una, risvegliata una e ripresa una, senza fallimenti.

Piccole cose che si trovano solo eseguendole

Un ritardo di tre secondi non produce un nodo non valido. Rende non valido l’intero grafo, perché il minimo è cinque, e l’esecutore non restituisce proprio nulla invece di un’esecuzione con un nodo rotto. È corretto ed è anche disorientante la prima volta, perché ci si aspettava un errore su un nodo e si è ottenuto silenzio su un grafo.

Le porte del gateway prendono il nome dal gateway. Impostare le variabili senza quel prefisso lo lascia in ascolto sui propri valori predefiniti, in silenzio, il che somiglia esattamente a un problema di rete.

Un arresto scrive nel registro come saltati i nodi che non ha mai raggiunto. È una differenza voluta rispetto al lasciarli fuori: un’esecuzione finita in anticipo e un’esecuzione che ha preso un percorso diverso si somigliano in tutto se i nodi non raggiunti sono semplicemente assenti.

E un nodo di approvazione lasciava righe orfane nella tabella delle notifiche, sedici sul database di sviluppo. La pulizia per questo è per esecuzione ed è basata sulla chiave di deduplicazione, mai su una corrispondenza per schema su un prefisso, perché una corrispondenza per schema porterebbe via righe appartenenti al turno di qualcun altro.

Che cosa annoteremmo

  1. Un test che non può raggiungere il percorso buono è verde per la ragione sbagliata. Si chieda che cosa farebbe quel test se la funzionalità venisse cancellata.
  2. Uno stub che rifiuta tutto non è uno stub, è un muro. Ogni test dietro di esso misura il rifiuto.
  3. Un oggetto fittizio deve tenere uno stato, non solo rispondere. Se la piattaforma decide su una colonna, qualcosa deve muovere quella colonna.
  4. Non renda uno stub più severo della cosa vera. Un codice di errore inventato collauda un contratto che nessuno implementa.
  5. Faccia passare un parametro anche quando non può onorarlo, così che un test possa dimostrare che è arrivato.
  6. Due risvegliatori su un solo database rendono casuale la misurazione. Uno dei due vince e non si può dire quale.
  7. Trasformi la precondizione scritta nel commento in un controllo che gira per primo. Allora è un fatto invece che una speranza.
  8. Dimostri una protezione disattivandola. Un tetto ai tentativi che nessuno ha mai raggiunto è un’affermazione.
  9. Una riga presa in carico è invisibile all’interrogazione che cerca il lavoro scaduto. Qualcosa deve rimettere indietro le prese in carico stantie.
  10. I nodi non raggiunti vanno nel registro come saltati, altrimenti un’uscita anticipata è indistinguibile da un percorso diverso.

Per chiarezza su che cosa sia: sette rami prima non dimostrati sono stati percorsi da capo a fondo, ciascuno con una controprova rossa; la verifica di merge ha eseguito 16 passi su 16 senza una riga di fallimento, la suite web è stata 4583 verdi, packages, gateway e tools insieme 7287 verdi, la suite propria dell’agente stub 481 verdi, lo script end to end 89 verdi, il ramo del risvegliatore 13 verdi, il ramo della posta di approvazione 22 verdi e il ciclo con azioni vere 18 verdi. Non dimostrati: non è stato eseguito nulla contro un Proxmox reale, un grafo illeggibile sulla riga di attesa stessa, un lotto più grande della dimensione del lotto e la consegna vera della posta via SMTP.

Dal lavoro di piattaforma su Provibr. Come la visita ha imparato a fermarsi e a continuare domani si trova in ogni chiamata ricorsiva era in posizione di coda, e dove passa la linea fra una pausa che una richiesta può assorbire e una che sospende l’esecuzione si trova in una pausa che teneva aperto l’ordine.

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.