8 min

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.

Pattern cron + database: lavori in background senza una coda

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 jsonb così 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 queued e failed)

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 LOCKED permette 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.
  • RETURNING consegna 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

Prototype your cron runner
Trasforma questo pattern in un worker eseguibile attivato da cron e in una API rapidamente.

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 failed se attempts < max_attempts, altrimenti dead
  • calcolare run_at per il prossimo tentativo (solo per failed)

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_code o error_type (aiuta a raggruppare)
  • failed_at e next_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_emails con 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_generated indicizzato 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

Own your scheduler code
Esporta il sorgente in qualsiasi momento e mantieni il tuo sistema di job portabile.

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

  1. Crea le tabelle e gli indici. Aggiungi una tabella jobs (più eventuali tabelle di lookup), poi indicizza run_at e aggiungi un indice che aiuti il worker a trovare job disponibili velocemente (per esempio su (status, run_at)).

  2. Scrivi una funzione di enqueue minimale. La tua app dovrebbe inserire una riga con run_at settato 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);
  1. Implementa il claim loop. Eseguilo in una transazione. Seleziona alcuni job dovuti, lockali così gli altri worker li saltano, e marcali come running nella 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 *;
  1. Processa e finalizza. Per ogni job reclamato, esegui il lavoro, poi aggiorna a done con finished_at. Se fallisce, registra un messaggio di errore e rimetti in queued con un nuovo run_at (backoff). Mantieni gli update di finalizzazione piccoli e assicurati di eseguirli sempre, anche se il processo sta spegnendosi.

  2. Aggiungi regole di retry spiegabili. Usa una formula semplice come run_at = now() + (attempts^2) * interval '10 seconds' e fermati dopo max_attempts impostando status = '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_nightly per “oggi”.
  • Al momento della registrazione: enqueue di send_weekly_report per il prossimo lunedì dell'utente.
  • Dopo un evento (per esempio “utente ha cliccato Export report”): enqueue immediato di send_weekly_report per 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

Earn credits while you build
Crea contenuti o invita colleghi e guadagna crediti per continuare a costruire su Koder.ai.

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_id o 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_until e max_attempts (più last_error o 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.

Related posts