Expressions Cron : pièges et tâches planifiées en production

Utilitaires

Expressions Cron : 5 champs pour maîtriser le temps

┌───────── Minute (0-59)
│ ┌───────── Heure (0-23)
│ │ ┌───────── Jour (1-31)
│ │ │ ┌───────── Mois (1-12)
│ │ │ │ ┌───────── Jour de la semaine (0-7, 0 et 7 sont dimanche)
│ │ │ │ │
* * * * *

I. Aide-mémoire de syntaxe

Expression Signification Fréquence d'exécution
* * * * * Chaque minute 1440 fois/jour
*/5 * * * * Toutes les 5 minutes 288 fois/jour
0 * * * * Chaque heure pile 24 fois/jour
0 9 * * * Tous les jours à 9:00 1 fois/jour
0 9 * * 1-5 Jours ouvrés à 9:00 5 fois/semaine
30 2 1 * * Le 1er de chaque mois à 2:30 1 fois/mois
0 0 1 1 * Le 1er janvier à 0:00 1 fois/an

Caractères spéciaux

Caractère Signification Exemple
* Toute valeur * * * * *
, Énumération 0 9,12,18 * * * (9h/12h/18h)
- Plage 0 9-17 * * 1-5 (jours ouvrés 9h-17h)
/ Pas */15 * * * * (toutes les 15 minutes)
L Dernier 0 0 L * * (dernier jour du mois)
W Jour ouvré le plus proche 0 0 1W * * (jour ouvré le plus proche du 1er)
# N-ième jour de la semaine 0 0 * * 5#3 (3e vendredi)

II. Pièges courants

Piège 1 : Spécifier simultanément le jour et le jour de la semaine

# Compréhension erronée : le 15 de chaque mois ET un vendredi
0 0 15 * 5

# Signification réelle : le 15 de chaque mois OU chaque vendredi (union !)
# L'écriture correcte nécessite une logique supplémentaire

Règle : Lorsque le jour et le jour de la semaine sont spécifiés simultanément, l'union est prise et non l'intersection.

Piège 2 : Problèmes de fuseau horaire

# Exécution attendue à 9:00 heure de Pékin
0 9 * * *

# Serveur en fuseau UTC → exécution réelle à 17:00 heure de Pékin !
# Solution : spécifier explicitement le fuseau horaire
// node-cron spécifier le fuseau horaire
cron.schedule('0 9 * * *', task, {
  scheduled: true,
  timezone: 'Asia/Shanghai',
});

Piège 3 : Erreur d'écriture de 60 secondes

# Incorrect : le champ des minutes n'a pas 60
0 0 60 * * *

# Correct : exécuter chaque heure
0 * * * *

Piège 4 : 30 février

# Février n'a pas de 30, cette expression ne s'exécutera pas en février
0 0 30 2 *

Piège 5 : Heure d'été

Lors du passage à l'heure d'été, certaines heures peuvent ne pas exister ou apparaître deux fois :

Printemps avancer : 2:00 → 3:00 (2:30 n'existe pas)
Automne reculer : 2:00 → 1:00 (1:30 exécuté deux fois)

Recommandation : Utiliser le fuseau horaire UTC pour éviter les problèmes d'heure d'été.


III. Vérifier les expressions Cron avec ToolsKu

Étapes de fonctionnement

  1. Ouvrir l'outil d'expressions Cron
  2. Saisir l'expression Cron
  3. Consulter l'interprétation en français :
    • 0 9 * * 1-5 → "Lundi au vendredi à 09:00"
    • */30 8-18 * * * → "Toutes les 30 minutes de 08:00 à 18:00 chaque jour"
  4. Consulter la liste des prochaines exécutions (5-10 prochaines)
  5. Vérifier si l'expression est valide

IV. Design de tâches planifiées de niveau production

Contrôle de concurrence

import { CronJob } from 'cron';

let isRunning = false;

const job = new CronJob('0 */5 * * * *', async () => {
  if (isRunning) {
    console.log("Le tour précédent n'est pas encore terminé, passage de cette exécution");
    return;
  }

  isRunning = true;
  try {
    await processData();
  } finally {
    isRunning = false;
  }
});

Verrou distribué (déploiement multi-instances)

import { Redis } from 'ioredis';

const redis = new Redis();

async function acquireLock(key, ttl = 300) {
  const acquired = await redis.set(key, '1', 'EX', ttl, 'NX');
  return acquired === 'OK';
}

const job = new CronJob('0 2 * * *', async () => {
  const lockKey = `cron:daily-report:${new Date().toISOString().slice(0, 10)}`;

  if (!(await acquireLock(lockKey))) {
    console.log("Une autre instance a déjà acquis le verrou, passage");
    return;
  }

  try {
    await generateDailyReport();
  } finally {
    await redis.del(lockKey);
  }
});

Nouvelles tentatives après échec

async function runWithRetry(task, maxRetries = 3, delay = 5000) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await task();
    } catch (error) {
      console.error(`Tentative ${attempt} échouée :`, error.message);
      if (attempt < maxRetries) {
        await sleep(delay * attempt); // Délai incrémentiel
      } else {
        throw error;
      }
    }
  }
}

const job = new CronJob('0 */10 * * * *', () => {
  runWithRetry(syncData, 3, 5000);
});

Contrôle du délai d'attente

async function runWithTimeout(task, timeoutMs = 60000) {
  return Promise.race([
    task(),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("La tâche a dépassé le délai d'attente")), timeoutMs)
    ),
  ]);
}

V. Surveillance des tâches

Journal d'exécution

const job = new CronJob('0 2 * * *', async () => {
  const startTime = Date.now();
  const context = { job: 'daily-sync', startedAt: new Date().toISOString() };

  try {
    await runWithRetry(syncData);
    context.status = 'success';
  } catch (error) {
    context.status = 'failed';
    context.error = error.message;
    await sendAlert(context); // Alerte
  } finally {
    context.duration = Date.now() - startTime;
    await logExecution(context); // Enregistrer le journal
  }
});

Vérification de santé

// Enregistrer l'heure de la dernière exécution réussie
await redis.set('cron:last-success:daily-sync', Date.now());

// Vérification de santé : si pas d'exécution depuis plus de 25 heures, déclencher une alerte
const lastSuccess = await redis.get('cron:last-success:daily-sync');
if (Date.now() - lastSuccess > 25 * 3600 * 1000) {
  await sendAlert({ type: 'cron_stale', job: 'daily-sync' });
}

VI. Modèles courants de tâches planifiées

Besoin Expression Cron Description
Synchronisation horaire des données 0 * * * * À l'heure pile
Rapport matinal des jours ouvrés 0 9 * * 1-5 Lundi au vendredi à 9:00
Sauvegarde quotidienne 0 2 * * * À 2:00 du matin
Nettoyage hebdomadaire 0 3 * * 0 Dimanche à 3:00
Rapprochement mensuel 0 6 1 * * Le 1er de chaque mois à 6:00
Heartbeat toutes les 15 minutes */15 * * * * Vérification haute fréquence
Rapport trimestriel 0 8 1 1,4,7,10 * Premier jour du mois du trimestre

VII. Liste des meilleures pratiques

  1. Toujours utiliser le fuseau horaire UTC, sauf si le métier dépend fortement de l'heure locale
  2. Ne pas spécifier le jour et le jour de la semaine simultanément, éviter le piège de l'union
  3. Contrôle de concurrence : une seule instance en cours d'exécution à un moment donné
  4. Verrou distribué : prévenir l'exécution dupliquée lors de déploiements multi-instances
  5. Nouvelles tentatives + alertes après échec : ne pas échouer silencieusement
  6. Contrôle du délai d'attente : empêcher les tâches de se bloquer indéfiniment
  7. Design idempotent : les exécutions répétées donnent des résultats identiques
  8. Journal d'exécution : enregistrer l'heure, la durée et le statut de chaque exécution
  9. Vérifier les expressions avec des outils : confirmer la sémantique correcte avant le déploiement
#Cron#定时任务#crontab#时区#运维