Aller au contenu
Bastide Connexion Créer un compte
Appuyez sur / pour chercher

Tâches planifiées

EnglishMarkdownv0.3.0

Une tâche planifiée exécute du travail à heure fixe, sans visiteur : purger des sessions, envoyer un récapitulatif, recalculer un classement. Bastide en offre deux sortes : les tâches de l'application, déclarées dans glowsoft.json, et les tâches en base, écrites en SQL à la manière de pg_cron. Cette page décrit les deux, leur syntaxe, leur fuseau, leurs limites par palier et la lecture de leurs exécutions.

Les tâches de l'application

Elles se déclarent dans le glowsoft.json à la racine de votre dépôt, et prennent effet au déploiement suivant. Elles ne s'exécutent que pour la production.

Appeler un chemin

La forme est celle de vercel.json : un projet importé de Vercel garde ses tâches.

{
  "crons": [
    { "path": "/api/cron/nettoyage", "schedule": "0 3 * * *" },
    { "path": "/api/cron/rapport?format=court", "schedule": "30 7 * * MON", "timezone": "Europe/Paris", "overlap": "skip", "timeoutSeconds": 120, "retries": 1 }
  ]
}

À chaque échéance, la plateforme appelle GET <path> sous le nom d'hôte de la production, en passant par l'edge comme un visiteur : une application endormie se réveille pour sa tâche. L'appel porte :

  • User-Agent: bastide-cron/1 ;
  • X-Bastide-Execution : l'identifiant unique de la tentative (exp_…) ;
  • X-Bastide-Signature : une signature Ed25519, t=<secondes Unix>,kid=<empreinte>,v1=<signature en base64url>.

Une réponse 2xx réussit la tentative ; toute autre l'échoue, et elle se réessaie. Une application qu'on ne joint pas (aucune connexion, ou un 502 de la plateforme) fait dire l'échéance manquée après sa dernière tentative.

Vérifier que l'appel vient de la plateforme

User-Agent et X-Bastide-Execution ne prouvent rien : vérifiez la signature. Les octets signés sont six lignes jointes par \n, sans ligne finale : bastide-cron/1, GET, l'en-tête Host reçu en minuscules, le chemin reçu (chaîne de requête comprise), la valeur de X-Bastide-Execution, et t.

  1. Lisez la clé publique une fois : GET https://api.bastide.app/v1/scheduled-runs/signing-key rend { algorithm, keyId, publicKey }, la clé en PEM. Aucun jeton n'est demandé.
  2. Refusez un kid qui n'est pas le keyId lu : la clé a été remplacée, relisez-la.
  3. Refusez un t à plus de 300 secondes de votre horloge.
  4. Vérifiez la signature sur ce que vous avez reçu. Pour refuser un rejeu, retenez X-Bastide-Execution : il ne revient jamais.
import { createPublicKey, verify } from "node:crypto";

const cle = await (await fetch("https://api.bastide.app/v1/scheduled-runs/signing-key")).json();
const publique = createPublicKey(cle.publicKey);

export function vientDeLaPlateforme(req) {
  const m = /^t=(\d+),kid=([0-9a-f]{16}),v1=([A-Za-z0-9_-]{86})$/.exec(
    req.headers["x-bastide-signature"] ?? "",
  );
  if (!m || m[2] !== cle.keyId) return false;
  if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
  const signe = [
    "bastide-cron/1",
    "GET",
    String(req.headers.host).toLowerCase(),
    req.url,
    req.headers["x-bastide-execution"],
    m[1],
  ].join("\n");
  return verify(null, Buffer.from(signe), publique, Buffer.from(m[3], "base64url"));
}

Lancer un conteneur : le service cron

Une tâche peut aussi tourner dans son propre conteneur, sans passer par votre service web :

{
  "services": [
    { "name": "web", "type": "web" },
    { "name": "nettoyage", "type": "cron", "start": "nettoyer", "schedule": "0 3 * * *" }
  ]
}
  • services porte toujours exactement un web.
  • start est le nom d'un script de votre package.json, jamais une commande. Le conteneur se construit comme un worker et démarre par ce script.
  • Aucun appel ne part et rien n'est signé : le code de sortie conclut la tentative, zéro réussit, tout autre échoue et se réessaie. Passé son délai de garde, le conteneur est arrêté et la tentative échoue.

Les champs d'une tâche

ChampCe qu'il ditDéfaut
pathLe chemin appelé, chaîne de requête comprise ; commence par /, 255 caractères au plus.requis (tâche par chemin)
scheduleL'expression, 128 caractères au plus.requis
timezoneUn nom de la base IANA.Europe/Paris
overlapSi l'exécution précédente tourne encore : skip écarte l'échéance, queue l'attend, kill arrête la précédente.skip
timeoutSecondsLe délai de garde d'une exécution, de 10 à 3 600 secondes.300
retriesLes réessais d'une exécution en échec, de 0 à 5, à 30 secondes de recul doublé à chaque tentative.2

Au moins une fois

Une tentative peut s'exécuter plus d'une fois : un réessai suit un échec même quand votre code avait fini son travail, et le conteneur d'un service cron dont le nœud est remplacé repart sur le suivant. Écrivez une tâche qui se rejoue sans dommage. Une échéance découverte avec plus de cinq minutes de retard n'est pas lancée : elle est inscrite manquée. Un déploiement neuf ne rattrape pas les échéances passées.

La syntaxe des expressions

Cinq champs, à la minute : minute, heure, jour du mois, mois, jour de la semaine.

ÉlémentExempleSens
Valeur30 7 * * *Chaque jour à 7 h 30.
Liste0 8,12,18 * * *À 8 h, 12 h et 18 h.
Intervalle0 9-17 * * MON-FRIChaque heure de 9 h à 17 h, du lundi au vendredi.
Pas*/15 * * * *Toutes les quinze minutes.
Noms0 0 1 JAN *Les mois JAN…DEC et les jours SUN…SAT, sans casse.
Raccourcis@daily@hourly, @daily, @midnight, @weekly, @monthly, @yearly, @annually.
  • Le dimanche vaut 0 et 7.
  • Quand le jour du mois ET le jour de la semaine sont restreints, la tâche passe si l'un OU l'autre convient : 0 0 1 * MON veut dire « le 1er et chaque lundi », comme chez cron et Vercel.
  • Les changements d'heure suivent la règle de cronie. Au printemps, une tâche à heure fixe tombée dans l'heure qui n'existe pas passe une fois, à la première minute après le saut. À l'automne, elle passe à la première occurrence seulement. Une tâche dont le champ des heures est * ou */n suit le temps réel.
  • La granularité est la minute : une expression à la seconde est refusée.

Une expression ou un fuseau faux écartent la tâche sans faire échouer le déploiement : le journal du déploiement dit le rang de la tâche, le champ et la règle, jamais la valeur écrite.

Les tâches en base

Pour du travail purement SQL, écrivez la tâche dans votre base, avec les fonctions que vous écririez chez Supabase :

select cron.schedule('purge-sessions', '*/10 * * * *', $$ delete from sessions where expire_le < now() $$);
select cron.unschedule('purge-sessions');
select * from cron.job_run_details order by start_time desc limit 20;

cron.alter_job(job_id, active, fenetre_secondes) suspend ou réactive une tâche et règle sa fenêtre. Ce n'est pas pg_cron : la plateforme lit vos tâches et les exécute elle-même, ce qui change plusieurs choses.

  • Le fuseau est UTC, et la granularité la minute.
  • La commande s'exécute dans une transaction, sous le rôle de votre projet, par le mutualiseur : ni VACUUM ni COMMIT. Elle est arrêtée au bout de 10 minutes.
  • 50 tâches au plus par base.
  • Une fenêtre (de 60 à 3 600 secondes, 300 par défaut) : une échéance qui n'a pas démarré dans ce délai est inscrite missed, avec sa cause, au lieu de disparaître. Une échéance dont l'exécution précédente tourne encore l'est aussi.
  • Une tâche neuve est vue au plus tard cinq minutes après sa création.
  • L'historique (cron.job_run_details, états running, succeeded, failed, missed) est purgé au-delà de 30 jours.
  • Une alerte : chaque échec ou échéance manquée émet l'événement database.cron.failed vers les points de réception du projet, groupé par tâche (voir Webhooks).

Les limites par palier

PalierTâches de l'application par projet
Découverte5
Standard20
Pro100
Souverain500

Les tâches par chemin (crons) et les services cron comptent ensemble. Toutes les tâches déclarées comptent, même celles qui seront écartées pour une expression fausse. Au-delà du plafond, le déploiement est refusé avec un message qui nomme le palier et le plafond. Les tâches en base ont leur propre plafond de 50 par base, quel que soit le palier.

Lire les exécutions

Chaque tentative d'une tâche de l'application (son échéance, son état, sa durée, son code, et le début de la réponse ou de ce que le conteneur a écrit) est gardée 90 jours.

  • Console : l'écran Journaux, vue « Tâches planifiées ».
  • Agent : logs_read avec scheduledRuns: true ; cinquante tentatives au plus par lecture, des plus récentes aux plus anciennes, le reste compté.
  • Ligne de commande : bastide logs read --project-id <projet> --scheduled-runs.
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "environmentId": "env_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "scheduledRuns": true
}

Les exécutions des tâches en base se lisent dans cron.job_run_details, par l'éditeur SQL ou database_sql_read.

Erreurs fréquentes

  • « N tâche(s) planifiée(s) écartée(s), le déploiement continue » : lisez le rang et le champ ; corrigez l'expression ou le fuseau.
  • Déploiement refusé, « au-delà des N que le palier permet » : retirez des tâches ou changez de palier.
  • Échéance manquée : l'application n'a pas répondu (arrêtée, 502) ; lisez ses journaux.
  • La tâche tourne deux fois : c'est un réessai ; rendez-la rejouable avec X-Bastide-Execution.
  • Une tâche en base ne part pas : vérifiez qu'elle est active et que son expression a cinq champs ; une expression refusée n'est jamais exécutée, et le refus n'est signalé qu'une fois.

Voir aussi

Signaler une erreur sur cette page