automation

Cron job: sintaxa crontab, capcanele DST și alternativele moderne

Un cron job execută o comandă la intervale definite în crontab. Sintaxa celor cinci câmpuri, capcanele DST și container restart, și când n8n bate cron clasic.

Cuprins

Un cron job este o sarcină programată să ruleze automat la intervale definite pe sisteme Unix. Configurarea se face în crontab (prescurtare de la cron table), un fișier text în care fiecare linie descrie când și ce comandă să execute demonul cron. Deși simplu la suprafață, cron ascunde capcane reale: timezone-uri ambigue, execuții pierdute la restart, output care dispare fără urmă.

Termenul vine de la cuvântul grec pentru timp (kronos). Sintaxa de bază nu s-a schimbat semnificativ din Unix v7 (1979), ceea ce asigură portabilitate dar și absența garanțiilor moderne: niciun retry, nicio persistență, nicio vizibilitate.

Ce este un cron job mai exact?

O intrare în crontab asociază un program cron (demonul care rulează în fundal) cu o comandă de executat și un program de timp. Demonul verifică crontab-ul în fiecare minut și lansează comenzile al căror program se potrivește cu minutul curent.

Câteva caracteristici care definesc cron față de alte schedulere:

  • Fără persistență. Cron nu știe dacă serverul era oprit când ar fi trebuit să ruleze. Execuțiile pierdute sunt pierdute definitiv.
  • Fără retry. Dacă comanda eșuează, cron nu o mai rulează. Trebuie să implementezi retry explicit în script.
  • Fără dependențe între joburi. Cron nu știe că jobul B trebuie să ruleze după jobul A. Coordonarea e responsabilitatea ta.
  • Output implicit ignorat. Dacă nu redirecționezi stderr, cron trimite output-ul prin email local (rareori configurat pe serverele moderne). Erorile dispar fără urmă.

Care e sintaxa crontab (cele cinci câmpuri plus extensiile @daily, @weekly)?

O linie crontab are forma minut oră zi-lună lună zi-săptămână comandă. Fiecare câmp acceptă valori exacte, * (orice valoare), */N (la fiecare N unități), sau liste separate prin virgulă. Exemple: 0 3 * * * rulează zilnic la 03:00; */15 * * * * la fiecare 15 minute; 0 9 * * 1 luni la 09:00. Câmpul oră folosește timezone-ul sistemului, ceea ce introduce problema DST descrisă mai jos.

Extensiile @daily, @weekly, @monthly și @reboot sunt prescurtări acceptate pe majoritatea distribuțiilor moderne. @daily echivalează cu 0 0 * * * (miezul nopții). @reboot rulează o singură dată la pornirea sistemului. Sunt mai lizibile decât forma numerică, dar comportamentul exact depinde de implementarea cron; verifică documentația dacă portabilitatea contează.

Care sunt greșelile clasice (DST, container restart, fără output, suprapunere)?

  • Timezone ambiguu, mai ales la tranziția DST. Dacă serverul rulează în Europa/Bucharest și ai un job la 0 2 * * *, la trecerea la/de la ora de vară există o oră care apare de două ori sau dispare complet. Jobul rulează de două ori sau nu rulează deloc. Soluția simplă: setează CRON_TZ=UTC în crontab sau configureaz timezone-ul sistemului la UTC. UTC nu are tranziție DST, problema dispare.
  • Container restart resetează state in-memory. Dacă implementezi logica de deduplicare sau de lock direct în memorie (variabile globale, un fișier temp în tmpfs), un restart de container sau o repornire a procesului cron pierde acea stare. Lockfile-urile pe disc persistent sau un entry în baza de date sunt singurele opțiuni fiabile pentru coordonare.
  • Output silent. O comandă care eșuează fără să redirecționezi stderr undeva vizibil dispare. Adaugă cel puțin 2>>/var/log/job-name.log la fiecare comandă sau folosește un wrapper care scrie în syslog (logger). Fără logging, nu știi că jobul tău a eșuat timp de zile.
  • Suprapunere când jobul durează mai mult decât intervalul. Un job configurat la fiecare 5 minute care în realitate rulează 7 minute produce două instanțe paralele. Dacă jobul face scriere în baza de date sau procesează un fișier comun, rezultatul e corupție sau dubluri. Soluțiile standard: flock pentru excludere mutuală sau rescrierea jobului ca să fie idempotent și să detecteze o execuție activă la start.
  • Variabile de mediu lipsă. Cron rulează cu un environment minim, fără variabilele definite în ~/.bashrc, /etc/environment sau configurația shell-ului. Dacă comanda ta depinde de PATH, DATABASE_URL, sau orice altă variabilă, declară-le explicit în crontab sau folosește un script wrapper care le încarcă dintr-un fișier.

Când n8n sau un scheduler dedicat e mai potrivit decât cron clasic?

Cron rămâne valabil pentru sarcini simple, izolate: rotație de log-uri, sincronizare backup, healthcheck periodic. Când sarcina are nevoie de retry la eșec, notificare la eroare, dependențe între etape, sau când vrei să știi dacă a rulat fără să te uiți la log-uri, cron clasic nu mai ajunge.

Criteriile concrete care indică trecerea la un orchestrator:

  • Retry automat. Dacă un API extern întoarce 503 la 3 dimineața, cron nu mai încearcă. Un orchestrator cu politică de retry (circuit breaker inclus) reia execuția după un interval.
  • Dependențe între pași. Pasul B rulează numai dacă pasul A a terminat cu succes. Cron nu modelează asta. Ai nevoie de un workflow engine.
  • Vizibilitate la execuție. Cron nu îți arată o listă cu execuțiile din ultima săptămână, ce a rulat, cât a durat, ce a eșuat. Un orchestrator are UI sau API pentru asta.
  • State între execuții. Jobul trebuie să știe ce a procesat ultima dată pentru a continua de unde a rămas. Cron nu persistă nimic; trebuie să implementezi asta tu în scriptul propriu-zis.

Pentru orchestrarea sarcinilor periodice care depind de stare sau trebuie observabile (cu retry și notificare la eșec), folosim n8n cu metrici Prometheus expuse pe endpoint dedicat. Cron clasic rămâne valabil pentru sarcini simple, izolate (rotație log-uri, sincronizare backup, healthcheck periodic), unde overhead-ul unui orchestrator dedicat nu se justifică1. Alternativele la n8n pentru echipe cu cerințe mai mari: Airflow pentru pipeline-uri de date complexe, Temporal pentru workflows cu stare distribuită și garanții de execuție stricte. Cloud schedulers (AWS EventBridge, Google Cloud Scheduler) sunt opțiunea gestionată dacă infrastructura ta e deja în cloud.

Cum verifici că rulează când nu te uiți (heartbeat, dead man switch)?

Problema centrală a cron-ului este că eșecul tăcut este default. Un job care nu rulează nu produce nicio alertă, niciun log vizibil, nicio notificare. Știi că ceva e stricat doar când consecința devine vizibilă (backupul care nu există, raportul care nu a ajuns, baza de date care nu a fost curățată).

Soluția este un dead man switch sau heartbeat: jobul semnalează că a terminat cu succes, iar un sistem extern alertează dacă semnalul nu vine în fereastra așteptată. Implementări practice:

  • Serviciu dedicat de cron monitoring. Healthchecks.io și altele similare funcționează simplu: creezi un check cu o perioadă așteptată, jobul face un request HTTP la un URL unic după execuție cu succes, serviciul alertează dacă URL-ul nu e apelat în fereastra configurată. Setup de cinci minute.
  • Endpoint intern de heartbeat. Dacă ai deja un stack de observabilitate, adaugă o regulă de alertare care se declanșează dacă timestamp-ul ultimului ping e mai vechi decât intervalul jobului plus o marjă.
  • Runbook pentru fiecare job critic. Fiecare job periodic cu consecințe la eșec trebuie să aibă un runbook asociat: ce face, ce se întâmplă dacă nu rulează, cum îl pornești manual. Fără runbook, incidentul de la 3 dimineața devine o investigație de o oră în loc de cinci minute.

Asigură-te că jobul este idempotent: dacă rulează de două ori consecutiv din cauza unui restart sau a unui dead man switch prea agresiv, rezultatul trebuie să fie identic, fără duplicări. Dacă jobul produce date pentru un flux downstream, vezi și diferența dintre queue și stream pentru a decide cum livrezi output-ul.

  1. n8n-ul crawlerra are endpoint-ul Prometheus activat via N8N_METRICS=true, scraped de Prometheus la interval de 30 de secunde. [crawlerra.n8n_metrics]

Întrebări frecvente

Ce se întâmplă cu un cron job dacă serverul repornește?

Un cron job nu rulează pentru intervalele ratate în timp ce serverul era oprit. Spre deosebire de un scheduler cu persistență, cron clasic nu ține evidența execuțiilor pierdute. Dacă serverul a fost oprit 30 de minute și aveai un job care trebuia să ruleze la fiecare 15, ambele execuții sunt pierdute fără nicio notificare. Dacă asta e o problemă, ai nevoie de un orchestrator cu state, nu cron.

Cum evit suprapunerea execuțiilor unui cron job?

Folosește flock sau un mecanism de lockfile ca să previi execuțiile paralele. Comanda flock -n /var/lock/job.lock comanda-ta sare execuția nouă dacă precedenta rulează încă. Alternativ, rescrie jobul ca să fie idempotent și să detecteze o execuție activă la start. În schedulere moderne (systemd timers, n8n), protecția la suprapunere e configurabilă fără cod suplimentar.

Care e diferența dintre cron job și systemd timer?

systemd timer e mai expresiv și mai integrat cu sistemul de operare. Suportă calendare relative (OnBootSec, OnCalendar), activare la eveniment de sistem, și logging nativ prin journald fără redirect manual. Dezavantajul: necesită două fișiere (.timer + .service) față de o singură linie în crontab. Pentru sarcini simple, cron e suficient; pentru sarcini cu dependențe de sistem sau logging serios, systemd timers câștigă.

Pot rula cron job-uri în containere Docker?

Da, dar cu precauție la câteva capcane specifice containerelor. Variabilele de mediu definite de Docker nu sunt automat disponibile în cron (trebuie exportate explicit sau scrise într-un fișier env). Și containerul nu are garantat cron instalat dacă e bazat pe o imagine minimală. O alternativă mai curată: un container dedicat de scheduler care apelează API-uri sau scripturi în celelalte containere.

Cum monitorizez că un cron job a rulat cu succes?

Cu un dead man switch sau heartbeat: dacă jobul nu trimite semnalul așteptat în fereastra de timp, primești alertă. Servicii dedicate (Healthchecks.io, Better Uptime Cron) așteaptă un ping HTTP după fiecare execuție și alertează la tăcere. Varianta self-hosted: fiecare job trimite o cerere HTTP la un endpoint de monitoring imediat după execuția cu succes. Absența ping-ului e eșecul detectat.