Pattern cron + database: lavori in background senza una coda
Scopri il pattern cron + database per eseguire job schedulati con retry, lock e idempotenza — senza mettere in piedi un sistema di code completo.

Il problema: lavoro schedulato senza infrastruttura extra
La maggior parte delle app ha bisogno che del lavoro venga eseguito più tardi o su uno schedule: inviare email di follow-up, eseguire un controllo di fatturazione notturno, pulire record vecchi, ricostruire un report o aggiornare una cache.
All'inizio può sembrare naturale aggiungere un sistema di code completo perché sembra il modo “giusto” di fare background jobs. Ma le code aggiungono parti in movimento: un altro servizio da far girare, monitorare, deployare e debuggare. Per un team piccolo (o un founder da solo), quel peso extra può rallentare.
La vera domanda è: come eseguire lavoro schedulato in modo affidabile senza mettere in piedi altra infrastruttura?
Un primo tentativo comune è semplice: aggiungi un cron che richiama un endpoint e lascia che quell'endpoint faccia il lavoro. Funziona… finché non smette di farlo. Quando hai più di un server, un deploy al momento sbagliato o un job che impiega più del previsto, cominciano a comparire guai confusi.
Il lavoro schedulato di solito si rompe in modi prevedibili:
- Esecuzioni duplicate: due server eseguono lo stesso task, quindi fatture o email vengono inviate due volte.
- Esecuzioni perse: una chiamata cron fallisce durante un deploy e nessuno se ne accorge fino alle lamentele degli utenti.
- Errori silenziosi: il job fallisce una volta e poi non viene più riprovato perché non c'è un piano di retry.
- Lavoro parziale: il job crasha a metà e lascia dati in uno stato strano.
- Nessuna traccia: non puoi rispondere a “quando è stato eseguito l'ultima volta?” o “cosa è successo ieri notte?”.
Il pattern cron + database è una via di mezzo. Continui a usare cron per "svegliarti" secondo uno schedule, ma memorizzi l'intento e lo stato del job nel database in modo che il sistema possa coordinare, fare retry e registrare cosa è successo.
È una buona scelta quando hai già un database (spesso PostgreSQL), un numero ridotto di tipi di job e vuoi comportamento prevedibile con il minimo lavoro operativo. È anche naturale per app costruite velocemente su stack moderni (ad esempio, uno stack React + Go + PostgreSQL).
Non è indicato quando hai bisogno di throughput molto elevato, job di lunga durata che devono streammare progresso, ordinamento rigoroso tra molti tipi di job o un forte fan-out (migliaia di sotto-task al minuto). In quei casi, una coda reale e worker dedicati solitamente valgono l'investimento.
L'idea principale in parole semplici
Il pattern cron + database esegue lavoro in background su uno schedule senza gestire un sistema di code completo. Continui a usare cron (o un qualsiasi scheduler), ma cron non decide cosa eseguire: si limita a svegliare spesso un worker (una volta al minuto è comune). È il database che decide quale lavoro è dovuto e si assicura che un solo worker prenda ogni job.
Pensalo come una checklist condivisa su una lavagna. Cron è la persona che entra nella stanza ogni minuto e chiede: “C'è qualcosa da fare ora?” Il database è la lavagna che mostra cosa è dovuto, cosa è già preso e cosa è fatto.
I pezzi sono semplici:
- Un trigger dello scheduler eseguito frequentemente.
- Una tabella jobs che contiene il “cosa” e il “quando” (run_at), più stato e conteggio tentativi.
- Uno o più worker che interrogano la tabella, reclamano un job e lo eseguono.
- La presa del job usa un lock nel database così due worker non possono prendere la stessa riga.
- Il database rimane la fonte di verità su cosa è stato eseguito, cosa è fallito e cosa va ritentato.
Esempio: vuoi inviare promemoria di fatture ogni mattina, aggiornare una cache ogni 10 minuti e pulire sessioni vecchie di notte. Invece di tre cron separati (ognuno con i propri problemi di overlap e failure), memorizzi voci di job in un unico posto. Cron avvia lo stesso processo worker. Il worker chiede a Postgres: “Cosa è dovuto adesso?” e Postgres risponde permettendo al worker di reclamare in modo sicuro esattamente un job alla volta.
Questo si scala gradualmente. Puoi partire con un worker su un server. Più avanti, puoi eseguire cinque worker su più server. Il contratto resta lo stesso: la tabella è il contratto.
Il cambio di mentalità è semplice: cron è solo la sveglia. Il database è il vigile che decide cosa può girare, registra cosa è successo e ti dà una storia chiara quando qualcosa va storto.
Progettare la tabella jobs (uno schema pratico)
Questo pattern funziona meglio quando il tuo database diventa la fonte di verità su cosa dovrebbe girare, quando dovrebbe girare e cosa è successo l'ultima volta. Lo schema non è sofisticato, ma piccoli dettagli (campi di lock e gli indici giusti) fanno una grande differenza con l'aumentare del carico.
Una tabella o due?
Due approcci comuni:
- Una tabella combinata quando ti interessa solo lo stato più recente di ogni job (semplice, meno join).
- Due tabelle quando vuoi separare chiaramente “cos'è questo job” e “ogni volta che è stato eseguito” (storia migliore, debug più semplice).
Se prevedi di fare spesso debug dei fallimenti, tieni la storia. Se vuoi la configurazione più piccola possibile, inizia con una tabella e aggiungi la storia dopo.
Uno schema pratico (versione a due tabelle)
Qui c'è un layout comodo per PostgreSQL. Se costruisci in Go con PostgreSQL, queste colonne si mappano facilmente a struct.
-- What should exist (the definition)
create table job_definitions (
id bigserial primary key,
job_type text not null,
payload jsonb not null default '{}'::jsonb,
schedule text, -- optional: cron-like text if you store it
max_attempts int not null default 5,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- What should run (each run / attempt group)
create table job_runs (
id bigserial primary key,
definition_id bigint references job_definitions(id),
job_type text not null,
payload jsonb not null default '{}'::jsonb,
run_at timestamptz not null,
status text not null, -- queued | running | succeeded | failed | dead
attempts int not null default 0,
max_attempts int not null default 5,
locked_by text,
locked_until timestamptz,
last_error text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
Alcuni dettagli che evitano problemi in futuro:
- Tieni job_type come una stringa corta su cui puoi fare routing (ad esempio
send_invoice_emails). - Memorizza payload come
jsonbcosì puoi evolverlo senza migrazioni. - run_at è il tuo “prossimo orario dovuto”. Cron (o uno script scheduler) lo imposta, i worker lo consumano.
- locked_by e locked_until permettono ai worker di reclamare job senza pestarsi i piedi.
- last_error dovrebbe essere breve e leggibile. Metti gli stack trace altrove se ti servono.
Indici che vorrai
Senza indici, i worker finiscono per scansionare troppo. Parti con:
- Un indice per trovare lavoro dovuto velocemente:
(status, run_at) - Un indice per aiutare a rilevare lock scaduti:
(locked_until) - Opzionale: un indice parziale per lavoro attivo solo (per esempio, status in
queuedefailed)
Questi mantengono rapida la query “trova il prossimo job eseguibile” anche quando la tabella cresce.
Locking e claim sicuri dei job
L'obiettivo è semplice: molti worker possono girare, ma solo uno deve prendere un job specifico. Se due worker processano la stessa riga ottieni email duplicate, addebiti doppi o dati incasinati.
Un approccio sicuro è trattare la presa del job come un "lease". Il worker marca il job come lockato per una finestra breve. Se il worker crasha, il lease scade e un altro worker può prenderlo. Questo è il senso di locked_until.
Usa un lease così i crash non bloccano il lavoro per sempre
Senza un lease, un worker potrebbe lockare un job e non sbloccarlo mai (processo killato, reboot del server, deploy andato storto). Con locked_until, il job diventa nuovamente disponibile quando il tempo passa.
Una regola tipica: un job può essere reclamato quando locked_until è NULL o locked_until <= now().
Claim dei job con un singolo update atomico
Il dettaglio chiave è reclamare il job in una singola istruzione (o in una transazione). Vuoi che il database faccia da arbitro.
Ecco un pattern comune in PostgreSQL: prendi un job dovuto, lockalo e ritorna la riga al worker. (Questo esempio usa una tabella jobs; la stessa idea vale se reclami da job_runs.)
WITH next_job AS (
SELECT id
FROM jobs
WHERE status = 'queued'
AND run_at \u003c= now()
AND (locked_until IS NULL OR locked_until \u003c= now())
ORDER BY run_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE jobs j
SET status = 'running',
locked_until = now() + interval '2 minutes',
locked_by = $1,
attempts = attempts + 1,
updated_at = now()
FROM next_job
WHERE j.id = next_job.id
RETURNING j.*;
Perché funziona:
FOR UPDATE SKIP LOCKEDpermette a più worker di competere senza bloccarsi a vicenda.- Il lease è impostato al momento del claim, così gli altri worker lo ignorano fino alla scadenza.
RETURNINGconsegna la riga al worker che ha vinto la gara.
Quanto dovrebbe durare il lease e come rinnovarlo?
Imposta il lease più lungo di una run normale, ma abbastanza corto da recuperare rapidamente da un crash. Se la maggior parte dei job finisce in 10 secondi, un lease di 2 minuti è più che sufficiente.
Per task lunghi, rinnova il lease mentre lavori (heartbeat). Un approccio semplice: ogni 30 secondi estendi locked_until se sei ancora il proprietario del job.
- Lunghezza del lease: 5x a 20x il tempo medio del job
- Intervallo heartbeat: 1/4 a 1/2 del lease
- L'update di rinnovo dovrebbe includere
WHERE id = $job_id AND locked_by = $worker_id
Quest'ultima condizione è importante. Previene che un worker estenda un lease su un job che non possiede più.
Retry e backoff prevedibili
I retry sono il punto in cui questo pattern o è zen o diventa un caos rumoroso. L'obiettivo è semplice: quando un job fallisce, riprovarlo più tardi in modo spiegabile, misurabile e arrestabile.
Inizia rendendo lo stato del job esplicito e finito: queued, running, succeeded, failed, dead. In pratica, molte squadre usano failed per dire “fallito ma verrà ritentato” e dead per “fallito e abbiamo rinunciato”. Quella distinzione evita loop infiniti.
Il conteggio degli tentativi è la seconda barriera. Conserva attempts (quante volte hai provato) e max_attempts (quante volte permetti). Quando un worker cattura un errore, dovrebbe:
- incrementare
attempts - impostare lo stato a
failedseattempts < max_attempts, altrimentidead - calcolare
run_atper il prossimo tentativo (solo perfailed)
Il backoff è semplicemente la regola che decide il prossimo run_at. Scegline una, documentala e mantienila consistente:
- Ritardo fisso: aspetta sempre 1 minuto
- Esponenziale: 1m, 2m, 4m, 8m
- Esponenziale con cap: esponenziale ma mai più di, per esempio, 30m
- Aggiungi jitter: randomizza un po' così i job non ritentano tutti nello stesso secondo
Il jitter conta quando una dipendenza va giù e poi torna su. Senza jitter, centinaia di job possono ritentare tutti insieme e fallire di nuovo.
Conserva dettagli sugli errori sufficienti per rendere i fallimenti visibili e debugabili. Non ti serve un sistema di logging completo, ma le basi:
last_error(messaggio breve, sicuro da mostrare in una schermata admin)error_codeoerror_type(aiuta a raggruppare)failed_atenext_run_at- opzionale
last_stack(solo se ne controlli la dimensione)
Una regola concreta che funziona: marca i job come dead dopo 10 tentativi e usa backoff esponenziale con jitter. Così i fallimenti transitori vengono ritentati, ma i job rotti non consumano CPU per sempre.
Idempotenza: prevenire duplicati anche se un job viene ripetuto
Idempotenza significa che il tuo job può essere eseguito due volte e dare comunque lo stesso risultato finale. In questo pattern conta perché la stessa riga potrebbe essere ripresa dopo un crash, un timeout o un retry. Se il tuo job è “invia un'email di fattura”, eseguirlo due volte non è innocuo.
Un modo pratico per pensarci: dividi ogni job in (1) fare il lavoro e (2) applicare un effetto. Vuoi che l'effetto avvenga una sola volta, anche se il lavoro viene tentato più volte.
Usa una chiave di idempotenza legata all'evento di business
Una idempotency key dovrebbe venire da ciò che il job rappresenta, non dal tentativo del worker. Chiavi buone sono stabili e facili da spiegare, come invoice_id, user_id + day o report_name + report_date. Se due tentativi di job si riferiscono allo stesso evento del mondo reale, dovrebbero condividere la stessa chiave.
Esempio: “Genera il report vendite giornaliero per 2026-01-14” può usare sales_report:2026-01-14. “Addebita la fattura 812” può usare invoice_charge:812.
Imporre “solo una volta” con vincoli di database
La guardia più semplice è lasciare che PostgreSQL rifiuti i duplicati. Memorizza la chiave di idempotenza in un campo indicizzabile, poi aggiungi un vincolo unico.
-- Example: ensure one logical job/effect per business key
ALTER TABLE jobs
ADD COLUMN idempotency_key text;
CREATE UNIQUE INDEX jobs_idempotency_key_uniq
ON jobs (idempotency_key)
WHERE idempotency_key IS NOT NULL;
Questo impedisce l'esistenza di due righe con la stessa chiave nello stesso momento. Se il tuo design permette più righe (per la storia), metti l'unicità su una tabella "effects" separata, ad esempio sent_emails(idempotency_key) o payments(idempotency_key).
Effetti collaterali comuni da proteggere:
- Email: crea una riga
sent_emailscon una chiave unica prima di inviare, o registra l'ID del provider una volta inviato. - Webhook: memorizza
delivered_webhooks(event_id)e salta se esiste. - Pagamenti: usa sempre la funzione di idempotenza del provider di pagamenti oltre al tuo vincolo unico.
- Scritture su file: scrivi su un nome temporaneo, poi rinomina, o memorizza un record
file_generatedindicizzato su(type, date).
Se costruisci su uno stack con Postgres (per esempio, un backend Go + PostgreSQL), questi controlli di unicità sono veloci e facili da tenere vicini ai dati. L'idea chiave è semplice: i retry sono normali, i duplicati sono opzionali.
Passo-passo: costruire un worker e uno scheduler minimi
Scegli un runtime banale e attieniti a quello. Lo scopo del pattern cron + database è avere meno parti in movimento, quindi un piccolo processo in Go, Node o Python che parla con PostgreSQL è di solito sufficiente.
Costruiscilo in cinque passi piccoli
-
Crea le tabelle e gli indici. Aggiungi una tabella
jobs(più eventuali tabelle di lookup), poi indicizzarun_ate aggiungi un indice che aiuti il worker a trovare job disponibili velocemente (per esempio su(status, run_at)). -
Scrivi una funzione di enqueue minimale. La tua app dovrebbe inserire una riga con
run_atsettato a “now” o a un tempo futuro. Mantieni il payload piccolo e prevedibile (ID e tipo job, non blob enormi).
INSERT INTO jobs (type, payload, status, run_at, attempts, max_attempts)
VALUES ($1, $2::jsonb, 'queued', $3, 0, 10);
- Implementa il claim loop. Eseguilo in una transazione. Seleziona alcuni job dovuti, lockali così gli altri worker li saltano, e marcali come
runningnella stessa transazione.
WITH picked AS (
SELECT id
FROM jobs
WHERE status = 'queued' AND run_at \u003c= now()
ORDER BY run_at
FOR UPDATE SKIP LOCKED
LIMIT 10
)
UPDATE jobs
SET status = 'running', started_at = now()
WHERE id IN (SELECT id FROM picked)
RETURNING *;
-
Processa e finalizza. Per ogni job reclamato, esegui il lavoro, poi aggiorna a
doneconfinished_at. Se fallisce, registra un messaggio di errore e rimetti inqueuedcon un nuovorun_at(backoff). Mantieni gli update di finalizzazione piccoli e assicurati di eseguirli sempre, anche se il processo sta spegnendosi. -
Aggiungi regole di retry spiegabili. Usa una formula semplice come
run_at = now() + (attempts^2) * interval '10 seconds'e fermati dopomax_attemptsimpostandostatus = 'dead'.
Aggiungi visibilità di base
Non ti serve una dashboard completa il primo giorno, ma ti serve abbastanza per notare i problemi.
- Logga una riga per job: claimed, succeeded, failed, retried, dead.
- Crea una query o view semplice per “dead jobs” e “running jobs vecchi”.
- Aggiungi alert sui conteggi (per esempio, più di N dead jobs nell'ultima ora).
Se sei già su uno stack Go + PostgreSQL, questo si mappa pulitamente a un singolo binario worker più cron.
Un esempio realistico che puoi copiare
Immagina una piccola app SaaS con due lavori schedulati:
- Una pulizia notturna che rimuove sessioni scadute e file temporanei vecchi.
- Una email settimanale “il tuo report di attività” inviata a ogni utente ogni lunedì mattina.
Mantieni semplice: una tabella PostgreSQL per i job e un worker che gira ogni minuto (attivato da cron). Il worker reclama i job dovuti, li esegue e registra successo o fallimento.
Cosa viene enqueued e quando
Puoi enqueuere job da diversi punti:
- Ogni giorno alle 02:00: enqueue di un job
cleanup_nightlyper “oggi”. - Al momento della registrazione: enqueue di
send_weekly_reportper il prossimo lunedì dell'utente. - Dopo un evento (per esempio “utente ha cliccato Export report”): enqueue immediato di
send_weekly_reportper un range di date specifico.
Il payload è solo il minimo che il worker serve. Mantienilo piccolo così è facile da retryare.
{
"type": "send_weekly_report",
"payload": {
"user_id": 12345,
"date_range": {
"from": "2026-01-01",
"to": "2026-01-07"
}
}
}
Come l'idempotenza previene l'invio doppio
Un worker può crashare nel momento peggiore: subito dopo aver inviato l'email, ma prima di marcare il job come “done”. Al riavvio potrebbe riprendere lo stesso job.
Per evitare doppie inviate, dai al lavoro una chiave naturale di deduplica e memorizzala dove il database la può far rispettare. Per i report settimanali, una buona chiave è (user_id, week_start_date). Prima di inviare, il worker registra “sto per inviare il report X”. Se quel record esiste già, salta l'invio.
Questo può essere semplice come una tabella sent_reports con un vincolo unico su (user_id, week_start_date), o una idempotency_key unica direttamente sul job.
Come si presenta un fallimento (e come si riprende)
Supponiamo che il provider email faccia timeout. Il job fallisce, quindi il worker:
- incrementa
attempts - salva il messaggio di errore per il debug
- programma il prossimo tentativo con backoff (per esempio: +1 min, +5 min, +30 min, +2 ore)
Se continua a fallire oltre il limite (per esempio 10 tentativi), marca il job come “dead” e smette di ritentare. Il job o riesce una volta, o ritenta secondo una tabella chiara, e l'idempotenza rende il retry sicuro.
Errori comuni e trappole
Il pattern cron + database è semplice, ma piccoli errori possono trasformarlo in duplicati, lavoro bloccato o carico a sorpresa. La maggior parte dei problemi arriva dopo il primo crash, deploy o picco di traffico.
Errori che causano duplicati o job bloccati
La maggior parte degli incidenti reali deriva da poche trappole:
- Eseguire lo stesso job da più entry cron senza un lease. Se due server scattano nello stesso minuto, possono entrambi reclamare lo stesso lavoro a meno che il claim sia atomico e imposti un lock (o lease) nella stessa transazione.
- Saltare
locked_until. Se un worker crasha dopo aver reclamato un job, quella riga può restare “in progress” per sempre. Un timestamp di lease permette a un altro worker di prenderlo più tardi. - Retentare istantaneamente su failure. Quando un'API è giù, retry istantanei generano picchi, consumano rate limit e continuano a fallire in loop. Programma sempre il prossimo tentativo nel futuro.
- Trattare “at least once” come “exactly once”. Un job può essere eseguito due volte (timeouts, restart del worker, problemi di rete). Se eseguirlo due volte è dannoso, rendi gli effetti laterali sicuri da ripetere.
- Memorizzare payload enormi nella riga del job. Blob JSON grandi gonfiano la tabella, rallentano gli indici e rendono il locking più pesante. Memorizza un riferimento (es.
user_id,invoice_ido una chiave file) e recupera il resto al momento dell'esecuzione.
Esempio: invii una email settimanale di fattura. Se il worker va in timeout dopo l'invio ma prima di marcare il job come fatto, lo stesso job può essere ritentato e inviare una email duplicata. Questo è normale per il pattern a meno che tu non aggiunga una guardia (per esempio, registra un evento "email inviata" unico per invoice id).
Trappole meno ovvie
Evita di mescolare scheduling ed esecuzione nella stessa lunga transazione. Se tieni una transazione aperta mentre fai chiamate di rete, tieni lock più a lungo del necessario e blocchi altri worker.
Fai attenzione alle differenze di orologio fra le macchine. Usa il tempo del database (NOW() in PostgreSQL) come fonte di verità per run_at e locked_until, non l'orologio dell'app server.
Imposta un tempo massimo di esecuzione chiaro. Se un job può impiegare 30 minuti, fai il lease più lungo di quello e rinnovalo se serve. Altrimenti un altro worker potrebbe prenderlo a metà esecuzione.
Mantieni la tabella jobs sana. Se le righe completate si accumulano per sempre, le query rallentano e la contesa dei lock aumenta. Scegli una regola semplice di retention (archivia o cancella le righe vecchie) prima che la tabella diventi enorme.
Lista di controllo rapida e prossimi passi
Lista di controllo rapida
Prima di mettere in produzione questo pattern, verifica le basi. Una piccola omissione qui di solito si trasforma in job bloccati, duplicati a sorpresa o un worker che martella il database.
- La tabella jobs ha l'essenziale:
run_at,status,attempts,locked_untilemax_attempts(piùlast_erroro simile per vedere cosa è successo). - Ogni job può essere eseguito due volte senza danni. Se non sei sicuro, aggiungi una chiave di idempotenza o una regola di unicità attorno all'effetto (per esempio, una sola fattura per
invoice_id). - C'è un posto chiaro per osservare i fallimenti e decidere cosa fare: vista dei job falliti, rilanciare un job o marcare come dead quando va fermato.
- Il timeout del lease è sensato per il lavoro. Deve essere abbastanza lungo per run normali, ma abbastanza corto perché crash non blocchino per ore.
- Il backoff dei retry è prevedibile. Deve rallentare i fallimenti ripetuti e fermarsi dopo
max_attempts.
Se tutto questo è vero, il pattern cron + database è solitamente stabile per carichi reali.
Prossimi passi
Una volta che la checklist è OK, concentrati sull'operatività quotidiana.
- Aggiungi due azioni admin semplici: “retry now” (imposta
run_at = now()e pulisce il lock) e “cancel” (sposta in uno stato terminale). Queste salvano tempo durante gli incidenti. - Fai sì che il worker logghi una riga per job: tipo job, id job, numero tentativo e risultato. Aggiungi un alert su conteggi di fallimenti in crescita.
- Testa il carico con uno spike realistico: molti job schedulati per lo stesso minuto. Se il claim dei job diventa lento, aggiungi l'indice giusto (spesso su
status, run_at).
Se vuoi costruire questo setup velocemente, Koder.ai (koder.ai) può aiutarti ad andare dallo schema a un'app Go + PostgreSQL deployata con meno wiring manuale, mentre tu ti concentri su locking, retry e regole di idempotenza.
Se in futuro questo setup non ti bastasse, avrai comunque capito chiaramente il lifecycle dei job e le stesse idee si trasferiscono facilmente a un sistema di code completo.
Domande frequenti
Quando dovrei usare cron con un database per i job in background?
Usa questo schema quando hai già un database, solo un numero limitato di tipi di job e vuoi pianificare attività senza gestire una coda separata. È adatto a promemoria via email, pulizie, report e aggiornamenti della cache.
Che cosa fa cron in questa configurazione?
In questa configurazione, cron deve solo avviare il worker a intervalli regolari, spesso una volta al minuto. Il database decide quali job sono in scadenza, ne registra lo stato e consente a un worker di acquisire ogni job.
Quali campi dovrebbe includere una tabella dei job?
Memorizza il tipo di job, un piccolo payload JSON, l'orario di esecuzione, lo stato, il numero di tentativi, il limite di tentativi, il proprietario del lock, la scadenza del lock e un breve messaggio di errore. Questi campi forniscono ai worker informazioni sufficienti per eseguire e ripristinare i job in sicurezza.
Come evitano più worker di eseguire lo stesso job?
Acquisisci i job in un'unica transazione di database con il locking delle righe, ad esempio PostgreSQL FOR UPDATE SKIP LOCKED. Aggiorna la riga allo stato in esecuzione e imposta il lease prima che il worker avvii l'attività effettiva.
Perché i job in background necessitano di un timeout del lease?
Un lease è un lock temporaneo registrato con locked_until. Se un worker si arresta in modo anomalo, il lease scade e un altro worker può prendere il job invece di lasciarlo bloccato per sempre.
Come dovrebbero riprovare i job non riusciti?
Riprova più tardi seguendo una chiara regola di backoff, ad esempio 1 minuto, 2 minuti, 4 minuti, poi un ritardo limitato con una piccola componente casuale. Dopo il numero di tentativi consentito, contrassegna il job come non recuperabile per evitare che continui a consumare tempo dei worker.
Che cos'è l'idempotenza e perché è importante?
Rendi sicuro ripetere l'effetto collaterale. Ad esempio, usa una chiave di idempotenza stabile, come un ID fattura oppure un ID utente più la data del report, quindi imponi l'unicità in PostgreSQL o presso il fornitore esterno.
Questo schema garantisce l'elaborazione esattamente una volta?
No. Un worker può arrestarsi in modo anomalo dopo aver inviato un'email o chiamato un fornitore di pagamenti, ma prima di salvare l'esito positivo. Progetta per un'elaborazione almeno una volta, quindi usa l'idempotenza per evitare effetti duplicati.
Un worker dovrebbe mantenere aperta una transazione di database mentre esegue il job?
Mantieni brevi le transazioni di database. Acquisisci il job, esegui il commit, effettua le chiamate di rete fuori dalla transazione, quindi salva l'esito positivo o negativo con un aggiornamento separato. Le transazioni lunghe mantengono i lock e rallentano gli altri worker.
Come posso monitorare cron e i job nel database?
Tieni traccia dei job non recuperabili e dei job in esecuzione da troppo tempo, registra ogni acquisizione e risultato, e invia avvisi quando aumentano gli errori. Aggiungi semplici azioni amministrative per riprovare subito un job, annullarlo o controllare il suo ultimo errore.