Nel caso documentato dall’audit del mio sistema del 31 agosto 2026, un monitor aveva rilevato problemi da segnalare all’operatore. La notifica Slack era fallita; anche il fallback email era fallito. Il registro era aggiornato e il processo era terminato regolarmente, ma il verdetto non era arrivato. I controlli per alcuni dei guasti esistevano già: era la consegna degli avvisi a interrompersi.
L’exit code rimane utile per sapere come termina un processo. Per stabilire se il suo lavoro è disponibile serve una prova anche alla destinazione. Con gli agenti questo confine conta: produrre un report, proporre una modifica e renderla utilizzabile da un altro programma sono passaggi distinti. Il controllo deve sapere a quale di questi può assegnare il successo.
Per mostrarlo con poche righe passeremo a un esempio didattico diverso dal caso storico: un job produce un report su file per un programma che lo deve importare. Completeremo questo percorso, poi torneremo agli avvisi. Il principio sarà lo stesso; cambierà il riscontro da cercare. Provenienza e rettifica della diagnosi storica sono nell’estratto documentale allegato.
Un report, un importatore, un controllo
Nell’esempio, il job genera un report JSON. L’importatore è il programma destinatario: deve aprire quel report e caricarne il contenuto nella propria applicazione. Il controllo verifica una condizione precedente: che nella destinazione usata dall’importatore sia leggibile il file dell’esecuzione attesa.
È questo il confine della dimostrazione. Il controllo non esegue l’importatore e non certifica un’importazione riuscita. Il suo verdetto si chiamerà OUTPUT_READABLE_OK. Per sapere se l’importatore ha elaborato il report servirà invece un riscontro prodotto dall’importatore, legato alla stessa esecuzione.
La sequenza è questa:
- Lo script di avvio stabilisce l’identificatore dell’esecuzione e il percorso del report. Li conserva come aspettative e passa entrambi al job.
- Il job scrive il report nella destinazione assegnata, includendo l’identificatore ricevuto.
- Dopo il job, lo script di avvio lancia il controllo con le aspettative iniziali. Il controllo apre la destinazione configurata per l’importatore, senza chiedere al job dove cercare o quale esecuzione aspettarsi.
- Il verdetto descrive soltanto quella lettura. L’importatore, quando viene avviato, rimane responsabile della propria elaborazione e del relativo esito.
L’origine delle aspettative è decisiva. Se il controllo prendesse l’identificatore dall’ultimo report disponibile, troverebbe sempre il report giusto per definizione. Anche quello della settimana scorsa.
Il minimo eseguibile
Crea una cartella locale vuota per la prova e salva al suo interno i programmi seguenti. Useranno soltanto la sottocartella destinatario: nessun servizio esterno, database o percorso di produzione.
Questo è avvio.py. Per rendere leggibili le prove usa l’identificatore fisso demo-a: identifica esclusivamente l’esercizio descritto qui. In un job reale lo script di avvio deve assegnare un identificatore nuovo a ogni esecuzione e conservarlo prima di avviare il job. Riutilizzare quello fisso impedirebbe di distinguere esecuzioni successive.
from pathlib import Path
import subprocess
import sys
run_id = "demo-a"
output = Path("destinatario/report.json").resolve()
output.parent.mkdir(exist_ok=True)
job = subprocess.run([sys.executable, "job.py", run_id, str(output)])
check = subprocess.run([sys.executable, "verifica.py", run_id, str(output)])
raise SystemExit(0 if job.returncode == 0 and check.returncode == 0 else 1)Code language: JavaScript (javascript)
Lo script non trasforma un errore del job in un successo. Esegue comunque il controllo, così può mostrare anche se il report manca o è inutilizzabile; alla fine conserva l’esito negativo di uno qualsiasi dei passaggi.
Questo è job.py, un produttore dimostrativo che scrive un testo fisso al posto di calcolare un report:
import json
import sys
from pathlib import Path
run_id, filename = sys.argv[1:]
report = {"run_id": run_id, "payload": "Report pronto"}
Path(filename).write_text(json.dumps(report), encoding="utf-8")Code language: JavaScript (javascript)
Infine verifica.py controlla il documento obbligatorio. Non contiene una dichiarazione di consegna scritta dal produttore, né una politica di skip da interpretare prima di aver visto il caso normale.
import json
import sys
from pathlib import Path
expected_run, filename = sys.argv[1:]
try:
report = json.loads(Path(filename).read_text(encoding="utf-8"))
if not isinstance(report, dict):
raise ValueError("Il report non è un oggetto JSON")
if report.get("run_id") != expected_run:
raise ValueError("Il report appartiene a un altro run")
payload = report.get("payload")
if not isinstance(payload, str) or not payload.strip():
raise ValueError("Il report è vuoto o ha un payload non valido")
except (OSError, UnicodeError, ValueError) as error:
raise SystemExit(f"VERIFY_FAILED: {error}")
print(f"OUTPUT_READABLE_OK run_id={expected_run}")Code language: JavaScript (javascript)
Dalla cartella della prova esegui:
python3 avvio.pyCode language: CSS (css)
Il job scrive destinatario/report.json; il controllo lo apre e stampa OUTPUT_READABLE_OK run_id=demo-a. Lo script di avvio termina con successo perché entrambi i programmi sono riusciti.
Qui i programmi girano con lo stesso utente, nello stesso ambiente locale. In un sistema reale il controllo deve avere accessi, percorso e configurazione equivalenti a quelli dell’importatore. Leggere con l’utente del job un file che l’importatore non può aprire non dimostra questa disponibilità.
Lo stesso report, quando qualcosa manca
Ora proviamo soltanto il controllo. Non rilanciare avvio.py: il job ricreerebbe il file e cancellerebbe la condizione che vogliamo osservare. L’identificatore atteso resta demo-a, scelto dall’esercizio, indipendentemente dal contenuto del report.
Il comando è sempre questo, dalla stessa cartella:
python3 verifica.py demo-a destinatario/report.json
Per ogni prova sostituisci il contenuto di destinatario/report.json con l’input indicato. Nel caso mancante, sposta temporaneamente il file fuori da destinatario, lasciando invariato il comando.
| Input nella destinazione attesa | Esito del comando |
|---|---|
{"run_id":"demo-a","payload":"Report pronto"} | OUTPUT_READABLE_OK run_id=demo-a, exit 0 |
| File assente | VERIFY_FAILED con errore di file mancante, exit 1 |
{"run_id":"demo-prima","payload":"Report pronto"} | VERIFY_FAILED: Il report appartiene a un altro run, exit 1 |
{"run_id":"demo-a","payload":""} | VERIFY_FAILED: Il report è vuoto o ha un payload non valido, exit 1 |
L’output della riga mancante include un dettaglio del sistema operativo, che può variare. Il comportamento atteso rimane il fallimento. Anche un JSON malformato o un payload composto soltanto da spazi vengono rifiutati.
Il vecchio report è il test più istruttivo: il file esiste ed è leggibile, ma non soddisfa l’esecuzione attesa. Guardare soltanto la sua presenza perderebbe questo caso. La data di modifica non basta: copiare il report precedente può aggiornarla senza aggiornare il contenuto.
Il report accettato, invece, dimostra ancora soltanto presenza, identità dichiarata dell’esecuzione e leggibilità minima. Il controllo non autentica il produttore e non giudica il significato del testo. Un JSON ben formato può raccontare qualunque sciocchezza con sintassi impeccabile. Per contenuti reali occorrono verifiche di dominio; per l’elaborazione successiva occorre il riscontro dell’importatore.
Chi si accorge che il controllo non è partito
Nell’esercizio lo script di avvio chiama il controllo subito dopo il job. Se il job rimane bloccato, oppure lo script di avvio non parte, quella chiamata non avviene. Il codice non copre queste assenze e non implementa timeout o retry.
Nel sistema schedulato, chi pianifica il lavoro deve stabilire in anticipo la scadenza entro cui si aspetta il report e il verdetto. Un controllo esterno, avviato indipendentemente dal job, verifica a quella scadenza che il verdetto dell’esecuzione sia comparso. Un esito mancante deve restare distinguibile da un controllo eseguito che ha rifiutato il report.
La scelta della scadenza dipende dalla cadenza del lavoro e dalla tolleranza del destinatario. Va configurata fuori dal report: permettere al job di spostare da solo il momento in cui dovrebbe essere controllato allarga il problema invece di risolverlo.
Il costo è mantenere anche questo piccolo stato atteso e verificare che il controllo esterno giri davvero. Non serve costruire una gerarchia infinita di guardiani: serve scegliere il punto di osservazione indipendente e sapere quali guasti comuni rimangono.
Se il report può mancare legittimamente
Completiamo ora il caso che abbiamo escluso dal primo esempio. Prima di avviare il job, lo script di avvio potrebbe accertare, da una fonte indipendente, che non esistono nuovi input. In quel caso la politica del lavoro può autorizzare l’assenza del report.
Deve comunque rimanere un esito distinto e riconducibile all’esecuzione: per esempio, uno skip motivato dall’assenza di input, registrato dallo script di avvio. Il controllo della scadenza confronterà quell’esito con l’autorizzazione già conservata. Il semplice file mancante continuerà a non bastare.
È un’estensione del contratto, non un’opzione che il job può attivare scrivendo una giustificazione. Se lo script di avvio si aspettava il report, una dichiarazione di skip proveniente dal job deve richiedere una verifica, senza diventare automaticamente successo. Gli snippet restano volutamente sul caso in cui il report è obbligatorio: accettare assenze richiede anche la prova che le autorizza.
Torniamo all’avviso che non arriva
Per gli avvisi cambia l’oggetto della verifica: la presenza di un file locale non prova che l’operatore abbia ricevuto il messaggio. La distinzione è coerente con il monitoraggio black-box descritto nel capitolo Monitoring Distributed Systems del libro SRE di Google, consultato il 30 ottobre 2026: osservare il comportamento disponibile all’utente.
Seguiamo un singolo evento didattico, avviso-demo. Il monitor rileva una condizione e, prima di tentare l’invio, conserva evento e destinazione con stato pendente. Il processo incaricato dell’invio prova a comunicare col canale, ma il tentativo fallisce. Registra l’errore; l’evento resta pendente. Una politica di retry stabilisce quando riprovare e quando richiedere un intervento.
Al tentativo successivo arriva il riscontro scelto dal contratto. Soltanto allora cambia lo stato dell’evento. La sequenza che dobbiamo poter ricostruire è questa:
Passaggio di avviso-demo | Che cosa rimane osservabile |
|---|---|
| Evento salvato come pendente | Esiste un avviso ancora da consegnare |
| Tentativo fallito | L’errore del tentativo, senza cancellare l’avviso pendente |
| Riscontro concordato ottenuto | La prova che autorizza la chiusura per quel livello di consegna |
Quel livello va scelto con precisione. L’accettazione del provider conferma che il servizio ha accettato il messaggio. Il riscontro della sua presenza nel canale destinatario conferma un passaggio ulteriore. La lettura o presa in carico umana ne conferma un altro ancora. Per un allarme che richiede intervento, fermarsi al primo livello può essere insufficiente.
Il registro deve conservare quale riscontro è stato ottenuto, senza chiamare tutto sent. Va inoltre sorvegliata l’età degli eventi pendenti da un processo indipendente da quello che tenta l’invio. Un retry può produrre duplicati se il servizio ha ricevuto il messaggio ma la risposta si è persa: l’identificatore stabile aiuta a riconoscerli, senza promettere che il canale li elimini.
È qui che il caso storico torna utile. I canali Slack ed email erano diversi, ma i tentativi dipendevano dalla rete della stessa macchina. Cambiare destinatario senza cambiare il punto di guasto lascia una ridondanza molto educata: fallisce insieme al canale principale. Il rimedio da progettare riguarda conservazione, ritrasmissione e riscontro; lo snippet sul file dimostra soltanto il principio di verificare una destinazione, non implementa quel percorso di notifica.
Una breve variante riguarda Git: se il programma destinatario legge main, trovare il report nel branch del job non ne prova la disponibilità. Il riferimento letto entra nel contratto come il percorso del file. Una pull request può essere un risultato corretto per un agente autorizzato a proporre modifiche, ma non coincide con il prodotto già disponibile in produzione.
Per iniziare, scegli un job e nomina il suo destinatario. Scrivi quale risultato aspetta, dove e per quale esecuzione. In una prova isolata fai mancare quel risultato, anche lasciando terminare regolarmente il job. Osserva poi il controllo e il suo riscontro: da lì emerge il tratto della consegna che devi ancora verificare.
Articolo scritto con l’assistenza dell’AI, ideato e riletto da Giovanni Liguori.

