Skip to main content

Module 4 · Processes & Services

Cron & Scheduling

  • Lesson 12/15
  • 30 min
  • Difficulty: Intermediate

This lesson is not available in English yet: you are reading it in Italian.

Objective

Validare un'espressione temporale prima di aspettare che il lavoro parta, e scrivere un lavoro periodico che non dipenda dall'ambiente ne' perda la propria uscita

Prerequisites

Prima di leggere: due domande sulle lezioni precedenti

Nella lezione sulle variabili d'ambiente hai visto che una riga come PATH=$HOME/bin:$PATH non funziona in un file di crontab, e perché. Prima domanda: che valore ha allora PATH dentro un lavoro periodico? Seconda domanda: dove hai letto la risposta, e con quale comando la verifichi sulla tua macchina?

La seconda domanda è quella che conta, e la risposta onesta per la prima è che non lo sai ancora. Lo scoprirai qui, e scoprirai anche che la risposta che circola in rete non corrisponde a quello che il sistema dichiara.

Il problema: un lavoro che doveva girare e non è girato

Un lavoro periodico che non parte è il guasto più silenzioso che esista. Non c'è un utente che segnala, non c'è una pagina che dà errore, non c'è un processo che muore in modo visibile. C'è un backup che non esiste, scoperto il giorno in cui serve, oppure un dato che non si aggiorna da tre settimane e nessuno se n'era accorto.

E c'è il guasto gemello, più raro e più costoso: un lavoro che è girato due volte, o che è girato in un momento diverso da quello che credevi, perché l'espressione temporale non significava quello che pensavi.

Linux offre due meccanismi per questi lavori, e non sono equivalenti. Uno è vecchio di decenni ed è ovunque; l'altro è recente, più verboso, e risolve problemi che il primo non affronta. La competenza utile non è saperne usare uno, è saper scegliere e saper verificare prima di aspettare.

La sintassi, letta dalla fonte

Il formato di una riga di crontab è documentato nel file di sistema stesso, e questa è la documentazione migliore perché sta sulla macchina su cui lavori. Questo è il contenuto di /etc/crontab su questo sistema, tolte le righe non pertinenti:

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *	* * *	root	cd / && run-parts --report /etc/cron.hourly

Cinque campi temporali, poi l'utente, poi il comando. Una nota che la riga del giorno della settimana dichiara e che vale la pena notare: la domenica si scrive 0 oppure 7, quindi l'intervallo 1-5 sono i giorni lavorativi e 6-7 non è «sabato e domenica» in modo intuitivo ma funziona perché sette è domenica.

La trappola grossa della sintassi di cron è che quando il giorno del mese e il giorno della settimana sono entrambi specificati, le due condizioni vengono messe in disgiunzione e non in congiunzione: il lavoro parte se è il quindici oppure se è lunedì, non se è un lunedì quindici. È documentato in crontab(5), e contraddice la lettura naturale di una riga con cinque campi che altrimenti si leggono tutti come condizioni da soddisfare insieme.

L'ambiente, letto dalla fonte e non dal ricordo

La risposta alla domanda d'apertura. La pagina crontab(5) presente su questo sistema, del pacchetto cron versione 3.0pl1-200ubuntu1, dichiara quali variabili vengono impostate automaticamente, e l'elenco è breve: «SHELL is set to /usr/bin/sh, and LOGNAME and HOME are set from the /etc/passwd line of the crontab's owner».

PATH non è in quell'elenco. E il file /etc/crontab su questo sistema porta la riga del PATH commentata, con una nota accanto:

SHELL=/bin/sh
# You can also override PATH, but by default, newer versions inherit it from the environment
#PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Quindi la frase che si legge spesso, «cron ha un PATH minimo che contiene solo /usr/bin e /bin», su questo sistema non è confermata da nessuna delle due fonti: il manuale non promette un PATH, e il file di sistema dice che per default lo eredita dall'ambiente. E quella riga è un commento, quindi vale la regola che su questo progetto si applica da sempre: il dato batte il commento.

La conseguenza operativa è più forte della risposta. Non importa quale sia il valore: se il valore dipende dalla versione del pacchetto, da come è stato avviato il servizio e dall'ambiente di quel momento, allora un lavoro periodico non deve dipendere dal PATH. Si scrivono i percorsi assoluti dei comandi, e il problema smette di esistere su tutte le macchine insieme. Se vuoi comunque sapere il valore sulla tua, il modo è misurarlo: una riga che esegue env e ne scrive l'uscita in un file, letta la volta dopo.

Il confronto: cron e i timer di systemd

Un timer di systemd è una coppia di file: un servizio che fa il lavoro e un timer che dice quando. Più verboso di una riga di crontab, e con quattro differenze che decidono quando conviene.

[Unit]
Description=Ogni 10 secondi, per la lezione

[Timer]
OnBootSec=5s
OnUnitActiveSec=10s
AccuracySec=1s
Unit=lezione-timer.service

[Install]
WantedBy=timers.target
$ systemctl --user start lezione-timer.timer
$ journalctl --user -u lezione-timer.service -o cat | grep giro
giro alle 2026-10-30T17:47:53+01:00
giro alle 2026-10-30T17:48:04+01:00

Le quattro differenze. La prima è il log: l'uscita del lavoro va nel giornale e si legge con lo stesso comando degli altri servizi, mentre cron per default manda un messaggio di posta all'utente, che su una macchina senza posta configurata significa che l'uscita si perde. La seconda è il tempo relativo: OnUnitActiveSec conta dalla fine dell'esecuzione precedente, quindi due esecuzioni non si sovrappongono mai, cosa che cron non garantisce.

La terza è il recupero delle esecuzioni mancate: con Persistent=true, un lavoro che non è girato perché la macchina era spenta viene eseguito alla riaccensione, e cron non ha questo meccanismo. La quarta è la verificabilità, e per me è quella decisiva: l'espressione temporale si può validare prima di aspettare.

$ systemd-analyze calendar "Mon..Fri 09:30"
  Original form: Mon..Fri 09:30
Normalized form: Mon..Fri *-*-* 09:30:00
    Next elapse: Mon 2026-11-02 09:30:00 CET
       (in UTC): Mon 2026-11-02 08:30:00 UTC
       From now: 2 days left

$ systemd-analyze calendar "Lun 09:30"
Failed to parse calendar specification 'Lun 09:30': Argomento non valido
$ echo $?
1

Lo strumento normalizza l'espressione, dice quando partirà la prossima volta, e lo dice anche in tempo universale, che è il dato che serve quando devi coordinare due macchine in fusi diversi. Su un'espressione non valida fallisce con codice uno. Per cron non esiste nulla di equivalente: l'unico modo di sapere se hai scritto bene è aspettare il momento e guardare se è partito.

L'errore di classificazione plausibile è concludere che i timer sono «meglio» e cron è superato. Non è così, e le ragioni per cui cron resta sono concrete: una riga contro due file, è presente su qualunque sistema anche senza systemd, e un utente senza privilegi può installare il proprio crontab con un comando. Il criterio che uso: lavori semplici e personali in cron, lavori di servizio in un timer, e tutto ciò che deve recuperare un'esecuzione mancata obbligatoriamente in un timer.

Esempio svolto: un'espressione che sembra mensile e non lo è

Il problema: devi eseguire un lavoro l'ultimo giorno di ogni mese, alle tre di notte. La prima espressione che viene in mente è il giorno trentuno.

L'informazione disponibile: i mesi non hanno tutti trentuno giorni. Il ragionamento: se il campo del giorno del mese è una condizione da soddisfare, allora nei mesi di trenta giorni la condizione non si verifica mai, e il lavoro semplicemente non parte. Quante volte partirebbe davvero in un anno? Invece di contare a mano, lo chiedo allo strumento.

$ systemd-analyze calendar "*-*-31 03:00:00" --iterations=4
    Next elapse: Sat 2026-10-31 03:00:00 CET
   Iteration #2: Thu 2026-12-31 03:00:00 CET
   Iteration #3: Sun 2027-01-31 03:00:00 CET
   Iteration #4: Wed 2027-03-31 03:00:00 CEST

Guarda i salti. Dopo il 31 ottobre, la prossima volta è il 31 dicembre: novembre è saltato. Poi gennaio, poi marzo: febbraio e aprile saltati. L'espressione non significa «l'ultimo giorno del mese», significa «il trentuno», e in un anno si verifica sette volte su dodici.

La decisione che prendo: per «l'ultimo giorno del mese» non uso un numero di giorno. Con un timer di systemd esiste una forma dedicata per l'ultimo giorno; con cron, dove quella forma non c'è, la soluzione è girare ogni notte e far decidere allo script se oggi è l'ultimo giorno, con un controllo sulla data di domani. L'alternativa che ho scartato è scrivere quattro righe di crontab, una per lunghezza di mese: sarebbe corretta, e sarebbe quattro volte più facile da sbagliare al prossimo cambiamento.

Nell'ultima riga dell'output c'è un dettaglio che vale la pena aver visto una volta: la sigla del fuso cambia da CET a CEST, perché quell'esecuzione cade dopo il passaggio all'ora estiva. Le espressioni temporali si valutano nel fuso locale, quindi nella notte del cambio un lavoro programmato in un certo orario può non trovare quell'ora o trovarla due volte. Per i lavori in cui questo conta, l'uso del tempo universale è la difesa, e lo strumento qui sopra mostra entrambi affinché tu possa decidere.

La verifica che chiude il ragionamento: nel blocco qui sopra i salti sono il dato, e li ho letti dallo strumento invece di contarli. L'errore che avrei potuto commettere, e che è l'errore più comune di questa materia, è verificare l'espressione ragionandoci: su quattro iterazioni il ragionamento regge, su un anno no.

Il caso negativo: un comando che non ha mai funzionato

Questa riga di crontab dovrebbe archiviare i log ogni notte. È stata scritta un anno fa, nessuno ha mai segnalato niente, e l'archivio è vuoto. Trova i tre difetti.

0 3 * * * cd /var/log && tar -czf /archivio/log-$(date +%Y-%m).tar.gz *.log

Il primo difetto è il più sottile, e lo riconosci solo se hai letto la pagina di manuale. La pagina crontab(5) dice che «the entire command portion of the line, up to a newline or % character, will be executed», e aggiunge che i caratteri per cento vengono trasformati in ritorni a capo e che tutto ciò che segue il primo viene passato al comando come ingresso. Quindi date +%Y-%m tronca il comando e il resto diventa dati. Il rimedio è scrivere \% per ciascuno, forma che la pagina di manuale usa nei propri esempi, oppure, molto meglio, non mettere affatto la logica nella riga di crontab.

Il secondo è l'assenza di percorsi assoluti per tar: alla luce di quello che hai letto sopra sull'ambiente, non c'è nessuna garanzia su quale PATH quel comando riceverà, e la differenza fra «funziona sul mio portatile» e «funziona nel lavoro notturno» è esattamente questa. Il terzo è che l'uscita non va in nessun posto: senza un reindirizzamento, l'uscita e gli errori finiscono in un messaggio di posta che su una macchina senza posta configurata viene scartato, e questo spiega perché nessuno ha mai segnalato niente in un anno.

L'errore è plausibile perché la riga è corretta come comando di shell: se la incolli in un terminale funziona. Il file di crontab non è uno script di shell, ed è la stessa lezione della riga del PATH che hai visto nel modulo precedente. Lo riconosci da un sintomo e uno solo: un lavoro che non ha mai prodotto né un risultato né un errore.

La correzione che risolve tutti e tre i difetti insieme non è correggerli uno per uno: è spostare il lavoro in uno script, con set -euo pipefail come hai imparato, percorsi assoluti, e un reindirizzamento dichiarato. La riga di crontab diventa un solo comando senza logica, senza caratteri speciali e senza niente da sbagliare.

Esercizio 1, guidato: valida cinque espressioni

Per ciascuna di queste cinque intenzioni, scrivi l'espressione temporale e poi validala con lo strumento, non con il ragionamento. Riporta la prima e la terza esecuzione prevista.

  1. Ogni giorno alle 2 e 30 di notte.
  2. Ogni lunedì mattina alle 8.
  3. Il primo giorno di ogni trimestre.
  4. Ogni quarto d'ora.
  5. L'ultimo giorno di febbraio, che negli anni bisestili cade il 29.

Esercizio 2, autonomo: misura l'ambiente del tuo lavoro periodico

Installa un lavoro periodico che scrive il proprio ambiente in un file, e la volta dopo leggi quel file. Confronta il valore di PATH con quello del tuo terminale. Criterio di successo: hai il valore misurato, non quello che hai letto da qualche parte, e sai dire se un comando che usi nei tuoi script sarebbe stato trovato.

Esercizio 3, trasferimento

Prendi il lavoro dell'esercizio precedente e riscrivilo come coppia timer e servizio di systemd, usando quello che sai dalla lezione sulle unità. Poi rispondi: quale dei difetti del caso negativo di questa lezione non può più verificarsi in questa forma, e perché?

Soluzioni commentate

Esercizio 1. Le prime quattro sono dirette: *-*-* 02:30:00, Mon *-*-* 08:00:00, *-01,04,07,10-01 00:00:00, e *-*-* *:00/15:00 per il quarto d'ora. Su quest'ultima, la forma con la barra significa «ogni quindici a partire da zero», e vale la pena validarla proprio perché la sintassi non si indovina.

La quinta è l'unica interessante, e la risposta corretta è che non si scrive come espressione. Un'espressione che dica «il 28 oppure il 29 di febbraio» partirebbe due volte negli anni bisestili; una che dica «il 29» salterebbe tre anni su quattro, come hai visto nei salti del trentuno. Con i timer di systemd esiste però una forma dedicata, che conta i giorni dalla fine del mese, e l'ho verificata sulla mia macchina:

$ systemd-analyze calendar "*-*~01 03:00:00" --iterations=4
Normalized form: *-*~01 03:00:00
    Next elapse: Sat 2026-10-31 03:00:00 CET
   Iteration #2: Mon 2026-11-30 03:00:00 CET
   Iteration #3: Thu 2026-12-31 03:00:00 CET
   Iteration #4: Sun 2027-01-31 03:00:00 CET

Trentuno ottobre, trenta novembre, trentuno dicembre: nessun mese saltato. È il confronto diretto con i salti dell'esempio svolto, e la differenza sta tutta nella tilde. Con cron, dove quella forma non esiste, la soluzione resta girare ogni notte e far decidere allo script. Il punto dell'esercizio è questo: alcune intenzioni temporali non si esprimono con un numero di giorno, e insistere a esprimerle così produce un lavoro che gira il numero sbagliato di volte.

Esercizio 2. Qualunque valore tu abbia misurato, la risposta utile è la stessa: non scriverlo in nessun posto come se fosse una costante. Io non ho eseguito questa misura su questa macchina, perché installare un lavoro nel crontab di un sistema che non è mio avrebbe modificato quel sistema, e per una lezione non si modifica la macchina di qualcun altro. Quello che ho fatto è leggere le due fonti, il manuale e il file di sistema, e constatare che nessuna delle due promette un valore. Da cui la regola: percorsi assoluti.

Esercizio 3. Il difetto che non può più verificarsi è il primo, il carattere per cento troncato, e la ragione è strutturale: nel file di una unità di systemd quel carattere non ha quel significato, e soprattutto la riga ExecStart non passa per una shell, come hai misurato nella lezione sulle unità. Quindi non ci sono né sostituzioni di comando né caratteri speciali da proteggere: se provassi a scriverne, verrebbero passati come argomenti letterali.

E questo spiega perché la risposta è «il primo» e non «tutti e tre». Il secondo difetto, il comando scritto senza percorso assoluto, può verificarsi ancora, e qui ho dovuto correggere quello che credevo. Avevo scritto che ExecStart richiede un percorso assoluto, e sulla versione di systemd di questa macchina non è vero: ho provato un'unità con ExecStart=echo ciao, il comando è stato eseguito, il giornale riporta ciao, e systemctl show -p ExecStart riporta path=echo senza lamentarsi. Quindi il nome nudo viene cercato, e la dipendenza dall'ambiente resta: i percorsi assoluti vanno scritti perché sono giusti, non perché qualcuno ti fermi.

Il terzo difetto, l'uscita che si perde, non può verificarsi, perché l'uscita standard di un servizio finisce nel giornale per costruzione e non per configurazione. Quindi la risposta completa è: uno reso impossibile dal formato, uno che resta a carico di chi scrive, e uno risolto dalla raccolta automatica dei log. Vale la pena distinguere i tre casi, perché solo il primo e il terzo sono garanzie, e trattare il secondo come una garanzia è il modo in cui si riaprono i guasti già chiusi.

Che cosa adesso sai fare

Sai leggere i cinque campi di una riga di crontab dalla documentazione che sta sulla tua macchina, e sai che giorno del mese e giorno della settimana insieme si combinano in modo controintuitivo. Sai che il manuale non promette un PATH ai lavori periodici, e sai che la conseguenza è scrivere percorsi assoluti invece di cercare il valore giusto.

Sai validare un'espressione temporale prima di aspettare, e hai visto con una misura che «il trentuno» non è «l'ultimo giorno del mese». Sai scegliere fra una riga di crontab e un timer in base a quattro differenze concrete. La verifica concreta: prendi un lavoro periodico che esiste sulla tua macchina e controlla se la sua uscita finisce in un posto dove la ritroveresti.