Blog ·
Un flusso è un grafo, e un grafo dev’essere finito
Trasformare un elenco di passi in un editor a nodi sembra un cambio di interfaccia. È una decisione sui cicli, sui rami non presi e su un clic rubato.
Di Provibr
Un flusso nel nostro pannello fa una sola cosa, molto stretta: quando un server ha finito di essere allestito, produce testo. Un messaggio di benvenuto, un frammento di configurazione, una nota per il ticket. Non riavvia nessun hypervisor e non parla con nessuna API. È proprio questa ristrettezza a rendere la funzione consegnabile a un cliente senza timori.
Fino a questa tornata un flusso era un elenco: passo uno, passo due, passo tre, e arresto al primo errore salvo che una casella dicesse altro. La richiesta era di farne una tela, con nodi e collegamenti. Sembra un modo diverso di mostrare gli stessi dati. Non lo è, e la parte interessante sono le domande che una tela costringe a risolvere e a cui un elenco rispondeva già da solo.
La versione uno non ha cicli, e non è pigrizia
Un collegamento che torna indietro è la prima cosa che chiunque prova. Lo rifiutiamo al salvataggio, e il messaggio nomina i nodi che formano il ciclo.
La ragione sta nella natura del sottosistema e non nella difficoltà del codice. Un flusso di rendering non ha uno stato di attesa. Non c’è un nodo di ritardo, non c’è attesa di approvazione, non c’è una controparte che riferisca più tardi; ogni nodo è sincrono e finisce in pochi millisecondi. Un ciclo senza stato di attesa è quindi infinito per costruzione, e l’unico modo di domarlo sarebbe un contatore di iterazioni nascosto: un limite invisibile che nessuno vede e che, il giorno in cui viene raggiunto, taglia a metà un’esecuzione senza che nessuno sappia spiegare perché.
Rifiutare il ciclo al salvataggio è la versione onesta dello stesso limite. Non appena esisterà un vero stato di attesa il bilancio cambierà, e un ciclo diventerà una scelta di progetto con una ragione dietro. Oggi sarebbe una trappola.
La diramazione è sequenziale per un motivo affine. Due collegamenti che partono dalla stessa porta vengono seguiti uno dopo l’altro, nell’ordine salvato, e il primo ramo viene percorso fino in fondo prima che cominci il secondo. Renderli in parallelo significherebbe che due rami scrivono nelle stesse variabili nello stesso momento, e allora l’esito di un flusso dipenderebbe da quale rendering ha finito prima per caso. Un rendering costa millisecondi: non c’è nulla da guadagnare e c’è riproducibilità da perdere.
Sei tipi, e porte che devono essere una funzione
Ci sono sei tipi di nodo, volutamente pochi: attivatore, script, deviazione, assegnazione, nota e fine. Ogni tipo in più è una voce di tavolozza, un pannello di proprietà, nove traduzioni, un ramo nell’esecutore e una riga nella tabella delle transizioni, perciò un test ricava quei requisiti dall’elenco stesso. Un settimo tipo senza pannello è un test rosso e non un nodo a metà.
| Tipo | Uscite |
|---|---|
trigger | una, e c’è esattamente un attivatore per grafo |
script | ok ed error |
switch | una per regola, più otherwise |
set | una |
note | nessuna, e non viene mai visitato |
end | nessuna, e dichiara l’esito |
La riga della deviazione è il motivo per cui le uscite le calcola una funzione anziché leggerle da una tabella. Una deviazione con tre regole ha quattro uscite; una tabella fissa lascerebbe aggiungere una quarta regola senza che compaia un’uscita, e quel ramo sparirebbe in silenzio. Tutto ciò la cui forma dipende dalla configurazione non si descrive con una tabella fissa.
La convalida procede in ordine fisso e ogni rifiuto nomina il suo nodo: identificatori doppi, zero o due attivatori, un percorso di variabile fuori dal catalogo, un modello pericoloso, una porta sconosciuta, un collegamento doppio, un ciclo, un nodo irraggiungibile e infine più nodi eseguibili del limite di venticinque. L’ordine conta, perché il primo rifiuto è quello che si legge, e «qui c’è un ciclo» aiuta più di «questa porta non esiste» quando entrambe le cose sono vere.
Scrivere in state. e mai in flow.
Un nodo set scrive variabili. Lo spazio dei nomi ovvio è quello che esiste già, ed è quello sbagliato. flow.name, flow.runId e flow.step portano fatti sull’esecuzione, quindi un nodo autorizzato a scrivere lì lascerebbe a un cliente la possibilità di sovrascrivere flow.name, e la traccia di controllo mentirebbe in una forma che sembra un dato.
Perciò set scrive in state., uno spazio dei nomi nuovo, vuoto all’inizio e che esiste solo per la durata di un’esecuzione. Un modello che legge {{ state.role }} senza che nessun nodo l’abbia impostato fallisce come fallisce ogni altra variabile mancante, ed è esattamente ciò che si vuole: una modalità rigorosa è un messaggio d’errore migliore di una riga vuota in un messaggio di benvenuto.
Vince la prima regola, e il ramo che nessuno ha preso
Una deviazione legge un percorso di variabile e valuta le sue regole dall’alto in basso. La prima regola che corrisponde decide la porta; se nessuna corrisponde, l’esecuzione esce da otherwise. Otto operatori, non i trenta di uno strumento generalista, e tutti e otto si spiegano in una riga.
Ciò che ha contato più della valutazione è ciò che resta scritto. Ogni nodo visitato produce una riga con il suo identificatore, il suo tipo e il suo esito, e per una deviazione quell’esito è quale porta è stata presa e quale valore è stato letto. Non poteva finire nel campo di errore, che porta la ragione per cui qualcosa è fallito o è stato saltato, né nell’istantanea del contesto, che è l’ingresso di un rendering e porta una promessa.
E ogni nodo eseguibile non visitato produce anch’esso una riga, con la propria ragione. «Un passo precedente è fallito, quindi questo non è stato eseguito» e «l’esecuzione ha preso un altro ramo, quindi questo nodo non è stato raggiunto» sono due frasi diverse, e distinguerle è esattamente ciò che si fa leggendo un’esecuzione sorprendente.
Un modello che non si può interrompere
Un operatore della deviazione valuta un’espressione regolare, ed è l’unico pezzo davvero pericoloso della funzione. JavaScript non offre alcun modo di interrompere un RegExp.test(). Non c’è scadenza né interruzione, e il motore torna indietro: un modello come (a+)+$ su quaranta caratteri blocca per minuti un gestore di richieste. È un blocco del servizio con un solo campo di immissione.
Il lato agente ha risolto lo stesso problema in una tornata precedente limitando il motore, con un’implementazione senza ritorno all’indietro e con un limite di memoria. Quella strada qui non esiste, quindi si limita invece il linguaggio a un sottoinsieme in cui il disastro non è esprimibile: niente riferimenti all’indietro, niente lookaround e altezza di stella uno, cioè una ripetizione non può stare dentro una ripetizione. (a+)+ diventa così impossibile e a+b+ resta permesso. Con un limite di lunghezza sul modello e sull’ingresso, il caso peggiore è quadratico su qualche migliaio di caratteri, cosa che un nucleo moderno sbriga senza accorgersene.
La vista a elenco non è quella minore
Ogni azione della tela ha un pulsante in una vista a elenco costruita sullo stesso modello puro: aggiungere, selezionare, spostare, eliminare, collegare e scollegare. L’ordine lì è topologico, cioè l’ordine in cui l’esecutore visita i nodi e quindi l’unico che un lettore senza disegno possa seguire; a una diramazione i rami stanno uno sotto l’altro, con i loro collegamenti scritti a parole.
Non è un’appendice di accessibilità avvitata a un disegno. È il motivo per cui sotto la tela non c’è alcuna libreria di grafi. Una libreria sa rendere i nodi raggiungibili col fuoco e spostarli con le frecce, ma nessuna traccia un collegamento con la tastiera: quella seconda strada doveva esistere comunque. Una volta che esiste, alla libreria resta solo il lavoro di mouse, e con venticinque nodi quella è la parte piccola.
Una riga di codice che mangiava ogni clic
La tela si spostava bene, trascinava bene e non selezionava nulla. Fare clic sull’uscita ok di un nodo non produceva assolutamente niente, e nel DOM non c’era nulla di sbagliato: il gestore era attaccato, l’elemento c’era, le coordinate erano giuste.
setPointerCapture() su un antenato rimanda ogni evento di puntatore successivo all’elemento che cattura, compreso il pointerup. Il browser lancia poi il click sull’antenato comune fra pressione e rilascio, e con la cattura attiva quell’antenato è il contenitore stesso. Il gestore di clic del nodo non viene mai raggiunto, e da nessuna parte compare un errore.
La riparazione è strutturale e non un interruttore: lo spostamento pende ora da uno strato di sfondo posto dietro i nodi, così una pressione su un nodo non può più avviare la cattura.
C’è una seconda lezione in quanto poco è mancato perché passasse inosservato. Il controllo del browser per «è stato aggiunto un collegamento» contava elementi svg path, e ogni icona dell’interfaccia è uno di quelli: il numero saliva sia che comparisse un collegamento sia che non comparisse. Ora conta i percorsi dentro lo strato dei collegamenti, contro una quantità esatta.
Cosa metteremmo per iscritto
- Rifiutare il ciclo invece di limitare le iterazioni. Un limite nascosto è una trappola; un rifiuto al salvataggio che nomina i nodi colpevoli è un messaggio.
- Rendere le porte una funzione del nodo. Ciò la cui forma dipende dalla configurazione non si descrive con una tabella fissa.
- Dare alle variabili nuove uno spazio dei nomi nuovo. Lasciare che l’immissione dell’utente scriva sui campi che descrivono l’esecuzione trasforma la traccia di controllo in un racconto.
- Registrare il ramo non preso, con la sua ragione. «Non raggiunto» e «non eseguito dopo un errore» rispondono a domande diverse.
- Se il motore non si interrompe, limitate il linguaggio. Altezza di stella uno più un limite di lunghezza trasforma un’esplosione esponenziale in aritmetica.
- Costruite prima la via da tastiera se è un requisito. Una volta che c’è, spesso si scopre che la libreria faceva solo la metà facile.
- La cattura del puntatore ruba i clic ai discendenti. Se una tela trascina bene e non seleziona nulla, quello è il primo posto da guardare.
Per chiarezza su cosa sia tutto questo: i flussi vivono nella schermata degli script, oggi volutamente assente dal menu laterale, quindi sono raggiungibili e non pubblicizzati. Tre cose non sono dimostrate. La conversione da grafo a elenco è stata verificata nel codice e mai guidata in un browser. Il ramo di installazione via rete di un flusso a grafo non è stato percorso da capo a fondo. E un’esecuzione di prova è sincrona, quindi non c’è alcun evento in diretta da emettere.
Dal lavoro di piattaforma di Provibr. Perché un passo che non ha fatto nulla in silenzio è peggio di uno fallito è spiegato in la migrazione che non è mai partita, e come una sola vista di avanzamento legge tabelle altrui senza mai scriverci è spiegato in proiettare, mai sostituire.