Timestamps Unix în task-uri programate și joburi cron

Cron joburile par simple la suprafață: rulează comanda asta la ora asta. Dar programarea în producție se complică repede. Trebuie să urmărești când a rulat ultima dată un job, să detectezi dacă este întârziat, să gestionezi rulări suprapuse și să loghezi totul cu suficientă precizie încât să poți depana eșecuri ulterior. Timestamps-urile Unix sunt instrumentul practic pentru toate acestea.

Folosește Convertorul de timestamp Unix ca să transformi orice timestamp într-o dată lizibilă sau ca să obții timpul Unix curent. Acest articol acoperă cum sunt folosite timestamp-urile în sisteme de programare — de la cron de bază la cozi de joburi mai complexe.

De ce task-urile programate folosesc timestamps Unix

O expresie cron precum 0 2 îi spune scheduler-ului când* să ruleze un job. Dar nu îți spune nimic despre ce s-a întâmplat în timpul rulării. Pentru asta ai nevoie de timestamps înregistrate la runtime: când a pornit jobul, când s-a terminat, dacă a reușit.

Timestamps-urile Unix sunt un format natural pentru asta deoarece sunt:

  • Neambigue — fără confuzie de fus orar, fără formatare dependentă de limbă/locale
  • Comparabile — poți scădea două timestamps ca să obții secunde scurse
  • Compacte — un singur întreg de stocat în orice coloană de DB sau linie de log
  • Sortabile — ordonarea numerică corespunde ordinii cronologice

Un task programat care scrie started_at: 1712534400 într-o bază de date înregistrează ceva precis și ușor de interogat. Un task care scrie started_at: "8 aprilie 2026 la 2:00 AM" înregistrează ceva ce trebuie pars-at ca să devină util.

Stocarea timestamp-ului ultimei rulări

Cea mai comună utilizare este urmărirea ultimei rulări reușite. Un pattern simplu:

1. Jobul pornește și citește timestamp-ul last_run din stocare 2. Jobul își face treaba 3. La succes, scrie timestamp-ul Unix curent în last_run 4. La eșec, lasă last_run neschimbat (sau scrie într-un câmp separat last_failed_at)

# Shell example
LAST_RUN=$(cat /var/run/myjob.timestamp 2>/dev/null || echo 0)
NOW=$(date +%s)

# Do work here...
if [ $? -eq 0 ]; then
    echo $NOW > /var/run/myjob.timestamp
fi

Acest model face trivial să răspunzi la “când a reușit ultima dată jobul?” — citești fișierul și convertești timestamp-ul. De asemenea, ajută la detectarea “staleness”: dacă now - last_run > expected_interval, jobul este întârziat.

Detectarea joburilor întârziate sau ratate

Cron nu își detectează propriile eșecuri. Dacă un server este down la momentul programat, cron nu reîncearcă și nu alertează. Dacă un job rulează dar iese cu eroare, cron nu îl marchează ca “failed”. Detectarea acestor situații necesită monitorizare externă care verifică timestamps.

Un check simplu: dacă timpul curent minus last_run depășește un prag, ceva a mers prost.

import time

EXPECTED_INTERVAL_SECONDS = 3600  # job should run hourly
TOLERANCE_SECONDS = 300           # allow 5 minutes of drift

last_run = get_last_run_timestamp()  # from DB or file
now = int(time.time())

if now - last_run > EXPECTED_INTERVAL_SECONDS + TOLERANCE_SECONDS:
    alert("Job overdue — last ran at {}".format(last_run))

Toleranța contează. Joburile cron nu pornesc întotdeauna fix la secundă — încărcarea sistemului și timpul de pornire înseamnă că un job setat la 02:00:00 poate începe la 02:00:04. Fără toleranță, un check care rulează la 02:59:56 poate raporta fals că jobul este întârziat, deși a rulat corect la 02:00:04 și urmează să ruleze din nou în 4 secunde.

Pentru joburi orare, 5 minute este o toleranță rezonabilă. Pentru joburi zilnice, 30–60 minute este tipic.

Prevenirea rulărilor suprapuse cu timestamps

Joburile care rulează mult se pot suprapune dacă următoarea execuție pornește înainte să se termine cea curentă. Un backup zilnic care durează 2 ore e OK. Unul care durează 26 de ore începe să se suprapună și, în timp, poate produce eșecuri în lanț.

Timestamps rezolvă asta cu un pattern de lock simplu:

1. Jobul pornește și citește started_at dintr-un record de lock 2. Dacă started_at există și now - started_at < timeout, există o rulare în curs — ieși 3. Dacă nu există lock sau lock-ul e expirat, scrie timestamp-ul curent ca started_at 4. Rulează jobul 5. La final, eliberează lock-ul

LOCK_TIMEOUT = 7200  # 2 hours — if job runs longer, assume it's stuck

lock_time = get_lock_timestamp()
now = int(time.time())

if lock_time and (now - lock_time) < LOCK_TIMEOUT:
    print("Job already running, started at {}".format(lock_time))
    exit(0)

set_lock_timestamp(now)
# ... do work ...
clear_lock()

Timeout-ul acoperă cazul în care jobul se prăbușește fără să elibereze lock-ul. Fără timeout, un crash ar bloca toate rulările viitoare la nesfârșit. Cu un lock bazat pe timestamp, lock-ul “expiră” după timeout și următoarea execuție poate continua.

Programarea joburilor viitoare cu timestamps

Cozi de joburi (Sidekiq, Celery, BullMQ, RQ) programează adesea task-uri pentru viitor stocând un timestamp Unix pentru momentul execuției. Worker-ul caută joburi unde run_at <= current_timestamp.

-- Find jobs ready to run
SELECT * FROM scheduled_jobs
WHERE run_at <= EXTRACT(EPOCH FROM NOW())::int
  AND status = 'pending'
ORDER BY run_at ASC;

Acest model e mai flexibil decât cron pentru programări dinamice. În loc de “rulează la fiecare oră”, poți spune “rulează la 24 de ore după înscriere” sau “rulează la 15 minute după ce plata a eșuat”. Jobul e inserat cu run_at = NOW_UNIX + delay_seconds, iar worker-ul îl preia la timp.

Și retry-urile funcționează la fel. După un eșec, reprogramezi cu backoff exponențial:

attempt = job.attempt_count
delay = min(2 ** attempt * 60, 3600)  # 1min, 2min, 4min... up to 1hr
job.run_at = int(time.time()) + delay
job.save()

După 1 încercare: reîncearcă în 60 secunde. După 2: 120 secunde. După 3: 240 secunde. Cap-ul la 3600 previne întârzieri care cresc la nesfârșit.

Logarea rulărilor cu timestamps Unix

Logurile sunt cel mai utile când includ timpi exacți. O linie de log precum:

[1712534400] backup-job started
[1712534447] backup-job completed in 47s, 3.2GB written

este imediat utilă la depanare. Poți converti 1712534400 într-o dată lizibilă cu Convertorul de timestamp Unix, o poți corela cu alte loguri de sistem și poți calcula durata exactă fără să parsezi șiruri de date.

Alternativa — să loghezi șiruri formatate precum "2026-04-08 02:00:00 UTC" — e lizibilă pentru oameni, dar fragilă pentru mașini. Diferite sisteme formatează diferit, conversiile de fus orar pot introduce erori, iar comparația șirurilor e mai lentă decât comparația întregilor pentru interogări pe intervale de timp.

Un pattern comun este să loghezi ambele: timestamp-ul brut pentru mașini și data formatată pentru oameni.

started_at=1712534400 started_at_human="2026-04-08T02:00:00Z" duration_s=47

Măsurarea duratei și performanței în timp

Timestamps permit urmărirea performanței între rulări. Stochează started_at și completed_at pentru fiecare execuție și poți interoga:

  • Durata medie pe ultimele 30 de rulări
  • Cea mai lungă rulare din ultima săptămână
  • Dacă durata crește în timp (posibil regres de performanță)
SELECT
    AVG(completed_at - started_at) AS avg_duration_seconds,
    MAX(completed_at - started_at) AS max_duration_seconds,
    COUNT(*) AS run_count
FROM job_runs
WHERE job_name = 'nightly-report'
  AND started_at > EXTRACT(EPOCH FROM NOW())::int - (30 * 86400);

Aceste interogări sunt ușoare tocmai pentru că duratele sunt simple scăderi între întregi. Dacă ai stoca date formatate, ai avea nevoie de parsing înainte de aritmetică.

Precizia timestamp-urilor: secunde vs milisecunde

Pentru majoritatea task-urilor programate, precizia la nivel de secundă este suficientă. Un job setat să ruleze la 1712534400 nu are nevoie de precizie sub-secundă.

Dar pentru cozi de task-uri cu frecvență mare — sute de joburi pe secundă — milisecundele contează. Dacă două joburi sunt inserate în aceeași secundă și sortezi doar după timestamp pentru ordinea de procesare, timestamp-urile în secunde produc egalități. Timestamps în milisecunde păstrează ordinea în interiorul aceleiași secunde.

JavaScript Date.now() returnează milisecunde. Python time.time() returnează un float cu precizie de microsecunde. Multe baze de date suportă timestamps cu microsecunde. Alege precizia în funcție de frecvența planificării — milisecunde pentru cozi voluminoase, secunde pentru cron clasic.

Schimbarea orei (DST) și task-urile programate

Aici se ascund multe buguri de programare. Un cron setat 0 2 * rulează la 2:00 AM în ora locală. Când ceasul sare înainte (primăvara), 2:00 AM nu există — trece de la 1:59 AM la 3:00 AM. Jobul fie este sărit, fie rulează la 3:00 AM, în funcție de implementarea cron.

Când ceasul dă înapoi (toamna), 2:00 AM apare de două ori. Jobul poate rula de două ori.

Soluția: rulează cron în UTC. 0 2 * în UTC este întotdeauna 2:00 UTC — fără ambiguitate, fără surprize DST. Ora locală se schimbă de două ori pe an, dar jobul rulează o singură dată.

Timestamps-urile Unix sunt UTC prin definiție, de aceea sunt imune la această problemă. Un job programat la timestamp 1712534400 rulează la acel moment exact, indiferent de fusul orar al serverului sau de offset-ul DST de atunci. Acesta este motivul principal pentru planificare bazată pe timestamps în loc de cron, când contează precizia.

Pentru joburi unde semantica “ora locală” contează — “rulează la 9 AM în programul de lucru” — ai nevoie de gestionare explicită a fusului orar, nu doar UTC. Stochează fusul țintă împreună cu programarea, convertește la UTC când creezi jobul și recalculează dacă se schimbă regulile DST ale fusului.

Articole similare