8 min

Pattern cron + base de données : tâches planifiées sans queue

Apprenez le pattern cron + base de données pour exécuter des tâches planifiées avec retries, verrouillage et idempotence — sans déployer un système de queue complet.

Pattern cron + base de données : tâches planifiées sans queue

Le problème : du travail planifié sans infrastructure supplémentaire

La plupart des applications ont besoin que du travail s'exécute plus tard ou selon un planning : envoi d'emails de relance, vérification de facturation nocturne, nettoyage d'anciennes entrées, reconstruction d'un rapport ou rafraîchissement d'un cache.

Au début, il est tentant d'ajouter un système de queue complet parce que ça semble être la « bonne » façon de gérer les tâches en arrière-plan. Mais les queues ajoutent des pièces mobiles : un service supplémentaire à lancer, surveiller, déployer et déboguer. Pour une petite équipe (ou un fondateur solo), ce poids supplémentaire peut vous ralentir.

La vraie question est donc : comment exécuter du travail planifié de façon fiable sans déployer plus d'infrastructure ?

Une première tentative courante est simple : ajouter une entrée cron qui frappe un endpoint, et faire faire le travail à cet endpoint. Ça marche… jusqu'à ce que ça ne marche plus. Dès que vous avez plus d'un serveur, un déploiement au mauvais moment, ou une tâche qui prend plus de temps que prévu, vous commencez à voir des échecs confus.

Le travail planifié casse généralement de quelques façons prévisibles :

  • Exécutions doubles : deux serveurs exécutent la même tâche, donc des factures sont générées deux fois ou des emails sont envoyés en double.
  • Exécutions perdues : un appel cron échoue pendant un déploiement et personne ne s'en aperçoit avant qu'un utilisateur se plaigne.
  • Échecs silencieux : la tâche plante une fois, puis ne redémarre jamais parce qu'il n'y a pas de stratégie de retry.
  • Travail partiel : la tâche plante à mi-chemin et laisse les données dans un état étrange.
  • Pas de trace d'audit : on ne peut pas répondre à « quand ça a été exécuté pour la dernière fois ? » ou « que s'est-il passé la nuit dernière ? »

Le pattern cron + base de données est un compromis. Vous utilisez toujours cron pour « réveiller » le système selon un planning, mais vous stockez l'intention et l'état des tâches dans la base de données afin que le système puisse coordonner, réessayer et enregistrer ce qui s'est passé.

C'est adapté quand vous avez déjà une seule base de données (souvent PostgreSQL), un petit nombre de types de tâches, et que vous voulez un comportement prévisible avec un minimum d'opérations. C'est aussi un choix naturel pour des apps construites rapidement sur des stacks modernes (par exemple, React + Go + PostgreSQL).

Ce n'est pas adapté si vous avez besoin d'un très haut débit, de tâches longues nécessitant un streaming de progression, d'un ordre strict entre de nombreux types de tâches, ou d'un fort fan-out (des milliers de sous-tâches par minute). Dans ces cas, une vraie queue et des workers dédiés finissent généralement par payer.

L'idée centrale en termes simples

Le pattern cron + base de données exécute du travail en arrière-plan selon un planning sans déployer un système de queue complet. Vous utilisez encore cron (ou un scheduler), mais cron ne décide pas quoi exécuter. Il se contente de réveiller un worker fréquemment (une fois par minute est courant). La base de données décide du travail dû et s'assure qu'un seul worker prend chaque job.

Pensez-y comme une checklist partagée sur un tableau blanc. Cron est la personne qui entre dans la pièce chaque minute et demande « Qui doit faire quelque chose maintenant ? ». La base de données est le tableau blanc qui montre ce qui est dû, ce qui est pris et ce qui est fait.

Les éléments sont simples :

  • Un déclencheur de scheduler unique tourne fréquemment.
  • Une table jobs contient le « quoi » et le « quand » (run_at), plus le statut et le nombre de tentatives.
  • Un ou plusieurs workers sondent la table, réclament un job et effectuent le travail.
  • La réclamation utilise un verrouillage en base pour que deux workers ne prennent pas la même ligne.
  • La base de données reste la source de vérité pour ce qui a tourné, ce qui a échoué et ce qui doit être réessayé.

Exemple : vous voulez envoyer des relances de factures chaque matin, rafraîchir un cache toutes les 10 minutes, et nettoyer d'anciennes sessions chaque nuit. Au lieu de trois commandes cron séparées (chacune ayant ses propres modes d'échec et chevauchement), vous stockez les entrées de job en un seul endroit. Cron démarre le même process worker. Le worker demande à Postgres « Qu'est-ce qui est dû maintenant ? » et Postgres répond en laissant le worker réclamer en toute sécurité exactement un job à la fois.

Cela évolue progressivement. Vous pouvez commencer avec un worker sur un seul serveur. Plus tard, exécuter cinq workers sur plusieurs serveurs. Le contrat reste le même : la table est le contrat.

Le changement de mentalité est simple : cron n'est que l'appel de réveil. La base de données est le policier de la circulation qui décide de ce qui peut s'exécuter, enregistre ce qui s'est passé et vous donne un historique clair quand quelque chose tourne mal.

Concevoir la table de jobs (un schéma pratique)

Ce pattern fonctionne mieux quand votre base de données devient la source de vérité pour ce qui doit s'exécuter, quand cela doit s'exécuter et ce qui s'est passé la dernière fois. Le schéma n'est pas sophistiqué, mais des petits détails (champs de verrou et bons index) font une grande différence à mesure que la charge augmente.

Une table ou deux ?

Deux approches courantes :

  • Une table combinée quand vous ne vous souciez que de l'état le plus récent de chaque job (simple, moins de jointures).
  • Deux tables quand vous voulez une séparation propre entre « ce qu'est ce job » et « chaque fois qu'il a été exécuté » (meilleur historique, débogage plus simple).

Si vous prévoyez de déboguer souvent des échecs, gardez l'historique. Si vous voulez la configuration la plus petite possible, commencez par une table et ajoutez l'historique plus tard.

Un schéma pratique (version à deux tables)

Voici une disposition adaptée à PostgreSQL. Si vous construisez en Go avec PostgreSQL, ces colonnes se mappent proprement à des structs.

-- 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()
);

Quelques détails qui évitent des douleurs plus tard :

  • Gardez job_type comme une courte chaîne sur laquelle vous pouvez router (comme send_invoice_emails).
  • Stockez payload en jsonb pour pouvoir l'évoluer sans migrations.
  • run_at est votre « prochain horaire ». Cron (ou un script de scheduler) le définit, les workers le consomment.
  • locked_by et locked_until permettent aux workers de réclamer des jobs sans se marcher sur les pieds.
  • last_error doit être court et lisible par un humain. Mettez les traces de pile ailleurs si nécessaire.

Indexes dont vous aurez besoin

Sans index, les workers finissent par balayer trop de lignes. Commencez par :

  • Un index pour trouver rapidement le travail dû : (status, run_at)
  • Un index pour aider à détecter les verrous expirés : (locked_until)
  • Optionnel : un index partiel pour le travail actif seulement (par exemple, status dans queued et failed)

Cela garde la requête « trouver le prochain job exécutable » rapide même quand la table grossit.

Verrouiller et réclamer les jobs en toute sécurité

Le but est simple : de nombreux workers peuvent tourner, mais un seul doit prendre un job spécifique. Si deux workers traitent la même ligne, vous obtenez des emails en double, des facturations en double, ou des données incohérentes.

Une approche sûre est de traiter la réclamation d'un job comme un « bail » (lease). Le worker marque le job comme verrouillé pour une courte fenêtre. Si le worker plante, le bail expire et un autre worker peut le reprendre. C'est l'objet de locked_until.

Utilisez un bail pour que les crashs ne bloquent pas le travail indéfiniment

Sans bail, un worker peut verrouiller un job et ne jamais le déverrouiller (process tué, reboot du serveur, déploiement raté). Avec locked_until, le job redevient disponible quand le temps est écoulé.

Une règle typique : un job peut être réclamé quand locked_until est NULL ou locked_until <= now().

Réclamer les jobs avec une mise à jour atomique

Le détail clé est de réclamer le job en une seule instruction (ou une seule transaction). Vous voulez que la base de données soit l'arbitre.

Voici un pattern courant pour PostgreSQL : choisir un job dû, le verrouiller et le retourner au worker. (Cet exemple utilise une seule table jobs; la même idée s'applique si vous réclamez depuis 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.*;

Pourquoi ça marche :

  • FOR UPDATE SKIP LOCKED permet à plusieurs workers de se concurrencer sans se bloquer mutuellement.
  • Le bail est défini au moment de la réclamation, donc les autres workers l'ignorent jusqu'à son expiration.
  • RETURNING remet la ligne au worker qui a gagné la course.

Quelle durée pour le bail, et comment le renouveler ?

Fixez le bail plus long qu'une exécution normale, mais suffisamment court pour qu'un crash récupère vite. Si la plupart des jobs finissent en 10 secondes, un bail de 2 minutes suffit.

Pour les tâches longues, renouvelez le bail pendant l'exécution (heartbeat). Une approche simple : toutes les 30 secondes, étendez locked_until si vous possédez toujours le job.

  • Durée du bail : 5x à 20x votre temps typique de job
  • Intervalle de heartbeat : 1/4 à 1/2 du bail
  • La mise à jour de renouvellement doit inclure WHERE id = $job_id AND locked_by = $worker_id

Cette dernière condition est importante. Elle empêche un worker d'étendre le bail d'un job qu'il ne possède plus.

Re-essais et backoff prévisibles

Ship scheduled jobs with less ops
Build scheduled work without adding a queue service, using a simple Go and Postgres setup.

Les retries sont l'endroit où ce pattern paraît calme ou devient un bazar bruyant. Le but est simple : quand un job échoue, réessayez plus tard de manière explicable, mesurable et arrêtée.

Commencez par rendre l'état du job explicite et fini : queued, running, succeeded, failed, dead. En pratique, la plupart des équipes utilisent failed pour « échoué mais sera réessayé » et dead pour « échoué et on a abandonné ». Cette distinction évite les boucles infinies.

Le comptage des tentatives est le deuxième garde-fou. Stockez attempts (nombre de tentatives) et max_attempts (nombre maximum autorisé). Quand un worker attrape une erreur, il doit :

  • incrémenter attempts
  • mettre l'état à failed si attempts < max_attempts, sinon à dead
  • calculer run_at pour la prochaine tentative (uniquement pour failed)

Le backoff n'est que la règle qui décide du prochain run_at. Choisissez-en une, documentez-la et gardez-la cohérente :

  • Délai fixe : attendre toujours 1 minute
  • Exponentiel : 1m, 2m, 4m, 8m
  • Exponentiel plafonné : exponentiel mais jamais plus de, par ex., 30m
  • Ajoutez du jitter : rendez aléatoire un peu pour éviter que les jobs ne se relancent tous à la même seconde

Le jitter compte quand une dépendance tombe et revient. Sans lui, des centaines de jobs peuvent retenter en même temps et échouer à nouveau.

Stockez assez d'informations d'erreur pour rendre les échecs visibles et débogables. Vous n'avez pas besoin d'un système de logging complet, mais les bases suivantes sont nécessaires :

  • last_error (message court, sûr à afficher dans un écran admin)
  • error_code ou error_type (aide au regroupement)
  • failed_at et next_run_at
  • optionnel : last_stack (si vous contrôlez la taille)

Une règle concrète qui fonctionne bien : marquer dead après 10 tentatives, et utiliser un backoff exponentiel avec jitter. Cela permet aux échecs transitoires de se réessayer, mais empêche les jobs cassés de consommer indéfiniment du CPU.

Idempotence : empêcher les doublons même si un job est réexécuté

Idempotence signifie que votre job peut s'exécuter deux fois et produire le même résultat final. Dans ce pattern, c'est important car la même ligne peut être reprise après un crash, un timeout ou un retry. Si votre job est « envoyer un email de facture », l'exécuter deux fois n'est pas sans conséquence.

Une façon pratique de raisonner : scindez chaque job en (1) faire le travail et (2) appliquer un effet. Vous voulez que l'effet arrive une fois, même si le travail est tenté plusieurs fois.

Utilisez une clé d'idempotence liée à l'événement métier

Une clé d'idempotence doit venir de ce que représente le job, pas de la tentative du worker. De bonnes clés sont stables et faciles à expliquer, comme invoice_id, user_id + day, ou report_name + report_date. Si deux tentatives se réfèrent au même événement réel, elles doivent partager la même clé.

Exemple : « Générer le rapport des ventes du 2026-01-14 » peut utiliser sales_report:2026-01-14. « Prélèver la facture 812 » peut utiliser invoice_charge:812.

Faire respecter le "une seule fois" avec des contraintes en base

La garde la plus simple est de laisser PostgreSQL rejeter les doublons. Stockez la clé d'idempotence quelque part indexable, puis ajoutez une contrainte unique.

-- 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;

Cela empêche deux lignes avec la même clé d'exister en même temps. Si votre design permet plusieurs lignes (pour l'historique), mettez l'unicité sur une table d'« effets » à la place, comme sent_emails(idempotency_key) ou payments(idempotency_key).

Effets secondaires courants à protéger :

  • Emails : créez une ligne sent_emails avec une clé unique avant d'envoyer, ou enregistrez un id de message du fournisseur une fois envoyé.
  • Webhooks : stockez delivered_webhooks(event_id) et sautez si ça existe.
  • Paiements : utilisez toujours la fonction d'idempotence du fournisseur de paiement plus votre propre clé unique en base.
  • Écritures de fichiers : écrivez dans un nom temporaire, puis renommez, ou stockez un enregistrement file_generated lié à (type, date).

Si vous construisez sur une stack avec Postgres (par exemple, backend Go + PostgreSQL), ces vérifications d'unicité sont rapides et faciles à garder proches des données. L'idée centrale est simple : les retries sont normaux, les doublons sont optionnels.

Étape par étape : construire un worker et un scheduler minimal

Add a jobs admin screen
Generate a small internal page to inspect failed and dead jobs safely.

Choisissez un runtime basique et tenez-vous-y. Le but du pattern cron + base de données est de réduire les pièces mobiles, donc un petit process en Go, Node ou Python qui parle à PostgreSQL suffit généralement.

Construisez-le en cinq petites étapes

  1. Créez les tables et les index. Ajoutez une table jobs (et toutes tables de lookup désirées), indexez run_at, et ajoutez un index qui aide votre worker à trouver rapidement les jobs disponibles (par exemple sur (status, run_at)).

  2. Écrivez une fonction d'enqueue minimale. Votre application doit insérer une ligne avec run_at à « maintenant » ou à un futur. Gardez le payload petit et prévisible (IDs et type de job, pas de gros blobs).

INSERT INTO jobs (type, payload, status, run_at, attempts, max_attempts)
VALUES ($1, $2::jsonb, 'queued', $3, 0, 10);
  1. Implémentez la boucle de claim. Exécutez-la dans une transaction. Sélectionnez quelques jobs dus, verrouillez-les pour que d'autres workers les sautent, et marquez-les running dans la même transaction.
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. Traitez et finalisez. Pour chaque job réclamé, effectuez le travail, puis mettez à jour en done avec finished_at. En cas d'échec, enregistrez un message d'erreur et remettez en queued avec un nouveau run_at (backoff). Gardez les mises à jour de finalisation petites et exécutez-les toujours, même si votre process s'arrête.

  2. Ajoutez des règles de retry explicables. Utilisez une formule simple comme run_at = now() + (attempts^2) * interval '10 seconds', et arrêtez après max_attempts en réglant status = 'dead'.

Ajoutez une visibilité de base

Vous n'avez pas besoin d'un tableau de bord complet dès le premier jour, mais vous devez pouvoir repérer les problèmes.

  • Loggez une ligne par job : réclamé, réussi, échoué, réessayé, dead.
  • Créez une simple requête ou vue admin pour « jobs dead » et « anciens jobs en running ».
  • Mettez une alerte sur les compteurs (par ex., plus de N jobs dead dans la dernière heure).

Si vous êtes déjà sur une stack Go + PostgreSQL, cela se mappe proprement à un seul binaire worker plus cron.

Un exemple réaliste à copier

Imaginez une petite SaaS avec deux tâches planifiées :

  • Un nettoyage nocturne qui supprime les sessions expirées et les anciens fichiers temporaires.
  • Un email hebdomadaire « votre activité » envoyé à chaque utilisateur le lundi matin.

Restez simple : une table PostgreSQL pour contenir les jobs, et un worker qui tourne chaque minute (déclenché par cron). Le worker réclame les jobs dus, les exécute et enregistre succès ou échec.

Ce qui est enqueued, et quand

Vous pouvez enqueuer des jobs depuis plusieurs endroits :

  • Tous les jours à 02:00 : enqueuer un job cleanup_nightly pour « aujourd'hui ».
  • À l'inscription : enqueuer un job send_weekly_report pour le prochain lundi de l'utilisateur.
  • Après un événement (comme « l'utilisateur a cliqué Export report ») : enqueuer un job send_weekly_report qui s'exécute immédiatement pour une plage de dates spécifique.

Le payload est juste le minimum qu'il faut au worker. Gardez-le petit pour faciliter le retry.

{
  "type": "send_weekly_report",
  "payload": {
    "user_id": 12345,
    "date_range": {
      "from": "2026-01-01",
      "to": "2026-01-07"
    }
  }
}

Comment l'idempotence empêche les envois en double

Un worker peut planter au pire moment : juste après avoir envoyé l'email, mais avant d'avoir marqué le job comme « done ». Au redémarrage, il peut reprendre le même job.

Pour éviter les envois en double, donnez au travail une clé de déduplication naturelle et stockez-la où la base peut l'appliquer. Pour les rapports hebdomadaires, une bonne clé est (user_id, week_start_date). Avant d'envoyer, le worker enregistre « je m'apprête à envoyer le rapport X ». Si cet enregistrement existe déjà, il saute l'envoi.

Cela peut être aussi simple qu'une table sent_reports avec une contrainte unique sur (user_id, week_start_date), ou une idempotency_key unique sur le job lui-même.

À quoi ressemble un échec (et comment il se rétablit)

Imaginons que le fournisseur d'email timeoute. Le job échoue, donc le worker :

  • incrémente attempts
  • enregistre le message d'erreur pour le debug
  • programme la prochaine tentative avec backoff (par ex. : +1 min, +5 min, +30 min, +2 h)

S'il continue d'échouer au-delà de la limite (par exemple 10 tentatives), le job est marqué « dead » et on arrête les retries. Le job réussit une fois, ou il réessaie selon un calendrier clair, et l'idempotence rend les retries sûrs.

Erreurs communes et pièges

Own your scheduler code
Export the source code anytime and keep your background job system portable.

Le pattern cron + base de données est simple, mais de petites erreurs peuvent le transformer en source de doublons, de jobs bloqués ou de charge surprise. La plupart des problèmes apparaissent après le premier crash, déploiement ou pic de trafic.

Erreurs provoquant doublons ou jobs bloqués

La plupart des incidents réels viennent de quelques pièges :

  • Lancer le même job depuis plusieurs entrées cron sans bail. Si deux serveurs cochent la minute, tous deux peuvent réclamer le même travail à moins que votre étape de claim soit atomique et définisse un verrou (ou un bail) dans la même transaction.
  • Ne pas utiliser locked_until. Si un worker plante après avoir réclamé un job, cette ligne peut rester « en cours » pour toujours. Un timestamp de bail permet à un autre worker de la reprendre plus tard.
  • Retenter instantanément après un échec. Quand une API est tombée, des retries instantanés provoquent des pics, épuisent les quotas et échouent en boucle. Programmez toujours la prochaine tentative dans le futur.
  • Prendre « au moins une fois » pour « exactement une fois ». Un job peut s'exécuter deux fois (timeouts, redémarrages de worker, problèmes réseau). Si l'exécution en double est dangereuse, rendez les effets latéraux sûrs à répéter.
  • Stocker de gros payloads dans la ligne du job. Les gros blobs JSON gonflent la table, ralentissent les index et alourdissent les verrous. Stockez une référence (par ex. user_id, invoice_id ou une clé de fichier) et récupérez le reste à l'exécution.

Exemple : vous envoyez un email d'invoice hebdomadaire. Si le worker timeout juste après l'envoi mais avant de marquer le job comme terminé, il peut renvoyer l'email au retry. C'est normal pour ce pattern à moins d'ajouter une garde (par ex. enregistrer un événement unique « email envoyé » indexé par invoice id).

Pièges moins évidents

Évitez de mélanger scheduling et exécution dans une même transaction longue. Si vous gardez une transaction ouverte pendant des appels réseau, vous conservez les verrous plus longtemps que nécessaire et bloquez les autres workers.

Surveillez les différences d'horloge entre les machines. Utilisez l'heure de la base (NOW() dans PostgreSQL) comme source de vérité pour run_at et locked_until, pas l'horloge du serveur applicatif.

Fixez une durée maximale d'exécution claire. Si un job peut prendre 30 minutes, rendez le bail plus long que ça et renouvelez-le si nécessaire. Sinon, un autre worker peut le reprendre en plein milieu.

Gardez votre table de jobs saine. Si les jobs complétés s'accumulent pour toujours, les requêtes ralentissent et la contention sur les verrous augmente. Choisissez une règle de rétention simple (archiver ou supprimer les anciennes lignes) avant que la table ne devienne énorme.

Liste de vérification rapide et prochaines étapes

Liste de vérification rapide

Avant de déployer ce pattern, vérifiez l'essentiel. Une petite omission ici devient souvent des jobs bloqués, des doublons surprises ou un worker qui « tape » la base.

  • Votre table jobs contient l'essentiel : run_at, status, attempts, locked_until et max_attempts (plus last_error ou similaire pour voir ce qui s'est passé).
  • Chaque job peut s'exécuter deux fois sans dommage. Si vous n'êtes pas sûr, ajoutez une clé d'idempotence ou une règle d'unicité autour de l'effet (par ex. une seule facture par invoice_id).
  • Il y a un endroit clair pour observer les échecs et décider quoi faire : voir les jobs failed, relancer un job, ou le marquer dead quand il faut arrêter les retries.
  • Votre timeout de bail est raisonnable pour le travail. Il doit être assez long pour les exécutions normales, mais assez court pour que les workers crashés ne bloquent pas des heures.
  • Le backoff des retries est prévisible. Il doit ralentir les échecs répétés et s'arrêter après max_attempts.

Si tout cela est vrai, le pattern cron + base de données est en général suffisamment stable pour des charges réelles.

Prochaines étapes

Une fois la checklist validée, concentrez-vous sur l'exploitation au quotidien.

  • Ajoutez deux petites actions admin : « retry now » (met run_at = now() et efface le lock) et « cancel » (met en statut terminal). Cela fait gagner du temps lors des incidents.
  • Faites logger le worker une ligne par job : type, id de job, numéro de tentative et résultat. Ajoutez une alerte sur la hausse des échecs.
  • Testez la charge avec un pic réaliste : beaucoup de jobs planifiés à la même minute. Si la sélection des jobs devient lente, ajoutez le bon index (souvent sur status, run_at).

Si vous voulez construire rapidement ce type de setup, Koder.ai peut vous aider à passer du schéma à une app Go + PostgreSQL déployée avec moins de câblage manuel, pendant que vous vous concentrez sur le verrouillage, les retries et les règles d'idempotence.

Si vous dépassez plus tard ce dispositif, vous aurez néanmoins bien compris le cycle de vie des jobs, et ces mêmes idées se transposent facilement vers un système de queue complet.

FAQ

Quand utiliser cron avec une base de données pour les tâches en arrière-plan ?

Utilisez ce modèle si vous avez déjà une base de données, un nombre limité de types de tâches et souhaitez planifier des travaux sans gérer une file d’attente distincte. Il convient aux rappels par e-mail, au nettoyage, aux rapports et au rafraîchissement du cache.

Que fait cron dans cette configuration ?

Cron doit seulement lancer le worker à intervalle régulier, souvent une fois par minute. La base de données détermine quelles tâches doivent être exécutées, enregistre leur état et permet à un seul worker de réserver chaque tâche.

Quels champs une table de tâches doit-elle inclure ?

Stockez le type de tâche, une petite charge utile JSON, l’heure prévue, l’état, le nombre de tentatives, la limite de reprises, le propriétaire du verrou, l’expiration du verrou et un court message d’erreur. Ces champs donnent aux workers assez d’informations pour exécuter les tâches et les reprendre en toute sécurité.

Comment plusieurs workers évitent-ils d’exécuter la même tâche ?

Réservez les tâches dans une seule transaction de base de données avec un verrouillage de lignes, par exemple PostgreSQL FOR UPDATE SKIP LOCKED. Mettez à jour la ligne à l’état en cours d’exécution et définissez son bail avant que le worker ne commence la tâche réelle.

Pourquoi les tâches en arrière-plan ont-elles besoin d’un délai d’expiration du bail ?

Un bail est un verrou temporaire enregistré avec locked_until. Si un worker plante, le bail expire et un autre worker peut prendre la tâche au lieu de la laisser bloquée indéfiniment.

Comment les tâches échouées doivent-elles réessayer ?

Réessayez plus tard avec une règle de temporisation claire, par exemple 1 minute, 2 minutes, 4 minutes, puis un délai plafonné avec un peu d’aléa. Après le nombre de tentatives autorisé, marquez la tâche comme morte afin qu’elle ne consomme plus de temps de worker.

Qu’est-ce que l’idempotence et pourquoi est-elle importante ?

Rendez l’effet secondaire sûr à répéter. Par exemple, utilisez une clé d’idempotence stable, comme un ID de facture ou un ID utilisateur associé à la date du rapport, puis imposez l’unicité dans PostgreSQL ou chez le fournisseur externe.

Ce modèle garantit-il un traitement exactement une fois ?

Non. Un worker peut planter après l’envoi d’un e-mail ou l’appel d’un fournisseur de paiement, mais avant d’enregistrer le succès. Concevez le système pour un traitement au moins une fois, puis utilisez l’idempotence pour éviter les effets en double.

Un worker doit-il garder une transaction de base de données ouverte pendant l’exécution de la tâche ?

Gardez les transactions de base de données courtes. Réservez la tâche, validez la transaction, effectuez les appels réseau hors transaction, puis enregistrez le succès ou l’échec dans une mise à jour distincte. Les longues transactions conservent les verrous et ralentissent les autres workers.

Comment surveiller cron et les tâches de base de données ?

Suivez les tâches mortes et les anciennes tâches en cours d’exécution, consignez chaque réservation et chaque résultat, et déclenchez une alerte lorsque les échecs augmentent. Ajoutez de simples actions d’administration pour réessayer une tâche immédiatement, l’annuler ou examiner sa dernière erreur.

Related posts