La chiave, l'hash e il cursore
Questa è la seconda di quattro letture sullo stesso giro. La prima ha messo Espo da una parte e il canale dall’altra. Ha lasciato a questo capitolo la chiave, l’hash e il cursore. La terza prenderà la cella vuota e il record che si chiude. La quarta dirà che cosa resta spento. Qui si guarda come una riga viene riconosciuta, come il motore ricorda di averla già vista, e come ricorda fin dove è arrivata un’uscita.
Il file è lo stesso della prima lettura. È il file delle anagrafiche, di notte. Quando ha le colonne attese, C014 è invariata e Espo non viene chiamato. C001 è nuova: il nome è Nord Est, la città è Trieste, e la chiave finisce con C001. Quando alla diciottesima riga del racconto manca una colonna obbligatoria, il file non si sposta. Sono passaggi diversi. Vanno tenuti distinti, e più sotto si vedono uno per volta.
Se una frase di questo diario e una frase del manuale divergono, vale il manuale. Il manuale sta nella cartella docs del repository pubblico github.com/nessocrm/nessocrm, e cambia insieme al codice. Questo sito non lo ricompila. Il diario spiega il perché. Il manuale resta il testo che dice che cosa il programma fa oggi.
La chiave
L’identità di un record in ingresso è l’id dell’integrazione, due punti, il valore esterno. Il nome non è l’identità. La città non è l’identità. L’id che Espo assegna al record, dopo averlo creato, non è il valore che il file di notte già conosceva. La stringa è composta, e le due parti hanno due origini. L’id dell’integrazione lo tiene la regola che sta leggendo. Il valore esterno lo porta la riga, nella colonna che una persona ha indicato come identità.
Nel file di stanotte il valore esterno della riga nuova è C001. La chiave che si scrive sulla scheda finisce con C001. Prima dei due punti c’è l’id di questa integrazione, quella delle anagrafiche. Non è un id del collegamento, buono per tutte le regole attaccate allo stesso canale. Non è l’id di un’altra integrazione che, per caso, legge anch’essa una colonna di codici. La forma è questa, e dalla pagina non se ne sceglie un’altra. I due punti tengono insieme le due parti, così chi apre il record vede da quale regola è entrato e quale codice portava la riga.
La chiave si cerca e si scrive. In ingresso il motore guarda se quella stringa è già su un record della scheda. Se c’è, e la riga non è già stata salvata identica, il giro aggiorna. Se non c’è, il giro crea. L’hash può fermare il giro ancora prima: quando la riga è già quella salvata, Espo non viene chiamato. La chiave risponde a una domanda. Quale record è questo. L’hash risponde a un’altra. Questa riga è già stata vista così.
Due file possono portare lo stesso codice e restare due storie, perché a sinistra dei due punti l’id cambia con l’integrazione. Il primo capitolo ha anticipato la separazione. Qui si vede il carattere che la fa: i due punti, con un id vero a sinistra e il valore della colonna a destra. Senza l’id, C001 di un file e C001 di un altro file sarebbero la stessa stringa. Con l’id, la stringa intera cambia. L’aggiornamento dell’una lascia stare l’altra.
Dove finisce la chiave
Sulle schede standard il campo si chiama cExternalKey. Account, quando è la scheda standard che Espo ha già, tiene lì la chiave. Il nome comincia con la C minuscola perché è il modo in cui un campo custom sta su una scheda che esisteva già. Nesso, per quella scheda, usa quel campo. Non gliene affianca un secondo con un altro nome, da tenere «per chiarezza». Una chiave sola, un campo solo, sulla scheda standard.
Su una scheda creata da Nesso il campo si chiama externalKey. Manca la C iniziale. La scheda nuova ha già ricevuto da Espo il prefisso C sul nome dell’entità. I campi non ricevono un secondo prefisso. Il campo della chiave resta il nome dato, externalKey, senza un’altra C davanti. Le due grafie non sono due politiche. Sono la conseguenza di chi ha creato la scheda. Espo, per le schede che aveva già. Nesso, via API, per la scheda che prima non c’era, e anche in quel caso il prefisso sull’entità lo mette Espo.
Le due schede possono stare nella stessa installazione. Un Account standard cerca e scrive cExternalKey. Una scheda nata qui cerca e scrive externalKey. Il motore non copia il nome dell’una sull’altra. Chi legge un record guarda la scheda, e il nome del campo segue la scheda. La stringa dentro il campo ha la stessa forma in entrambi i casi: id dell’integrazione, due punti, valore esterno. C001, sulla scheda standard del racconto, finisce la chiave che abita in cExternalKey.
Una scheda che non c’era
Se la scheda non esiste, Nesso può crearla via API. Si indicano il singolare e il plurale. Sono i due nomi con cui la scheda viene chiamata, uno per il record e uno per l’elenco. Espo aggiunge il prefisso C. Il nome che resta in Espo comincia con quella C. I campi aggiunti alla scheda nuova non ricevono un secondo prefisso: restano i nomi indicati. La chiave, su quella scheda, è externalKey.
Indicare solo il singolare non basterebbe, e indicare solo il plurale nemmeno. Espo chiede i due nomi, e la creazione li porta tutti e due. Il prefisso non si scrive a mano davanti al singolare, sperando che Espo lo riconosca. Lo aggiunge Espo. Un secondo prefisso sui campi, messo «per essere coerenti» con la C dell’entità, produrrebbe nomi che la scheda non deve avere. I campi restano senza quella seconda C.
La scheda creata qui è una scheda Espo. La vede chi ha il team. Il file SQLite del motore non diventa il posto in cui quella scheda vive. Il primo capitolo ha già messo questo confine, e qui serve solo per non perdere la chiave: anche sulla scheda nuova la chiave è in Espo, nel campo externalKey, e l’hash resta nel motore. Creare la scheda cambia il nome del campo. Non cambia la forma del valore. Il valore resta id, due punti, valore esterno.
Un file di anagrafiche che punta ad Account e un file che punta a una scheda creata qui non si scambiano il nome del campo. Non si scambiano nemmeno l’id dell’integrazione. La prima chiave finisce nel cExternalKey dell’Account. La seconda finisce nell’externalKey della scheda nuova. Se i valori esterni coincidono, le stringhe intere coincidono solo quando anche l’id dell’integrazione coincide, cioè quando è la stessa regola. Due regole restano due chiavi.
Due regole sulla stessa scheda
Il primo capitolo ha lasciato il caso di due integrazioni che finiscono su Account. La chiave è il motivo per cui non si sovrascrivono. L’id dell’integrazione sta dentro la chiave. La chiave della prima contiene l’id della prima. La chiave della seconda contiene l’id della seconda. Anche se il valore esterno, per caso, è la stessa stringa, la chiave intera è diversa, perché a sinistra dei due punti ci sono due id.
C001, nel racconto, appartiene all’integrazione delle anagrafiche. La chiave finisce con C001 e comincia con l’id di quella integrazione. Un’altra integrazione può avere una riga il cui valore esterno è ancora C001. La chiave di quella riga finisce nello stesso modo e comincia in un altro. Su Espo l’aggiornamento dell’una non è l’aggiornamento dell’altra. Il motore non decide che i due C001 sono lo stesso cliente. La persona che ha fatto due integrazioni ha fatto due identità. Se un giorno vorrà un’identità sola, farà una integrazione sola, con una colonna sola.
Il collegamento può essere lo stesso, se i due file arrivano dallo stesso canale. Può essere diverso. La direzione, in ogni caso, segue il collegamento a cui l’integrazione è attaccata. Due integrazioni in ingresso sulla stessa scheda hanno ciascuna il proprio hash. Due integrazioni in uscita hanno ciascuna il proprio cursore. La chiave le tiene separate anche quando la scheda è una sola e il valore esterno, letto da solo, sembrerebbe unico.
Questo è anche il motivo per cui un collegamento solo, con una regola sola, non è la forma di due file. Il primo capitolo lo ha detto dal lato del confine. Qui si vede dal lato della stringa. Una colonna identità unica obbligherebbe i due codici a entrare nella stessa chiave, con lo stesso id a sinistra. Un codice dell’altro file verrebbe cercato, aggiornato o creato come se fosse di questa regola. Due id tengono due memorie. L’hash di C014 non è l’hash dell’altra integrazione, anche se stanotte i due file arrivano nella stessa cartella.
La direzione segue il canale
Un canale in ingresso porta righe verso Espo. I canali in ingresso sono il webhook, il database letto solo con SELECT, il CSV, l’Excel, l’SFTP, l’FTPS, il PDF il cui nome di file contiene la chiave dell’azienda, e il documento. Il database in sola SELECT può essere MySQL, MariaDB, Postgres, SQL Server o Oracle. La SELECT legge quel database. Le tabelle di Espo restano fuori da quella lettura: verso Espo si passa dalle API HTTP, e il capitolo torna su questo punto quando il giro scrive.
Il canale in uscita è HTTP. L’integrazione esce da Espo e manda i campi abbinati. Un CSV è ingresso. Un Excel è ingresso. Un HTTP di questo prodotto è uscita. Cambiare direzione significa cambiare collegamento. Il primo capitolo ha già detto la frase. Qui la frase decide quale memoria si usa. L’ingresso ricorda con l’hash. L’uscita ricorda con il cursore. Una integrazione in ingresso non consulta il cursore per sapere se C014 è invariata. Una integrazione in uscita non consulta l’hash della riga di un file per sapere se un record è già stato mandato.
Il PDF entra quando il nome del file contiene la chiave dell’azienda. È un ingresso, come il documento. L’SFTP e l’FTPS portano file, e la direzione è quella del collegamento. Un webhook che arriva è ingresso. La chiamata HTTP che Nesso fa verso l’esterno, sul collegamento di uscita, è uscita. Attaccare l’integrazione del file di notte a un collegamento HTTP la farebbe uscire da Espo. Attaccare un’uscita a una cartella la farebbe leggere file. L’errore, se c’è, è nella scelta del collegamento, ed è visibile prima del giro. Non è una casella, dentro la stessa regola, che rovescia il verso a metà strada.
La stessa installazione può avere i due versi insieme, su due integrazioni. Il file di notte entra. Un’altra regola, su HTTP, esce. Hanno id diversi, quindi chiavi diverse, quindi memorie diverse. Il successo dell’una non aggiorna il cursore dell’altra, e il cursore dell’una non segna l’hash del file. Alle 02:10 può partire l’ingresso, se quella è la sua cadenza. L’uscita parte secondo la propria. Se una delle due è già in corso, l’altra può comunque partire. La stessa, no.
L’hash, memoria di ingresso
L’hash è il ricordo della riga già vista, per quella integrazione e per quel valore esterno. Se l’hash della riga è già quello salvato, Espo non viene chiamato. Non parte la scrittura. Il record resta com’è. La riga è nel file, il motore la riconosce, e la chiamata non c’è.
Se la chiave esiste già su Espo e la riga non è quella già salvata, il giro aggiorna quel record. Se la chiave non esiste, il giro crea il record. Creare e aggiornare sono le due scritture. L’hash già salvato è l’assenza della scrittura. Un secondo invio identico non duplica. La seconda volta l’hash c’è, Espo non viene chiamato, e non nasce un secondo record con la stessa chiave.
L’ordine è questo. Prima l’hash. Poi, solo se la riga non è già salvata così, la chiave. Creare quando la chiave c’è aprirebbe un duplicato. Aggiornare quando la chiave non c’è non avrebbe un record su cui scrivere. Saltare la chiamata quando l’hash coincide evita di riscrivere ogni notte la riga che non è cambiata, solo perché il file è stato posato di nuovo nella cartella.
L’hash sta nel motore, insieme al resto della memoria del giro. La chiave sta sul record, nel campo della scheda: cExternalKey se la scheda è standard, externalKey se la scheda l’ha creata Nesso. I due posti non si scambiano. Su Espo arriva la chiave, quando il giro scrive. Nel motore resta l’hash, anche quando il giro non scrive. C014 di stanotte è il caso in cui nel motore l’hash c’è già e verso Espo non parte nulla.
Un hash per integrazione vuol dire che la stessa riga, letta da un’altra regola, ha un altro ricordo. L’altra regola ha un altro id, quindi un’altra chiave, quindi un altro confronto. Far condividere l’hash fra due integrazioni farebbe tacere la seconda solo perché la prima ha già visto un testo uguale. Non è il comportamento. Ogni regola ricorda le proprie righe.
C014 e C001, quando il file è quello atteso
Nel file di stanotte, quando le colonne obbligatorie ci sono, la riga del codice C014 è identica all’ultima volta. L’hash della riga è già quello salvato. Espo non viene richiamato. Non c’è un aggiornamento da mandare, e non c’è un motivo per toccare il record. Il file contiene la riga. Il motore la salta come chiamata. Il record su Espo resta quello che era.
La riga del codice C001 non c’è ancora. Il giro la prepara come un Account nuovo. Il nome è Nord Est. La città è Trieste. La chiave che nasce sulla scheda standard è l’id di questa integrazione, due punti, e C001. Finisce con C001. Il campo è cExternalKey. La strada è la creazione, perché la chiave non esiste. Non è un aggiornamento di un record trovato per nome. Nord Est è il nome che la riga porta. La domanda «esiste già un simile con un’altra chiave» è un altro caso, più sotto. In questo passaggio C001 è nuova, e il giro crea.
Un secondo invio della stessa riga C001, identica, non crea un secondo Account. L’hash ormai è salvato. Espo non viene chiamato. La chiave finisce ancora con C001, ed è la stessa chiave, perché l’integrazione è la stessa e il valore esterno è lo stesso. Non nasce una seconda volta un record la cui chiave finisce con C001 per questa regola.
Questa è la notte in cui il file ha la forma attesa. La diciottesima riga del racconto appartiene a un altro passaggio dello stesso file: manca una colonna obbligatoria, il file non si sposta, e le altre righe non vengono fatte passare a metà. C014 e C001 si vedono quando il file è quello atteso. La riga 18 si vede quando non lo è. Nello stesso passaggio non avvengono insieme. Se avvenissero insieme, o si caricherebbe un file troncato, o si butterebbe via anche la riga buona per colpa di una cella. Le due regole stanno più sotto, separate. Qui basta sapere che il racconto del file ha più di una notte, o più di una condizione, e che ciascuna condizione ha il suo gesto.
La creazione di C001 scrive la chiave nel campo. I campi abbinati portano il nome e la città: Nord Est, Trieste. Un campo non abbinato non riceve un valore inventato per riempire la scheda. L’uscita, quando sarà il momento del cursore, manderà allo stesso modo solo i campi abbinati. In ingresso, la scrittura verso Espo è la creazione o l’aggiornamento di ciò che la regola ha nominato, più la chiave nel campo della scheda. C014, invariata, non arriva nemmeno a quella scrittura.
Il cursore, memoria di uscita
In uscita non c’è un file da confrontare, riga per riga, con un hash di ingresso. C’è Espo. C’è un punto fino al quale l’invio è già riuscito. Il cursore ricorda l’ultimo modifiedAt e l’id Espo. I due valori insieme dicono fin dove il giro precedente è arrivato. Un record il cui modifiedAt è già dietro quel ricordo, e il cui id è già compreso, non esce di nuovo solo perché il pianificatore si è svegliato.
La lettura è a pagine di 50. Il giro chiede una pagina di record oltre il cursore, lavora quei record, e prosegue. Cinquanta è la misura della pagina. Serve a non chiedere l’archivio intero in una volta. L’id tenuto insieme al modifiedAt serve a non perdere i record che condividono lo stesso istante: il cursore sa l’istante e sa l’id, quindi un record con lo stesso modifiedAt e un id ancora da mandare resta nel lavoro della pagina, e non viene saltato perché «quell’orario è già stato visto».
Si mandano solo i campi abbinati. Un campo della scheda che l’integrazione non ha messo nell’abbinamento non entra nel corpo che parte. L’uscita non è una copia dell’Account. È l’elenco che una persona ha nominato. Il modifiedAt e l’id Espo servono al cursore per sapere dove si trova. Ciò che parte verso l’esterno, nel corpo del POST, sono i campi abbinati.
Il cursore avanza solo dopo i POST riusciti. Un POST che non è riuscito non è un punto superato. Il giro successivo ha ancora quel record da mandare, perché il cursore non gli è passato davanti. Avanzare prima della risposta sposterebbe il ricordo su un invio che non è avvenuto. Il record resterebbe fuori, e il motore sarebbe convinto di averlo già consegnato. Per questo l’avanzamento sta dopo il successo, non dopo la preparazione della pagina.
Una pagina di 50 può contenere record che escono e, se un POST non riesce, un punto in cui il cursore si ferma. I record il cui POST è riuscito possono essere ricordati fino all’ultimo successo. Il record il cui POST non è riuscito resta davanti al cursore. Il giro dopo lo ritrova. Non si marca l’intera pagina come fatta nel momento in cui la pagina è stata letta. La lettura prepara. Il POST riuscito fa avanzare.
Se in uscita manca la chiave esterna
Se manca la chiave esterna, il pull si ferma. I record che vengono dopo non escono. Non vengono saltati per proseguire con il resto della pagina. La mancanza è un buco nell’identità del record che si stava per mandare. Il giro di uscita si arresta lì. Il cursore non scavalca quel record per andare a prendere i successivi. Quei successivi restano da mandare, più avanti, quando la chiave c’è e un giro può passare oltre.
È una cautela diversa dall’hash. L’hash, in ingresso, fa tacere una chiamata perché la riga è già nota. Qui il cursore si rifiuta di andare avanti perché un record non ha l’identità con cui l’uscita lo riconoscerebbe. Mandare i successivi e lasciare il buco in mezzo farebbe un’uscita che sembra completa e non lo è. Il fermarsi è il gesto. Il giornale di quel giro dice che cosa è successo, nella lingua del giro. I record dopo non sono «già usciti». Non sono usciti.
La chiave di cui si parla, in uscita, è la chiave esterna del record su Espo. È la stessa forma già vista: id dell’integrazione, due punti, valore esterno, nel campo della scheda. Se quel valore manca sul record, il pull non ha un’identità da mandare avanti, e si ferma. Non inventa un valore esterno per sbloccare la pagina. Il record senza chiave resta il punto di arresto. I record dopo aspettano.
Questo arresto non è la coda di ritento. La coda resta spenta, ed è materia del quarto capitolo. L’uscita che si ferma per una chiave mancante non parcheggia i record dopo in una coda. Li lascia non usciti. Il cursore non li ha superati. Un giro successivo, quando la chiave c’è, può riprendere da dove l’uscita si era fermata, perché l’avanzamento avviene solo dopo i POST riusciti e il buco non è stato un POST riuscito.
Il secondo giro che esporta zero
Un secondo giro in uscita, se in Espo nessuno ha toccato quei record, esporta zero. Il modifiedAt non è andato avanti. Il cursore è già sull’ultimo modifiedAt e sull’ultimo id che erano usciti. Non c’è una pagina nuova da mandare. Zero non è un guasto del canale. È la memoria di uscita che ha fatto il suo lavoro. Il primo capitolo diceva che confondere hash e cursore è il modo in cui un’uscita rimanda gli stessi record all’infinito. Il secondo giro, a record fermi, mostra l’altra faccia: non rimanda.
Se qualcuno in Espo modifica un record, modifiedAt cambia. Il cursore, fermo all’istante precedente e all’id precedente, rivede quel record al giro dopo. Esce di nuovo, con i soli campi abbinati. Solo dopo il POST riuscito il cursore comprende il nuovo modifiedAt e l’id. Un tocco su un record non rifà uscire tutta la scheda. Esce ciò che sta oltre il cursore. Gli altri, non toccati, restano a zero in quel giro: non hanno un modifiedAt nuovo che li riporti davanti.
L’hash non entra in questo conto. L’hash è la memoria di ingresso. Il cursore è la memoria di uscita. Un file CSV non fa avanzare il cursore di un’integrazione HTTP. Un POST di uscita non segna l’hash di C014. Sono due ricordi. La stessa installazione può averli entrambi, su due integrazioni, e ciascuna ricorda solo il proprio verso. Alle sette del mattino, se l’uscita ha esportato zero, la frase giusta è che nessuno ha toccato quei record dopo l’ultimo cursore. Non è la frase «il file di notte era identico». Quella frase appartiene a C014, all’hash, all’ingresso.
Due memorie, due domande
Conviene tenerle affiancate, una volta, senza mescolarle.
L’hash domanda: questa riga in ingresso è già stata salvata così? Se sì, Espo non viene chiamato. Se no, si guarda la chiave. Chiave presente, aggiornamento. Chiave assente, creazione. Un secondo invio identico cade nella prima risposta e non duplica.
Il cursore domanda: questo record di Espo sta oltre l’ultimo modifiedAt e l’ultimo id già mandati con successo? Se no, non esce. Se sì, entra in una pagina di 50 e parte con i campi abbinati. Il cursore si sposta solo dopo il POST riuscito. Se la chiave esterna manca, la domanda si chiude: il pull si ferma e i record dopo non escono. Un secondo giro, senza tocchi su Espo, ha la risposta già pronta ed esporta zero.
Nessuna delle due domande sostituisce l’altra. Usare l’hash per l’uscita farebbe tacere un record modificato in Espo, se qualcuno confondesse il ricordo della riga con il ricordo dell’invio. Usare il cursore per l’ingresso farebbe rileggere C014 come «già mandata» o come «mai vista», a seconda di un modifiedAt che il file di notte non è. Il file ha colonne. L’uscita ha modifiedAt e id. Le memorie restano ciascuna sul proprio verso.
Il lucchetto, la cadenza e l’orologio, che vengono adesso, non sono una terza memoria dei dati. Sono la memoria di quando un giro può cominciare. L’hash e il cursore restano i ricordi di che cosa è già stato visto e di che cosa è già uscito. Un giro che non parte non consuma l’hash e non sposta il cursore. Un giro che parte li usa secondo il verso del canale.
La stessa integrazione non parte due volte
La stessa integrazione non parte due volte insieme. La seconda viene avvisata che un’esecuzione è già in corso. Non si aprono due giri dello stesso id che leggono lo stesso file e scrivono la stessa chiave. Due scritture contemporanee sulla stessa regola farebbero correre due confronti di hash, o due avanzamenti di cursore, verso lo stesso record.
Due integrazioni diverse possono girare insieme. L’id è diverso, la chiave è diversa, la memoria è diversa. Il file delle anagrafiche e un’altra regola non si aspettano per il solo fatto di essere due. Si aspettano solo quando l’id è lo stesso ed è già in corso. Un ingresso verso Account e un’uscita HTTP possono essere entrambi in esecuzione. Ciascuno tiene il proprio lucchetto. Ciascuno, alla fine, aggiorna solo la propria memoria: hash da una parte, cursore dall’altra.
Il pianificatore, se trova il lucchetto tenuto su un id, salta quell’id. Non scrive un errore ogni trenta secondi. Il lucchetto tenuto è una condizione ordinaria: un giro già partito tiene l’integrazione. Un errore ogni trenta secondi riempirebbe il giornale di una frase che non descrive un guasto. Il salto riguarda quell’id. Le altre integrazioni, se il loro lucchetto è libero e la loro cadenza è scaduta, partono.
Un avvio chiesto mentre la stessa integrazione è già in corso riceve l’avviso: un’esecuzione è già in corso. Non diventa un secondo giro parallelo. Quando il primo ha finito, il lucchetto si libera. Un avvio nuovo può cominciare, e userà l’hash o il cursore lasciati dal primo. Il pianificatore, nel frattempo, non ha riempito il giornale di quell’id.
Il salto del pianificatore e l’avviso alla seconda partenza sono due modi di dire la stessa esclusione, rivolti a due chiamanti. Il pianificatore non ha una persona in attesa: salta, e tace. L’altro avvio ha chiesto di partire: riceve la frase che un’esecuzione è già in corso. In entrambi i casi il secondo giro dello stesso id non comincia. L’hash non viene calcolato due volte in parallelo. Il cursore non avanza due volte in parallelo.
Cadenza, pausa, Esegui ora
La cadenza è spenta, oppure a mano; oppure cinque minuti; oppure quindici; oppure sessanta; oppure un orario fisso, come le 02:10. Spenta e a mano stanno dalla stessa parte: il pianificatore non sveglia da solo quella integrazione a intervalli. I cinque, i quindici e i sessanta sono intervalli. Le 02:10 sono un orario del giorno, non una durata di due ore e dieci minuti. Il file di notte sta bene su un orario fisso. Un webhook sta bene su un arrivo. La cadenza può restare a mano se l’arrivo stesso è ciò che sveglia la regola, e il webhook, se l’integrazione non è in pausa, può farla partire.
Un’integrazione in pausa viene saltata dal pianificatore e dal webhook. Alle 02:10 il pianificatore arriva, vede la pausa, e non parte. Un webhook che bussa durante la pausa non fa partire quella integrazione. Esegui ora non viene saltato. Una persona che chiede il giro lo ottiene anche con l’integrazione in pausa. La pausa ferma gli avvii che non hanno una mano sopra. Non ferma la mano.
La pausa non cancella l’hash e non rimette il cursore a zero. Li lascia dove sono. Un Esegui ora durante la pausa usa la stessa memoria. C014 invariata non chiama Espo. Un’uscita non rimanda i record già compresi nel cursore solo perché la regola era in pausa. La pausa è un salto degli avvii automatici. Non è una dimenticanza del giro precedente.
Pausa e lucchetto si sommano senza confondersi. La pausa salta pianificatore e webhook anche se nessun giro è in corso. Il lucchetto salta l’id, o avvisa la seconda partenza, quando un giro è in corso, anche se l’integrazione non è in pausa. Esegui ora attraversa la pausa. Esegui ora, se il lucchetto è tenuto, riceve che un’esecuzione è già in corso. Sono due porte. Una è la pausa. L’altra è il giro già aperto. Entrambe vanno guardate prima di credere che «alle 02:10 parte comunque».
I quindici minuti e l’orologio finto
I quindici minuti sono provati con un orologio finto. Un secondo prima della scadenza il giro non parte. Allo scadere parte una volta. Non riparte nello stesso minuto. Non è un quarto d’ora guardato sul contenitore, con l’orologio del muro e l’attesa vera. Il contenitore ha il suo tempo, e un quarto d’ora vissuto dal di fuori mescola il ritardo del processo con la regola. L’orologio finto mette l’istante dove serve. La regola risponde su quell’istante.
Si prenda la scadenza dei quindici minuti. Un secondo prima, la cadenza non è compiuta e il giro resta fermo. All’istante della scadenza la condizione è vera e il giro parte una volta sola. Un altro istante, ancora dentro lo stesso minuto dello scadere, non produce un secondo avvio. Il minuto dello scadere non è una finestra da riempire di partenze. È il minuto in cui la regola ha già detto sì, e non lo ridice finché non si entra in un minuto successivo in cui la cadenza è di nuovo compiuta.
La prova riguarda i quindici minuti. Cinque minuti, sessanta minuti e un orario fisso come le 02:10 sono la stessa famiglia di decisioni, con un altro intervallo o con un’ora del giorno. Questo capitolo non trasforma la prova dei quindici in un racconto sul contenitore per ciascuna cadenza. Dice dove sta la prova, e che cosa ha mostrato. Prima no. Allo scadere sì, una volta. Nello stesso minuto no.
L’orologio finto non sposta l’hash e non sposta il cursore. Sposta l’istante in cui si decide se partire. Se allo scadere il lucchetto di quell’id è tenuto, il pianificatore salta l’id e non scrive un errore ogni trenta secondi, anche se la cadenza sarebbe compiuta. Se l’integrazione è in pausa, la salta il pianificatore e la salterebbe il webhook. La scadenza è necessaria per l’avvio automatico a intervallo. Non è sufficiente da sola, se un’altra regola di questo capitolo dice di non partire.
Dopo un errore, l’intervallo intero
Dopo un errore l’orologio si aggiorna, e si aspetta l’intervallo intero. Un giro fallito su una cadenza di quindici minuti non riparte un minuto dopo per insistenza. Il ricordo dell’ultimo tentativo si sposta sull’errore, e l’intervallo ricomincia da lì. L’errore non è un invito a richiamare Espo di continuo. È un invito a leggere il giornale, che resta nella lingua del giro.
Solo se l’impostazione retry_failed vale 1 l’orologio resta fermo. In quel caso il tentativo non consuma l’intervallo. La scelta è il valore 1. Senza quel valore, l’orologio si aggiorna e l’intervallo intero va atteso. Il quarto capitolo parla di ciò che resta spento. Qui questo valore entra solo perché cambia l’orologio del prossimo tentativo. Non è il modo ordinario di rincorrere un errore, e non è la coda di ritento. La coda resta spenta. L’orologio che si aggiorna è un’altra cosa: è il tempo che passa prima che il pianificatore riprenda in considerazione quell’id.
Un errore di una integrazione non sposta l’orologio di un’altra. Cadenza, lucchetto ed errore sono per id. Due integrazioni che girano insieme possono finire in due modi. Una aggiorna il proprio orologio perché è fallita, e da lì aspetta l’intervallo intero. L’altra è riuscita, e il suo orologio segue il successo. Il pianificatore, la volta dopo, le tratta secondo il loro orologio. Se un lucchetto è ancora tenuto, salta quell’id senza scrivere un errore ogni trenta secondi.
Sul file di notte, un errore alle 02:10 con cadenza oraria fissa è l’orario che è stato tentato. Con una cadenza a minuti, l’errore fa partire l’attesa dell’intervallo intero, salvo retry_failed a 1. In entrambi i racconti il giornale tiene la frase dell’errore, nella lingua del giro. La home, la mattina, non trasforma quel fallimento nel successo di ieri. Questo sta nel paragrafo del numero.
La percentuale in home
La percentuale delle anagrafiche in home conta le integrazioni in ingresso verso Account, e solo se il giro di oggi è riuscito. Un ingresso verso un’altra scheda non entra in quel conto. Un’uscita HTTP, anche se legge Account, non è un ingresso verso Account, e non è quel numero. Il numero guarda il verso e la scheda. Guarda il giorno.
Il successo di ieri non viene mostrato come oggi. Se stanotte il giro non è riuscito, la percentuale non si riempie con il ricordo di ieri. Il giorno è il giorno del giro. Una integrazione in ingresso verso Account che ieri ha chiuso bene, e oggi non ha un successo, non presta il suo ieri alla home di stamattina. Chi guarda il numero la mattina guarda oggi.
Se il giro di oggi è riuscito, quell’integrazione in ingresso verso Account entra nel conto. C014 invariata e C001 creata, nel passaggio in cui il file era quello atteso, appartengono a un giro che può essere un successo di oggi: Espo è stato chiamato per la creazione, non è stato chiamato per la riga invariata, e il giro è andato a buon fine. Il numero in home non separa, sulla percentuale, la riga saltata per hash dalla riga creata. Separa il giorno, il verso e la scheda. Il dettaglio delle righe sta nel giornale.
Un giro fallito oggi, anche se ieri la stessa integrazione aveva creato o aggiornato, non tiene accesa la percentuale di oggi. Un’altra integrazione, diversa, in ingresso verso Account, entra nel conto solo se il suo giro di oggi è riuscito. Due ingressi verso Account sono due id. Il numero li conta secondo il successo di oggi di ciascuno, non secondo un successo medio della notte.
Il bordo, gli elaborati, l’avviso
Un file atteso entro un’ora accende il bordo rosso se quell’ora passa senza un giro riuscito. L’attesa è dell’ora. Passata l’ora, senza un successo, il bordo è rosso. Un successo dentro l’ora non è quel bordo. Il bordo è un controllo, come la percentuale. Non scrive su Espo. Non sposta il file. Non avanza un cursore. Dice che l’ora è passata e il giro riuscito non c’è.
Un file già nella cartella degli elaborati non viene riletto. Il giro che lo ha portato lì lo ha già avuto fra i file da leggere. Rileggerlo significherebbe trattare di nuovo un file già passato. Il prodotto non lo rimette fra i file del giro. La cartella è il posto di ciò che è già stato letto. L’hash, accanto, è il posto di ciò che è già stato visto come riga. Un file fermo negli elaborati non ricalcola C014 e non ricrea C001.
Se manca una colonna obbligatoria, il file non si sposta. Resta dove è arrivato. Non è negli elaborati, e quindi non cade nella regola di ciò che non si rilegge. Il giro dopo può ancora trovarlo nella cartella di arrivo. È il caso della diciottesima riga del racconto. Lo scarto è uno solo, di file. Non è una pila di scarti, una per riga. Non è un file parcheggiato negli elaborati come se la lettura fosse compiuta.
Un avviso di giro fallito si accende una volta, con il nome dell’integrazione. Non è una riga nuova ogni trenta secondi. Il pianificatore, davanti a un lucchetto, tace a quel ritmo. L’avviso di fallimento, quando c’è, si accende una volta e porta il nome, così si sa quale regola non è andata a buon fine. Una seconda integrazione, riuscita la stessa notte, non sostituisce quel nome con il proprio. L’avviso è di quell’integrazione. La percentuale, se l’integrazione fallita era un ingresso verso Account, non conta quel giro come successo di oggi.
Colonna mancante, cella mancante
Manca una colonna obbligatoria. Il file non si sposta. Lo scarto è uno solo, di file. Le altre righe non passano a metà. Il file non è quello atteso. Trattarlo come un elenco di righe buone e di righe no caricherebbe un’anagrafica fatta con le colonne rimaste, e un file spostato non verrebbe riletto. Per questo il file resta fermo e lo scarto è uno. La diciottesima riga del racconto è questo caso. Manca una colonna obbligatoria. Il file non si sposta. In quel passaggio non si crea C001 e non si confronta C014 fino a una chiamata: le righe non proseguono a metà.
Manca una cella, e la colonna c’è. Si scarta quella riga. Le altre proseguono. La colonna attesa è nel file. Una riga non ha il valore. Le righe che il valore ce l’hanno continuano, secondo l’hash e secondo la chiave. Hash già salvato: Espo non viene chiamato. Chiave esistente: aggiornamento. Chiave assente: creazione. La riga della cella mancante non diventa un record in quel giro.
Sono due errori. Il primo parla del file. Il secondo parla della riga. La riga 18 è il primo. Non è una cella vuota dentro una colonna presente. La cella vuota, quando la colonna c’è e il campo ha un padrone, è il capitolo tre. Qui la cella mancante con la colonna presente è solo la distinzione dello scarto: una riga si ferma, le altre no. Lo scarto di file della colonna mancante ferma il file intero e lo lascia dov’è.
Chi controlla il mattino dopo distingue i due giornali. Uno scarto di file, uno solo, e il file ancora nella cartella di arrivo: mancava una colonna. Una riga scartata e le altre andate avanti: la colonna c’era, la cella no. Il bordo rosso, se l’ora attesa è passata senza un giro riuscito, può accendersi in entrambi i fallimenti del giro. L’avviso porta una volta il nome dell’integrazione. La percentuale di oggi non conta un giro che non è riuscito.
Un simile con un’altra chiave
Espo può rifiutare la scrittura perché esiste già un record simile, con un’altra chiave. La riga viene scartata. Non viene unita in silenzio al record già presente. Non sostituisce la chiave di quel record con la chiave di questa riga. Il simile resta dov’è, con la chiave che aveva. La riga di questo giro non entra.
L’integrazione può essere istruita ad adottare quel record. Adottare significa prendere il record che ha provocato il rifiuto e proseguire su quello. Non è il comportamento di default. Di default la riga si scarta. Adottare è una istruzione sulla regola. Finché la regola non la porta, lo scarto resta lo scarto. Un nome uguale, da solo, non è un ordine di adozione. L’ordine, se c’è, è detto sull’integrazione.
Nord Est, con la chiave che finisce con C001, nel passaggio in cui C001 è nuova, non è questo rifiuto. È una creazione, perché la chiave non c’era e l’hash non poteva essere già salvato per una riga mai vista. Il simile con un’altra chiave è il caso in cui Espo dice di no. Il giornale ha lo scarto di quella riga, e se l’errore nomina un campo, il giornale riporta quel nome. Adottare, il giorno in cui qualcuno lo chiede sulla regola, è un’altra configurazione. Va letta come tale. Non è il giro di stanotte, se stanotte la regola non lo dice.
Lo scarto del simile non sposta un file che, per un’altra ragione, non andava spostato. Se il problema è la colonna obbligatoria mancante, si resta allo scarto di file, uno solo, e il file non si muove. Il simile è una risposta di Espo su una riga che si stava scrivendo. Quindi è una riga arrivata fino alla chiamata. C014, con l’hash già salvato, non arriva alla chiamata, e non può ricevere questo rifiuto. Il rifiuto appartiene a una riga che il motore ha provato a scrivere.
Data, elenco, telefono, nome del campo
Una data illeggibile non inventa un giorno. Il motore non sceglie la data di oggi, e non sceglie un giorno di comodo, per far passare la riga. Se la data non si legge, non diventa una data. Le date si leggono in tre formati. Il primo è il giorno, il mese e l’anno con la barra, come 18/10/2026. Il secondo è l’anno, il mese e il giorno con il trattino, come 2026-10-18. Il terzo è il giorno, il mese e l’anno con il trattino, come 18-10-2026. Un valore che non è uno di questi tre non viene tradotto in un giorno scelto al suo posto.
Un campo elenco ha bisogno di almeno un valore. Un elenco senza valori non soddisfa il campo. Il giro non inserisce una voce che la riga non ha portato, per poter dire che l’elenco c’è. Almeno un valore è la condizione. Sotto quella condizione il campo elenco non è riempito d’ufficio.
Un telefono italiano diventa +39. Il numero, nella forma italiana, viene riscritto con quel prefisso. Verso Espo parte il valore con +39. La riscrittura riguarda il telefono riconosciuto come italiano. Non è una chiave, e non è un hash. Non sposta il cursore. Sta fra i gesti che preparano il valore, prima della scrittura, sullo stesso piano della data letta in uno dei tre formati.
Un errore di Espo che nomina un campo riporta quel nome. Il giornale non si ferma a un rifiuto senza soggetto. Dice il campo che Espo ha nominato. Chi legge torna all’abbinamento e alla riga con quel nome in mano. Il nome è quello dell’errore. Se il rifiuto è il simile con un’altra chiave, e l’errore nomina il campo, nel giornale c’è quel nome. Se la data non si legge, non c’è un giorno inventato da andare a cercare su Espo sotto un altro nome.
Questi controlli stanno prima, o nel momento, della scrittura. Non sostituiscono l’hash. Una riga C014 già salvata non arriva a farsi rileggere la data: Espo non viene chiamato. Una riga che cambia, o che nasce, passa di lì. C001, nuova, con nome Nord Est e città Trieste, passa dalla creazione. Se uno dei suoi valori fosse una data illeggibile, quella data non diventerebbe un giorno. Se uno dei suoi valori fosse un elenco senza alcun valore, l’elenco non verrebbe colmato. Se uno dei suoi valori fosse un telefono italiano, diventerebbe +39. Se Espo rifiutasse e nominasse un campo, il giornale riporterebbe quel nome.
L’identità esterna vuota
Un’identità esterna vuota è una questione di file, per il ritento. Non è una riga messa in coda. Non va descritta come accodata. Manca il valore esterno. Manca la parte destra della chiave. Non c’è una riga individuata da ritentare come record già distinto. Il problema sta al livello del file.
La coda di ritento resta spenta. Appartiene al quarto capitolo, con ciò che resta spento. Qui basta la frase: resta spenta. L’identità vuota non è il modo in cui una riga entra in quella coda. Non ci entra. Questo capitolo non accende la coda, e non mette l’identità vuota in un elenco di ritento.
Lo scarto di file, quando manca la colonna obbligatoria, è il gesto della riga 18: il file non si sposta, lo scarto è uno. L’identità esterna vuota, come questione di ritento, sta su quel piano, il piano del file. Non sta sul piano di una coda di righe. Il capitolo tre parlerà della cella vuota quando la colonna c’è. Non è l’identità vuota, e non è questa pagina. Confondere la cella vuota di un campo qualsiasi con l’identità esterna vuota farebbe due errori diversi passare per uno. L’identità è la parte destra della chiave. Vuota, è un problema del file, ai fini del ritento, e non una voce in coda.
La notte, nell’ordine in cui il motore la vede
Il collegamento del file è un ingresso. Non è l’HTTP di uscita. L’integrazione punta ad Account. La chiave, sulla scheda standard, andrà in cExternalKey. La forma è l’id di questa integrazione, due punti, il valore esterno.
Se l’integrazione è in pausa, il pianificatore la salta e il webhook la salta. Esegui ora no. Se un giro di questa stessa integrazione è già in corso, la seconda partenza viene avvisata che un’esecuzione è già in corso, e il pianificatore salta l’id senza scrivere un errore ogni trenta secondi. Un’altra integrazione, con un altro id, può essere in corso insieme.
La cadenza può essere le 02:10, se è un orario fisso. Può essere quindici minuti. In quel caso l’orologio finto ha già detto come si giudica la scadenza: un secondo prima non parte, allo scadere parte una volta, nello stesso minuto non riparte. Non è un quarto d’ora guardato sul contenitore. Dopo un errore l’orologio si aggiorna e si aspetta l’intervallo intero. Solo con retry_failed uguale a 1 l’orologio resta fermo.
Sul file che ha le colonne attese, la riga C014 ha l’hash già salvato. Espo non viene chiamato. La riga C001 non ha ancora la chiave. Nasce un Account. Il nome è Nord Est. La città è Trieste. La chiave finisce con C001. Un secondo invio identico di quella riga non duplica: l’hash c’è, la chiamata non parte.
Sulla condizione della diciottesima riga manca una colonna obbligatoria. Quel passaggio non crea C001 e non chiama Espo per C014. Il file non si sposta. Lo scarto è uno, di file. Un file già negli elaborati, letto in un’altra condizione, non viene riletto. Se invece la colonna c’è e manca la cella, si scarta quella riga e le altre proseguono.
Se Espo rifiuta perché un simile esiste con un’altra chiave, la riga si scarta. Dire all’integrazione di adottare quel record non è il default. Una data illeggibile non inventa un giorno: si legge solo nei tre formati, 18/10/2026, 2026-10-18 e 18-10-2026. Un elenco ha bisogno di almeno un valore. Un telefono italiano diventa +39. Un errore di Espo che nomina un campo riporta quel nome. Un’identità esterna vuota resta una questione di file, per il ritento, e non una riga in coda. La coda resta spenta.
In uscita, su un’altra integrazione, il canale è HTTP. Il cursore ricorda l’ultimo modifiedAt e l’id Espo. Legge pagine di 50. Manda solo i campi abbinati. Avanza solo dopo i POST riusciti. Se manca la chiave esterna, il pull si ferma e i record dopo non escono. Un secondo giro, se nessuno in Espo ha toccato quei record, esporta zero.
La mattina, la percentuale in home conta l’ingresso verso Account solo se il giro di oggi è riuscito. Ieri non viene mostrato come oggi. Se il file era atteso entro un’ora e quell’ora è passata senza un giro riuscito, il bordo è rosso. L’avviso di giro fallito si accende una volta, con il nome dell’integrazione.
Solo le API HTTP
Le scritture verso Espo passano dalle API HTTP. Anche le letture verso Espo, quando il giro deve guardare una scheda, passano di lì. Le tabelle di Espo non sono un canale. Non si interrogano con SQL per cercare cExternalKey, e non si aggiornano con SQL per scrivere la chiave. La chiave si scrive con la chiamata. L’aggiornamento è una chiamata. La creazione è una chiamata. Se l’hash è già salvato, la chiamata non c’è.
Il database in sola SELECT, quando il canale di ingresso è quello, è il database esterno. Può essere MySQL, MariaDB, Postgres, SQL Server o Oracle. La SELECT legge quel lato. Non è una query sulle tabelle di Espo. Il cursore di uscita, dal lato opposto, non si implementa andando a leggere le tabelle: ricorda modifiedAt e id, chiede pagine di 50, e manda in HTTP i campi abbinati.
Nesso non è un secondo CRM. Le anagrafiche stanno in Espo. Nord Est, una volta creato, è un Account lì. Nel motore stanno l’hash, il cursore, il lucchetto, l’orologio, il giornale. Non sta una seconda scheda Account da consultare al posto di Espo. La percentuale in home guarda se il giro di oggi, in ingresso verso Account, è riuscito. Non conta un archivio parallelo.
Nesso non è un connettore certificato. Il nome del sistema che ha posato il file non sta nel codice. Il file di notte è un file, con colonne, su un canale di ingresso. La regola è l’integrazione. Se questo diario e il manuale divergono, vale il manuale, nella cartella docs del repository github.com/nessocrm/nessocrm.
Il giornale resta nella lingua del giro
Il giornale di un giro resta nella lingua del giro. Se il giro è partito da una pagina in italiano, le frasi restano in italiano anche se il giorno dopo la pagina è in inglese. Il giornale è il verbale di quell’esecuzione. Non si traduce a posteriori. Il pianificatore parla la lingua in cui è stato impostato. Chi legge un errore la mattina legge la frase che il motore ha scritto.
Questo diario esiste due volte, in italiano e in inglese, perché è una spiegazione. I due testi dicono gli stessi fatti. Il giornale non farà lo stesso. Dirà che cosa è successo a C014, a C001, alla riga 18, all’uscita che esporta zero, nella lingua del giro. Cercare nel giornale la prosa di questo capitolo è cercare nel posto sbagliato. Nel giornale, quando Espo nomina un campo, c’è quel nome. Quando l’avviso di un giro fallito si accende, si accende una volta e porta il nome dell’integrazione.
La lingua del giornale non cambia l’hash e non cambia il cursore. Un giro in italiano e un giro in inglese, su due integrazioni, ricordano ciascuno le proprie righe e il proprio modifiedAt. Non si traducono le chiavi. C001 resta C001. Nord Est resta il nome. Trieste resta la città. +39 resta il prefisso del telefono italiano. I tre formati della data restano quei tre valori, indipendentemente dalla lingua della pagina. La pagina sceglie la lingua di chi configura. Il verbale sceglie la lingua di chi ha lanciato il giro, e la tiene.
Che cosa questo capitolo non ha fatto
Non ha detto che cosa succede quando la cella arriva vuota, la colonna c’è, e il campo ha un padrone. Lo fa il terzo capitolo, insieme al record che nel file non c’è più e su Espo si chiude. Non ha acceso la coda di ritento. Resta spenta, ed è materia del quarto, con il resto di ciò che resta spento. Non ha spostato Espo dall’altra parte del canale. La chiave, l’hash e il cursore stanno sulla memoria del giro. Il file di stanotte resta un file.
C014, nel passaggio in cui il file è quello atteso, non ha prodotto una chiamata. C001 ha prodotto una creazione, con il nome Nord Est, la città Trieste, e una chiave che finisce con C001, nel cExternalKey della scheda standard. La riga 18 ha tenuto il file fermo, con uno scarto solo, di file. Il secondo giro in uscita, se nessuno ha toccato i record, ha esportato zero. Il quindici minuti è stato giudicato con l’orologio finto, non con un quarto d’ora sul contenitore. Sono i gesti di questo capitolo. I due capitoli dopo ne hanno altri.
La prima lettura resta il confine: Espo da una parte, il canale dall’altra, l’integrazione come regola. Questa lettura è il modo in cui la regola riconosce, ricorda e non rifà. Senza la chiave, C001 e un altro C001 si confonderebbero. Senza l’hash, la riga invariata chiamerebbe Espo ogni notte. Senza il cursore, l’uscita rimanderebbe gli stessi record, oppure perderebbe quelli che non sono usciti. Con i tre, il giro di stanotte ha un gesto per ciascuna riga, e un gesto per l’uscita, e la mattina la home può dire solo ciò che oggi è davvero riuscito.
La prova è il repository
Non c'è un modulo. Il codice è pubblico e la prova parte con un Espo vuoto.
github.com/nessocrm/nessocrmEspo da una parte. Il sistema esterno dall'altra.
La prova è il repository pubblico. Parte un Espo vuoto. Non importa un dump di produzione.
Come si prova