@generazioneai/jobs
v0.1.0
Published
Cron ai servizi proprietari: @Job con lock consultivo di transazione nel database del servizio (una sola esecuzione per giro fra le repliche), registro cron_runs, JOBS_ENABLE/JOBS_DISABLE e metriche OpenTelemetry (spec «Fondamenta enterprise» §2.4).
Readme
@generazioneai/jobs
Cron ai servizi proprietari (spec «Fondamenta enterprise» §2.4, decisione 12). Un metodo con @Job gira in una sola replica per giro, con il lock nel database del servizio (niente Redis), e ogni giro finisce in cron_runs.
npm i @generazioneai/jobs// app.module.ts
JobsModule.forRoot({ prisma: PrismaService })
// billing-jobs.service.ts
@Injectable()
export class BillingJobs {
@Job('billing.expireEntitlements', '23 * * * *', { tz: 'UTC', lockTtl: '5m' })
async expire() { /* ... */ }
// spostato dal gateway: parte solo se il nome è in JOBS_ENABLE
@Job('billing.reconcilePending', '41 * * * *', { defaultEnabled: false })
async reconcile() { /* ... */ }
}Come garantisce una sola esecuzione
- Il giro apre una transazione e prende
pg_try_advisory_xact_lock(hashtext('genai.jobs'), hashtext(<nome>)). Chi non lo prende salta (locked). La transazione resta aperta per tutto il giro con timeout =lockTtl: se il job la supera, il lock decade (il giro si segnalock_expired). - Con il lock preso, se
cron_runs.last_run_atè già dentro lo slot del tick (minuto, o secondo con espressioni a sei campi) il giro è già stato fatto da un'altra replica e si salta (already_ran): un job rapido non gira due volte perché la seconda replica scatta qualche millisecondo dopo. running_sinceelast_run_atsi scrivono fuori dalla transazione (autocommit): la salute vede il giro mentre gira.
Un giro tiene due connessioni (la transazione del lock e le scritture del registro): il pool di Prisma deve averne almeno quante i job che possono girare insieme, più quelle del job.
Senza la tabella cron_runs i job girano lo stesso (avviso nel log) ma non c'è la guardia «stesso giro», solo il lock.
Accensione
JOBS_ENABLE/JOBS_DISABLE: liste di nomi separate da virgola;billing.*= prefisso,*= tutti.JOBS_DISABLEvince. Di serie contadefaultEnabled(vero se non dichiarato).- G2: i job spostati dal gateway escono con
defaultEnabled: false; si accendono conJOBS_ENABLEnello stesso giro in cuiGATEWAY_CRON_DISABLEli spegne nel gateway. - Un job spento resta lanciabile a mano (
JobsService.runNow(name), che ignora anche la guardia «stesso giro» ma non il lock).
cron_runs
HR e Train hanno già la tabella (name, last_run_at, last_ok_at, last_error, consecutive_failures, running_since, instance): la libreria scrive quelle colonne e usa le colonne aggiunte solo se esistono (schedule, last_duration_ms, runs_total, last_status). Per un database nuovo (Bill, AI, Notify) o per aggiungerle: sql/cron_runs.sql (idempotente, solo additiva, con il rientro in testa) oppure ensureSchema: true / JOBS_ENSURE_SCHEMA=true. In stg e produzione la applica il registro delle SQL del workspace, prima del codice.
I tempi si scrivono con now() del database: nelle colonne timestamp(3) di HR valgono le stesse regole del suo CronRunStore (sessione in UTC).
Metriche (API di OpenTelemetry, esportate da @generazioneai/telemetry)
job_runs_total{job,status=ok|error|skipped}, job_duration_seconds{job}, job_last_success_timestamp{job} (da cron_runs.last_ok_at: vale per tutte le repliche).
Prove
JOBS_TEST_DATABASE_URL=postgresql://… npm test (senza, le prove su Postgres si saltano). Esempio locale: docker run -d -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=jobs_test -p 55432:5432 postgres:18.
