Aller au contenu

Tâches Cron

Rebase inclut un planificateur de tâches cron intégré pour exécuter des tâches d’arrière-plan récurrentes : nettoyage de données, génération de rapports, vérifications de santé (health checks), synchronisations avec des API externes, et bien plus encore.

Les tâches cron suivent le même modèle de découverte basée sur les fichiers que les fonctions personnalisées : déposez un fichier TypeScript dans votre répertoire crons/, et Rebase l’enregistre et le planifie automatiquement.

  • Zéro dépendance — Aucune bibliothèque externe de planification requise
  • API d’administration — Points de terminaison REST pour lister, déclencher, activer/désactiver et consulter les journaux
  • Tableau de bord Studio — Surveillez toutes les tâches, consultez l’historique d’exécution et déclenchez des exécutions manuellement
  • Persistance en base de données — Journaux d’exécution stockés dans PostgreSQL, survivant aux redémarrages
  • Cache en mémoire — Mémoire tampon circulaire rapide (50 dernières exécutions) pour le tableau de bord, adossée à la base de données

Créez un fichier dans votre répertoire backend/crons/ qui exporte par défaut une définition cron. Utilisez le helper defineCron d’@rebasepro/server pour l’inférence de types et l’autocomplétion :

// backend/crons/health-check.ts
import { defineCron } from "@rebasepro/server";
export default defineCron({
schedule: "*/5 * * * *", // every 5 minutes
name: "System Health Check",
description: "Monitors uptime and memory usage",
async handler(ctx) {
ctx.log("Running health check...");
const uptime = process.uptime();
const mem = process.memoryUsage();
ctx.log(`Uptime: ${Math.round(uptime)}s`);
ctx.log(`Heap: ${Math.round(mem.heapUsed / 1024 / 1024)}MB`);
return {
uptimeSeconds: Math.round(uptime),
heapUsedMB: Math.round(mem.heapUsed / 1024 / 1024),
};
},
});

rebase dev surveille le répertoire des crons, ainsi une tâche ajoutée pendant son exécution est enregistrée au rechargement suivant — sans redémarrage. (Il doit être notifié : le répertoire est scanné plutôt qu’importé, le watcher ne peut donc pas l’inférer.)

Le nom de fichier (sans extension) devient l’identifiant unique de la tâche — par exemple, health-check.

Activez les tâches cron en ajoutant cronsDir à la configuration de votre backend :

const instance = await initializeRebaseBackend({
// ... other config
functionsDir: path.resolve(__dirname, "../functions"),
cronsDir: path.resolve(__dirname, "../crons"), // ← add this
});

C’est tout. Rebase va :

  1. Scanner le répertoire à la recherche de fichiers .ts / .js
  2. Enregistrer chaque export par défaut en tant que tâche cron
  3. Créer automatiquement les tables rebase.cron_logs, rebase.cron_claims et rebase.cron_job_state dans PostgreSQL (si le pilote prend en charge le SQL)
  4. Démarrer le planificateur et initialiser les compteurs à partir des journaux existants en base de données
  5. Monter les routes REST d’administration sur /api/admin/cron

Depuis 0.25 En 0.24, les tâches cron ne proviennent que d’un répertoire.

crons prend les tâches sous la forme d’un enregistrement (record) qui associe l’identifiant de chaque tâche à sa définition, à côté de collections. Un répertoire n’est qu’une façon de le construire : loadCrons(dir) en lit un et produit exactement cette forme.

import path from "path";
import { defineCron, initializeRebaseBackend, loadCrons } from "@rebasepro/server";
const nightlyReport = defineCron({
schedule: "0 3 * * *",
async handler({ log }) {
log("building the report");
}
});
await initializeRebaseBackend({ app, server, crons: { "nightly-report": nightlyReport } });
// the same as cronsDir:
await initializeRebaseBackend({ app, server, crons: await loadCrons(path.resolve(__dirname, "../crons")) });

Donnez crons ou cronsDir, pas les deux : le démarrage refuse la paire. Construire le backend enregistre les tâches ; ce qui lance leurs timers, c’est start(), que initializeRebaseBackend appelle pour vous. Avec createRebaseBackend, vous l’appelez vous-même, dans le processus qui doit les exécuter. loadCronJobsFromDirectory et loadCronJobsWithDiagnostics fonctionnent toujours, sont dépréciées et avertissent une seule fois.

Les expressions cron utilisent le format standard à 5 champs :

┌───────────── minute (0–59)
│ ┌─────────── hour (0–23)
│ │ ┌───────── day of month (1–31)
│ │ │ ┌─────── month (1–12)
│ │ │ │ ┌───── day of week (0–6, Sunday = 0)
│ │ │ │ │
* * * * *
Expression Signification
* * * * * Chaque minute
0 * * * * Chaque heure
0 3 * * * Tous les jours à 3h00
0 0 * * 1 Chaque lundi à minuit
0 9 1 * * Le premier jour de chaque mois à 9h00
0,30 * * * * Toutes les 30 minutes (à :00 et :30)
0 9-17 * * 1-5 Toutes les heures, de 9h à 17h, du lundi au vendredi uniquement

Les pas (*/n), les plages (a-b) et les listes (a,b,c) sont tous pris en charge.

Une planification est comparée à l’heure locale de son fuseau. Lors d’un changement d’heure, cela signifie qu’une tâche à heure fixe dans l’heure répétée (30 2 * * * là où les horloges reculent à 03:00) s’exécute lors des deux passages de cette heure, et qu’une tâche dans l’heure sautée, quand les horloges avancent, ne s’exécute pas ce jour-là.

timezone est nouveau — sur la version 0.17.3, une planification est toujours interprétée dans le fuseau horaire propre de l’hôte. Tout le reste sur cette interface est déjà déployé.

interface CronJobDefinition {
// Cron schedule expression (5-field format)
schedule: string;
// IANA zone the schedule is read in, e.g. "Europe/Madrid". Without it the
// schedule is read in the host's own zone — UTC in nearly every container,
// yours on a laptop — so name it. An unknown zone is refused when the job
// loads rather than read as local time.
timezone?: string;
// Human-readable name shown in Studio
name: string;
// Optional description shown in Studio
description?: string;
// Whether the job runs (default: true). A pause or a resume from Studio
// or the admin API overrides it, for every process, until reset.
enabled?: boolean;
// Max execution time in seconds (default: 300). Infinity means no
// timeout; 0, a negative number or NaN is refused when the job loads.
timeoutSeconds?: number;
// How far back to look on startup for a slot that elapsed while no
// instance was ticking (default: off). See "Cron across instances".
catchUpWindowSeconds?: number;
// The function to run on each tick
handler: (ctx: CronJobContext) => Promise<unknown> | unknown;
}

Chaque gestionnaire reçoit un CronJobContext contenant des méthodes utilitaires et l’instance du client Rebase :

interface CronJobContext {
// The job's unique ID (derived from filename)
jobId: string;
// The scheduled tick timestamp
scheduledAt: Date;
// Logger — lines appear in Studio, the logs API and the server log
log: (...args: unknown[]) => void;
// Aborted when the run exceeds `timeoutSeconds`, or when a shutdown's
// wait for it runs out
signal: AbortSignal;
// The server-side Rebase singleton — the same object `import { rebase }
// from "@rebasepro/server"` returns, and the same one `defineFunction`
// hands its callback.
rebase: RebaseServerClient;
}

Utilisez ctx.log() pour émettre une sortie structurée. Ces lignes sont capturées dans le journal d’exécution et visibles dans Studio ainsi que via l’API REST. Chaque ligne est aussi écrite dans le journal du serveur au niveau info, avec un champ cron contenant l’ID du job : elle apparaît donc partout où la sortie de votre conteneur est collectée, y compris dans les journaux de débogage de Rebase Cloud. Cette copie est masquée comme les lignes de journal du serveur lui-même : les valeurs de clés comme password ou apiKey, ainsi que le SQL et les paramètres d’une requête en échec, sont remplacés. Le journal d’exécution conserve la ligne sans ce masquage.

Depuis 0.25 Le journal d’exécution conserve les 500 premières lignes d’une exécution ou 64 Ko d’entre elles, selon ce qui survient en premier, puis une ligne indiquant combien d’autres il y en avait : … 9500 more lines not kept — see the server log. Le journal du serveur reçoit toutes les lignes. Une tâche qui journalise une ligne par élément traité doit s’attendre à ce que son historique montre le début de l’exécution.

ctx.signal — interrompre le travail lorsque l’exécution s’arrête

Section intitulée « ctx.signal — interrompre le travail lorsque l’exécution s’arrête »

Le timeout met fin à l’exécution : le planificateur cesse d’attendre et enregistre un échec. Il n’arrête pas le gestionnaire. Passez ctx.signal à tout ce qui en accepte un, et le travail s’arrêtera avec lui :

export default defineCron({
name: "Sync inventory",
schedule: "*/15 * * * *",
timeoutSeconds: 60,
async handler({ signal, log }) {
const res = await fetch("https://supplier.example.com/stock", { signal });
log(`fetched ${res.status}`);
}
});

Sans cela, une tâche dont le timeout correspond à son intervalle laisse fuiter une requête abandonnée à chaque déclenchement — invisible, car chaque exécution est déjà enregistrée comme échouée.

Interagir avec la base de données et les services via ctx.rebase

Section intitulée « Interagir avec la base de données et les services via ctx.rebase »

ctx.rebase.dataAsAdmin est le plan de données au périmètre administrateur. Un cron ne disposant d’aucun utilisateur par requête, il n’existe pas d’alternative au périmètre utilisateur ici — appliquez vous-même les filtres de périmètre sur chaque requête.

// backend/crons/expire-users.ts
import { defineCron } from "@rebasepro/server";
export default defineCron({
schedule: "0 0 * * *", // Daily at midnight
name: "Expire Inactive Accounts",
async handler(ctx) {
ctx.log("Checking for expired trial users...");
// Fetch using the pre-initialized data driver. `collection<Row>(slug)`
// gives the query builder the row type — `where` keys are checked
// against it. Every filter is an `[operator, value]` tuple; a bare
// value is passed straight through and builds a malformed query.
const users = ctx.rebase.dataAsAdmin.collection<{
id: string;
email: string;
trial_status: string;
trial_ends_at: string;
status: string;
}>("users");
const { data: trials } = await users.find({
where: {
trial_status: ["==", "active"],
trial_ends_at: ["<", new Date().toISOString()]
}
});
ctx.log(`Found ${trials.length} users with expired trials.`);
for (const user of trials) {
await users.update(user.id, {
trial_status: "expired",
status: "disabled"
});
// Send email notification using the Rebase email service
await ctx.rebase.email.send({
to: user.email,
subject: "Your trial has expired",
html: "<p>Please upgrade your subscription to continue.</p>"
});
}
}
});

Lister, déclencher, suspendre et reprendre les tâches par HTTP, et client.cron du SDK typé : API des tâches cron.

Lorsque des tâches cron sont configurées, un outil Cron Jobs apparaît dans Rebase Studio sous la section Compute, à côté de la console JS. Le tableau de bord propose :

  • Liste des tâches — Toutes les tâches enregistrées avec des indicateurs d’état en direct
  • Panneau de détails — Planification, prochaine/dernière exécution, durée et informations sur les erreurs
  • Historique d’exécution — Entrées de journal extensibles avec les sorties capturées et les résultats
  • Déclenchement manuel — Exécutez n’importe quelle tâche à la demande en un clic. Le panneau attend la fin de l’exécution et indique comment elle s’est déroulée : une exécution qui a échoué affiche son erreur
  • Activer/Désactiver — Mettez en pause et reprenez les tâches sans redémarrer le serveur, pour tous les processus à la fois ; une pause est conservée après les redémarrages et les déploiements
  • Tâches introuvables — chaque tâche que le planificateur a refusée, avec sa planification et la raison, et le nombre de fichiers cron qui n’ont pas pu être chargés. Aucune d’elles n’est planifiée ; voir Les tâches introuvables

Le tableau de bord s’actualise automatiquement toutes les 15 secondes.

Le panneau affiche la même chose quel que soit le processus qui le sert. Une tâche qu’un autre processus exécute apparaît comme en cours. Sur chaque processus qui dispose d’un magasin, le nombre d’exécutions, le nombre d’échecs et la dernière exécution sont lus dans la base de données, où sont comptées les exécutions de tous les processus. Les compteurs propres d’un processus ne contiennent que les exécutions qu’il a faites : le rôle api à côté d’un worker n’exécute rien, et de deux répliques qui planifient toutes deux, aucune ne voit les créneaux que l’autre a réservés. Ce n’est que lorsque la base de données ne peut pas être lue que la carte se rabat sur les compteurs propres de ce processus, avec un avertissement dans le journal du serveur.

Depuis 0.25 Les compteurs couvrent toute la durée de vie ; l’historique couvre les 30 derniers jours. La ligne de chaque exécution dans rebase.cron_logs est supprimée 30 jours après son début, par un balayage horaire, tandis que les compteurs d’exécutions et d’échecs continuent de la compter. Une exécution ignorée compte comme une exécution, comme toujours : c’est une ligne de l’historique.

Lors de l’initialisation du backend, Rebase analyse toutes les planifications cron enregistrées à l’aide d’un expandeur cron en JS sans dépendance externe :

  • Vérification de syntaxe : Vérifie que la chaîne contient exactement 5 champs séparés par des espaces (minute, hour, day of month, month, day of week).
  • Développement des plages : Déconstruit les pas (*/15), les plages (9-17) et les listes séparées par des virgules (0,30) en tableaux explicites d’entiers valides associés à leurs limites respectives (ex. minutes 0-59, heures 0-23, mois 1-12).
  • Si une expression cron échoue à la validation, Rebase rejette la définition, consigne une erreur au démarrage et refuse d’enregistrer la tâche afin d’éviter les échecs d’exécution au runtime.

Sous le capot : Correction de la dérive de l’horloge (Clock-Drift)

Section intitulée « Sous le capot : Correction de la dérive de l’horloge (Clock-Drift) »

Les planificateurs standards basés sur des intervalles (tels que setInterval) dérivent au fil du temps et provoquent d’importants pics d’utilisation CPU en raison des délais d’ordonnancement de la boucle d’événements au niveau de l’OS. Pour garantir la précision d’exécution, Rebase implémente une boucle de calcul dynamique de l’heure cible :

  1. Calcul du candidat : Dès la fin d’une tâche ou au démarrage du planificateur, Rebase calcule le timestamp exact de la prochaine minute candidate correspondante.
  2. Sommeil dynamique : Il calcule la différence en millisecondes (nextRun.getTime() - now.getTime()) et planifie un unique setTimeout.
  3. Seuil de sécurité anti-dérive : Un tampon de sommeil minimal (MIN_SCHEDULE_INTERVAL_MS) de 5 000 ms est appliqué. Si un cycle du planificateur se termine extrêmement rapidement, ce seuil empêche un double déclenchement quasi instantané.
  4. Fermeture propre : Les identifiants de timers sont explicitement détachés de la boucle d’événements Node.js via timer.unref(), garantissant que les planificateurs cron en arrière-plan ne bloquent pas l’arrêt propre des processus lors des déploiements.

Chaque processus dont le planificateur est actif arme les mêmes minuteurs ; la base de données décide lequel d’entre eux exécute chaque créneau. Comment un créneau ne s’exécute qu’une fois, comment un créneau perdu lors d’un redémarrage est rattrapé, comment une pause atteint chaque réplique et pourquoi un déclenchement manuel ne s’exécute jamais à côté d’une exécution planifiée est décrit dans Cron entre plusieurs instances.


Délais d’expiration (Timeouts) et isolation des erreurs

Section intitulée « Délais d’expiration (Timeouts) et isolation des erreurs »
  • Course de timeout forcé : Les blocs d’exécution sont enveloppés dans un Promise.race face à un timer d’expiration dérivé de timeoutSeconds (par défaut : 300 secondes / 5 minutes ; Infinity pour aucun timeout). Si le gestionnaire reste bloqué au-delà de ce seuil, ctx.signal est annulé et la promesse est rejetée, levant l’erreur : Error: Cron job "<id>" timed out after <N>ms L’annulation est la partie qui interrompt le travail ; le rejet empêche seulement le planificateur de continuer à attendre. Un gestionnaire qui ignore ctx.signal continue de tourner au-delà de sa propre exécution.
  • Arrêt : backend.shutdown() attend une exécution en cours, dans le même budget que la file de jobs (les deux tiers du timeout d’arrêt). Si l’exécution tourne encore quand il est épuisé, ctx.signal est annulé et elle est enregistrée comme échouée, avec la raison. Son créneau reste réservé, donc aucune autre instance ne la réexécute.
  • Try/Catch sécurisé : Chaque gestionnaire de tâche s’exécute au sein d’une enveloppe isolée. Toute exception non interceptée est capturée, formatant la trace d’erreur en une chaîne, définissant le statut de la tâche sur "error" et mettant à jour le compteur d’échecs de la tâche. Un plantage au sein d’une tâche cron individuelle ne fera jamais planter la boucle du planificateur ni le serveur web HTTP principal sous Hono.
  • Mémoire tampon circulaire en mémoire : Le planificateur maintient un ring buffer contenant les 50 dernières exécutions par tâche. Cette mémoire tampon est conservée en mémoire vive pour permettre des lectures quasi instantanées depuis Rebase Studio.

Lorsque des adaptateurs de base de données prenant en charge le SQL (ex. PostgreSQL) sont actifs, Rebase configure la table rebase.cron_logs :

CREATE SCHEMA IF NOT EXISTS rebase;
CREATE TABLE IF NOT EXISTS rebase.cron_logs (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
job_id TEXT NOT NULL,
started_at TIMESTAMPTZ NOT NULL,
finished_at TIMESTAMPTZ NOT NULL,
duration_ms INTEGER NOT NULL,
success BOOLEAN NOT NULL DEFAULT true,
error TEXT, -- Stack trace or error message
result JSONB, -- Return value of handler
logs JSONB, -- Ring buffer array of ctx.log outputs
manual BOOLEAN NOT NULL DEFAULT false -- True if triggered from Studio/REST
);
ALTER TABLE rebase.cron_logs ADD COLUMN IF NOT EXISTS counted BOOLEAN; -- the run is in the counters below
CREATE INDEX IF NOT EXISTS idx_cron_logs_job ON rebase.cron_logs(job_id, started_at DESC);
CREATE INDEX IF NOT EXISTS idx_cron_logs_started ON rebase.cron_logs(started_at);
CREATE INDEX IF NOT EXISTS idx_cron_logs_uncounted ON rebase.cron_logs(job_id, started_at DESC) WHERE counted IS NULL;
CREATE TABLE IF NOT EXISTS rebase.cron_job_state (
job_id TEXT PRIMARY KEY,
enabled BOOLEAN, -- NULL: follow the job's own `enabled`
updated_at TIMESTAMPTZ, -- when `enabled` was last set, and by whom
updated_by TEXT,
running_until TIMESTAMPTZ, -- the run lease: live while in the future
running_by TEXT,
total_runs BIGINT NOT NULL DEFAULT 0, -- all-time, skips included
total_failures BIGINT NOT NULL DEFAULT 0,
last_run_at TIMESTAMPTZ, -- the most recently started run
last_duration_ms INTEGER,
last_success BOOLEAN,
last_error TEXT
);

Chacune est créée au démarrage si elle manque, de sorte qu’une base de données antérieure l’obtient à son prochain déploiement. Aucune n’est une collection, et le rôle d’utilisateur final rebase_user n’a aucun privilège sur elles : un cron_job_state accessible en écriture permettrait à un utilisateur connecté de mettre une tâche en pause pour tout le monde, ou de retenir son bail pour que rien ne l’exécute.

Depuis 0.25 La ligne d’une exécution et son décompte sont écrits en une seule instruction : l’insertion dans cron_logs et un upsert des compteurs de la tâche dans cron_job_state. Une liste lit une ligne de cron_job_state par tâche, quelle que soit la longueur de l’historique, et le planificateur fait de même lorsqu’il initialise totalRuns et totalFailures au démarrage. Les écritures de journal ne retardent pas l’exécution ; si l’une échoue, le planificateur enregistre l’erreur et poursuit, l’exécution restant dans la mémoire tampon circulaire en mémoire.

Une fois par heure, et au démarrage, le planificateur supprime les lignes d’historique de plus de 30 jours et compte toute exécution pas encore comptée, le tout par lots de 5 000 lignes. Un historique écrit avant la 0.24 est compté par le premier de ces balayages, hors du chemin de démarrage. Tant qu’il n’est pas terminé, une liste ajoute aux compteurs les exécutions pas encore comptées, de sorte que les chiffres ne changent pas à la mise à niveau. Il en va de même pour une exécution qu’un ancien serveur journalise pendant un déploiement progressif.

// backend/crons/cleanup-sessions.ts
import type { CronJobDefinition } from "@rebasepro/types";
import { rebase } from "@rebasepro/server";
const job: CronJobDefinition = {
schedule: "0 3 * * *", // daily at 3 AM
name: "Cleanup Expired Sessions",
description: "Removes user sessions older than 30 days",
async handler(ctx) {
ctx.log("Starting session cleanup...");
// Admin-scoped data access — see `ctx.rebase` above.
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
const expired = await ctx.rebase.dataAsAdmin.sessions.findAll({
where: { last_seen_at: ["<", cutoff] }
});
for (const session of expired) {
await ctx.rebase.dataAsAdmin.sessions.delete(session.id as string);
}
ctx.log(`Cleaned up ${expired.length} expired sessions`);
return { deletedSessions: expired.length };
},
};
export default job;

Chaque fichier cron constitue également une déclaration. rebase resources le liste sous le nom du fichier — le même identifiant sous lequel le planificateur l’exécute et que Studio affiche — avec sa planification et son fuseau horaire, de sorte qu’un hôte lit les planifications d’un projet avant de lancer quoi que ce soit. Un cron ne dépend d’aucune variable d’environnement ; rebase status l’affiche en vert sans rien à configurer.

Lire la planification implique d’importer le fichier, et rebase resources est une étape de build : aucun .env, aucun secret. Veillez donc à ce que la portée du module d’un cron ne contienne rien qui lise la configuration au moment de l’import — un client de base de données initialisé au début d’un helper, un fichier env.ts qui valide DATABASE_URL. Importez plutôt ce code à l’intérieur du gestionnaire :

async handler({ log }) {
const { runSeed } = await import("../src/seed.js");
await runSeed();
log("done");
}

Le gestionnaire s’exécute au sein du déploiement, là où ces variables existent. Un import de premier niveau de ce même module rend le graphe calculable uniquement sur une machine disposant par hasard d’un fichier .env — et cela charge l’intégralité de la dépendance à chaque démarrage qui se contente d’enregistrer la tâche.

Depuis 0.25 Charger un cron ne déclare rien. Une étape de build qui vous appartient et qui veut des crons dans le graphe le dit explicitement, avec declareCrons :

import { declareCrons, loadCrons } from "@rebasepro/server";
declareCrons(await loadCrons("./crons", { onSkip: (problem) => console.warn(problem) }));