ENbext.dev
DocumentationFonctionnalités

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

Programmé

La capacité Scheduled est la façon dont un plugin dit "exécuter cette chose selon un calendrier". Un plugin Scheduled déclare une liste de plannings au démarrage – chacun a un identifiant, une expression cron et une chaîne de configuration facultative – et le runtime de bext appelle run(ctx) sur le plugin au bon moment, en passant un ScheduledRunCtx qui indique au plugin quel planning vient de se déclencher.

C'est volontairement une forme très petite. La surface des traits comprend quatre méthodes : name(), schedules(), run(ctx) et cleanup(). Tout le reste – analyse cron, gestion des exécutions manquées, verrouillage multi-instance – est le travail du runtime, pas celui du plugin. Cela permet de conserver les implémentations de référence telles que @bext/cron suffisamment petites pour tenir sur une page tout en donnant aux opérateurs un seul bouton à tourner lorsqu'ils doivent changer de backend.

Déclaratif, pas dynamique#

La décision de conception la plus importante dans Scheduled est que les plannings sont déclarés et non enregistrés. Un plugin n'appelle pas de fonction hôte scheduler.register(...) au moment de l'exécution. Au lieu de cela, lorsque l'hôte demande au plugin what schedules do you have?, le plugin renvoie la liste complète d'un seul coup, et l'hôte possède désormais la machinerie cron.

La raison pour laquelle cela est important : un environnement d'exécution qui possède la liste de planification peut l'inspecter, détecter les chevauchements, valider la syntaxe cron avant que quoi que ce soit ne se déclenche, conserver l'historique d'exécution dans un magasin, coordonner le verrouillage multi-instance et afficher l'image complète dans la TUI sans rappeler le plugin. Un environnement d'exécution qui reçoit des planifications une par une via une fonction hôte n'a aucune de ces propriétés, car il ne voit jamais la forme complète.

C'est également pourquoi schedules() renvoie un Result<Vec<Schedule>, String> et non un flux : une liste de planification qui ne peut pas être énumérée au démarrage est, par définition, un état dynamique et l'état dynamique appartient à l'application, pas au planificateur.

Le trait#

rust
pub trait ScheduledPlugin: Send + Sync {
    fn name(&self) -> &str;
    fn priority(&self) -> u32 { 1000 }
    fn schedules(&self) -> Result<Vec<Schedule>, String>;
    fn run(&self, ctx: &ScheduledRunCtx) -> Result<(), String>;
    fn cleanup(&self) -> Result<(), String> { Ok(()) }
}
  • name() : un court identifiant que le runtime utilise dans les journaux, les métriques et la TUI. Gardez-le stable ; les opérateurs l’apprécient.
  • priority() — lorsque deux plugins fournissent le même identifiant de planification (rare, mais possible lors des migrations), celui qui a la priorité la plus élevée l'emporte. La valeur par défaut est 1000.
  • schedules() — renvoie la liste complète des planifications au démarrage. Appelé une fois par chargement de plugin. Un Err(...) abandonne ici le chargement du plugin ; un plugin mal configuré devrait échouer bruyamment plutôt que de ne rien exécuter en silence.
Attention

Si schedules() renvoie une erreur, le plugin ne se charge pas complètement : aucune planification de ce plugin ne se déclenchera. Validez vos expressions cron et votre configuration au moment de la construction, pas à l'intérieur de run().

  • run(ctx) — appelé à chaque fois qu'un planning se déclenche. ctx.schedule_id indique au plugin lequel ; si le plugin possède plusieurs plannings, il les répartit en interne sur ce champ.
  • cleanup() — appelé lors du déchargement du plugin. La valeur par défaut est no-op, ce qui est la bonne réponse pour la grande majorité des plugins.

Le modèle de répartition run() (une méthode saisie sur schedule_id) est intentionnel. Une version antérieure comportait des méthodes run_<schedule_id>, qui faisaient exploser la surface des traits et faisaient du WASM ABI un cauchemar. Un seul run(ctx) maintient le côté FFI propre et permet d'envoyer un if / match à l'intérieur du plugin, ce qui est bon marché.

Schedule#

Chaque planning est une structure de données simples que le plugin remplit :

rust
pub struct Schedule {
    pub id: String,
    pub description: String,
    pub cron: String,
    pub jitter_ms: u64,
    pub missed_run_policy: MissedRunPolicy,
    pub locking: LockingHint,
    pub config_json: String,
}
  • id est l'identifiant local du plugin. Doit être unique au sein d'un plugin.
  • description est une étiquette lisible par l'homme qui apparaît dans la TUI et les journaux.
  • cron est l'expression cron. Voir la syntaxe acceptée ci-dessous.
  • jitter_ms s'étend sur une fenêtre aléatoire pour éviter le tonnerre du troupeau, ce qui est utile lorsque de nombreuses instances partagent une horloge.
  • missed_run_policy indique au runtime quoi faire lorsque le planificateur a manqué une fenêtre d'incendie (veille, redémarrage, déploiement).
  • locking indique si cette planification doit s'exécuter une fois par flotte ou une fois par instance.
  • config_json est une chaîne opaque que le plugin se donne – le moteur d'exécution ne l'interprète pas, mais elle fait partie de la forme de la planification afin que les opérateurs puissent voir exactement ce pour quoi une planification est configurée.

Tout sur Schedule est de type POD (pas de tranches empruntées, pas de durée de vie générique) car les planifications traversent l'ABI WASM et la convention de chaîne JSON maintient la mise en page plate.

Syntaxe Cron#

L'implémentation de référence — @bext/cron — accepte le même sous-ensemble que le planificateur intégré de bext, donc les deux ne peuvent pas être en désaccord :

Formulaire Exemple Signification
Intervalle de secondes 30s toutes les 30 secondes
Intervalle de minutes 5m toutes les 5 minutes
Intervalle d'heures 2h toutes les 2 heures
Raccourci @hourly en haut de chaque heure
Raccourci @daily minuit UTC
Cron */N en minutes */5 * * * * toutes les 5 minutes (aligné sur le calendrier)
Cron */N pendant les heures 0 */3 * * * toutes les 3 heures à :00 (calendrier)

Il s’agit délibérément d’un petit sous-ensemble. Une grammaire cron complète est un océan de cas extrêmes ( L, W, nième jour du mois, jour de la semaine contre jour du mois, DST) et un environnement d'exécution qui tente de prendre en charge tout cela enverra un bug de planificateur par trimestre pour toujours. Si vous avez besoin d'une syntaxe plus riche, c'est le travail d'un plugin tiers construit sur le même trait - et comme le trait est façonné en fonction des capacités, remplacer @bext/cron par un plugin plus riche est un changement de configuration, pas une réécriture.

MissedRunPolicy#

Que faire lorsque le planificateur se réveille et découvre qu'il a dormi à travers une fenêtre coupe-feu :

Variante Signification
RunOnce Lancez le planning exactement une fois maintenant, même si plusieurs fenêtres ont été manquées. Défaut.
Skip Ne faites rien ; attendez la prochaine fenêtre régulière.
RunAll Lancez le planning une fois pour chaque fenêtre manquée, dans l'ordre. Utiliser avec précaution.

La valeur par défaut est RunOnce, car c'est presque toujours ce que vous voulez : une tâche idempotente (garbage collection, réchauffement du cache, cumul de métriques) devrait rattraper son retard, mais pas dix fois en rafale. RunAll ne convient que pour les charges de travail strictement ordonnées où le plugin lui-même est responsable de la sémantique de la file d'attente.

Conseil

Préférez RunOnce ou Skip pour la plupart des tâches. RunAll peut produire une grande rafale d'exécutions consécutives après une longue panne : utilisez-le uniquement lorsque le plugin a réellement besoin de traiter chaque fenêtre manquée et lorsque chaque exécution est peu coûteuse et indépendante.

Lorsqu'une exécution manquée se déclenche, ctx.is_catchup est défini sur true sur le ScheduledRunCtx. Un plugin qui souhaite se comporter différemment en mode rattrapage (ignorer les rollups coûteux, par exemple) peut vérifier cet indicateur.

LockingHint#

Les déploiements multi-instances doivent garantir qu'une planification donnée se déclenche une fois dans toute la flotte, et non une fois par instance. L'énumération LockingHint indique au runtime à quel point le plugin s'en soucie :

Variante Signification
NodeLocal Feu par nœud. À utiliser pour les tâches par instance (cache chaud, GC local).
RequireGlobal Doit tirer exactement une fois sur toute la flotte. Abandonnez l'exécution s'il n'y a pas de verrouillage global. Défaut.
PreferGlobal Préférez global, mais revenez à par nœud si aucun verrou n'est disponible.

L'exactitude multi-instance est un véritable problème pour les systèmes distribués et l'implémentation finale dépend du Capacité Locking - le trait et ses backends ont été livrés, mais leur consommation par le planificateur est toujours en cours de connexion. Le runtime d'aujourd'hui honore NodeLocal (c'est le cas trivial), et PreferGlobal se dégrade en NodeLocal sur un déploiement à instance unique. Une fois que le planificateur est connecté à Locking, RequireGlobal et la branche de verrouillage global de PreferGlobal commenceront à prendre un véritable bail distribué – et aucun code de plugin n'a besoin de changer pour que cela se produise.

Exemple : bext.config.toml#

Le planificateur intégré (qui délègue aux implémentations ScheduledPlugin dans une version ultérieure) lit à partir d'un tableau [[tasks]] de niveau supérieur, et @bext/cron accepte la même forme que sa propre configuration JSON :

toml
[[tasks]]
id          = "gc"
cron        = "5m"
description = "garbage collect expired tokens"

[[tasks]]
id          = "daily-report"
cron        = "@daily"
description = "email yesterday's traffic report"
config_json = '{"recipients":["ops@example.com"]}'

[[tasks]]
id          = "cache-warm"
cron        = "*/10 * * * *"
description = "warm edge cache for top 100 pages"

Chaque entrée [[tasks]] devient une Schedule sur le ScheduledPlugin actif. Changer de backend est un changement de configuration sur une seule ligne :

toml
# dev: use the reference plugin, dispatch inside the worker
[scheduled]
provider = "cron"

# prod: use a registry-backed scheduler plugin with a UI,
# same [[tasks]] entries, no code changes
[scheduled]
provider = "my-org-scheduler"

Écriture d'un plugin Scheduled#

La mise en œuvre la plus courte possible, ce qui correspond également à peu près à ce que fait @bext/cron :

rust
use bext_plugin_api::scheduled::{
    LockingHint, MissedRunPolicy, Schedule, ScheduledPlugin, ScheduledRunCtx,
};
use std::sync::Arc;

type RunCallback = Arc<dyn Fn(&ScheduledRunCtx) -> Result<(), String> + Send + Sync>;

struct MyScheduler {
    schedules: Vec<Schedule>,
    callback: RunCallback,
}

impl ScheduledPlugin for MyScheduler {
    fn name(&self) -> &str {
        "my-scheduler"
    }

    fn schedules(&self) -> Result<Vec<Schedule>, String> {
        Ok(self.schedules.clone())
    }

    fn run(&self, ctx: &ScheduledRunCtx) -> Result<(), String> {
        (self.callback)(ctx)
    }
}

C'est tout le plugin. Tout ce qui est en forme de cron - analyse, déclenchement, rattrapage, gigue, verrouillage - est le problème du runtime, pas le vôtre.

Lorsque Scheduled n'a pas la bonne forme#

Scheduled est destiné au travail court, répétitif et indépendant de : garbage collection, rapports par e-mail, échauffement du cache, sondes d'intégrité. Ce n'est volontairement pas pour :

  • Travaux de longue durée qui prennent des heures. Une exécution Scheduled devrait se terminer en quelques secondes ou minutes ; tout ce qui est plus long devrait mettre le travail en file d'attente et laisser un travailleur distinct le traiter.
  • Flux multi-étapes qui nécessitent un état durable entre les étapes. C'est le travail de la capacité Workflow : une forme future qui suit la progression aux points de contrôle et peut survivre aux redémarrages à mi-flux. Lorsque Workflow atterrira, attendez-vous à le voir documenté à côté de cette page.
  • Travail piloté par les événements déclenché par des webhooks, des arrivées dans des files d'attente ou des téléchargements de fichiers. Ceux-ci appartiennent aux capacités Queue et Hook.

Si votre tâche consiste à « exécuter ceci toutes les 10 minutes et à le faire en moins d'une minute », Scheduled est la bonne forme. S'il s'agit de « lancer un pipeline de 6 heures tous les dimanches à 3 heures du matin », faites appel à Scheduled pour déclencher le coup d'envoi et confier le travail réel à quelque chose de durable.

Où aller ensuite#

Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.