Le WordPress d'aujourd'hui, décodé pour les développeurs

Multilingue

Deux tâches WP-Cron qui se chevauchent pendant une resynchronisation traduite

Une condition de concurrence entre deux exécutions du planificateur laisse des chaînes traduites à moitié synchronisées : diagnostic et correctif.

Par WordPress Développement • 24 février 2024 • 5 min de lecture • Aucun commentaire
Deux tâches WP-Cron qui se chevauchent pendant une resynchronisation traduite

[24-Feb-2024 10:12:03 UTC] PHP Warning: Undefined array key 'de' in /wp-content/plugins/site-i18n-sync/includes/class-sync.php on line 118. Cette ligne de log, répétée une dizaine de fois dans la même minute, a mis la puce à l’oreille : une tâche de resynchronisation des traductions, censée tourner une fois par heure, semblait s’exécuter deux fois de suite à quelques secondes d’intervalle — parfois avec des résultats contradictoires sur les mêmes chaînes.

Le symptôme visible pour l’équipe éditoriale était plus simple à décrire qu’à diagnostiquer : certaines chaînes fraîchement traduites en allemand redevenaient temporairement identiques au français, avant de revenir à la bonne valeur quelques minutes plus tard. Un bug intermittent, difficile à reproduire à la demande, le genre qui use la patience de tout le monde.

Symptôme : deux exécutions pour un seul événement planifié

Le hook en question, site_i18n_resync_batch, était programmé via wp_schedule_event() avec une récurrence horaire. En théorie, un seul déclenchement par heure. En pratique, sur un site à trafic soutenu, WP-Cron se déclenche à chaque chargement de page (le mécanisme pseudo-cron de WordPress repose sur les visites, pas sur un vrai daemon système), et rien n’empêche deux requêtes HTTP simultanées de constater toutes les deux qu’un événement est « prêt » et de le lancer chacune de son côté.

function site_i18n_check_due_events() {
    $crons = _get_cron_array();
    foreach ( $crons as $timestamp => $hooks ) {
        if ( $timestamp > time() ) {
            continue;
        }
        // Rien n'empêche deux requêtes d'atteindre ce point en parallèle
    }
}

WordPress ne pose aucun verrou transactionnel sur cette vérification. Sur un hébergement avec plusieurs workers PHP-FPM actifs simultanément, deux requêtes peuvent parfaitement lire « cet événement est dû » au même instant et déclencher chacune leur propre traitement, avant que l’une ou l’autre n’ait eu le temps de reprogrammer l’événement suivant.

Diagnostic : reproduire le chevauchement

Pour confirmer l’hypothèse, on a ajouté un identifiant unique à chaque exécution, journalisé au début et à la fin du traitement :

add_action( 'site_i18n_resync_batch', function () {
    $run_id = wp_generate_uuid4();
    error_log( "sync start {$run_id}" );

    // ... traitement des chaînes ...

    error_log( "sync end {$run_id}" );
} );
L'essentiel à retenir : Deux exécutions concurrentes du même hook cassent l'intégrité des données ; wp_get_ready_cron_jobs ne verrouille rien par défaut ; Un verrou applicatif via une transient résout le chevauchement

En surveillant les logs sur une journée complète, deux run_id différents apparaissaient parfois avec un « start » à quelques secondes d’écart, sans qu’aucun « end » ne se soit produit entre les deux — la preuve du chevauchement. Le délai observé variait, mais dépassait rarement la minute et demie, ce qui correspondait bien à des requêtes concurrentes arrivant pendant un pic de trafic.

Correctif : un verrou applicatif via transient

La solution la plus simple, sans dépendance externe, consiste à poser un verrou avant de traiter le lot, avec une transient dont l’expiration protège contre un blocage permanent si le script plante en cours de route :

add_action( 'site_i18n_resync_batch', function () {
    $lock_key = 'site_i18n_sync_lock';

    if ( get_transient( $lock_key ) ) {
        return; // Une exécution est déjà en cours
    }

    set_transient( $lock_key, 1, 5 * MINUTE_IN_SECONDS );

    try {
        site_i18n_run_resync();
    } finally {
        delete_transient( $lock_key );
    }
} );

Le bloc finally garantit que le verrou est libéré même si une exception interrompt le traitement, évitant qu’une erreur bloque toute resynchronisation pendant les cinq minutes suivantes. La durée de la transient doit rester nettement supérieure au temps normal d’exécution du lot, sans quoi elle expirerait avant la fin d’un traitement légitime et laisserait passer un second chevauchement.

Une alternative plus robuste : verrou en base avec GET_LOCK

Sur un site à trafic très élevé, où deux workers pourraient lire la transient à quelques millisecondes d’écart (une transient stockée en wp_options n’est pas atomique par nature), une solution plus rigoureuse utilise le verrou nommé de MySQL :

global $wpdb;
$acquired = $wpdb->get_var( "SELECT GET_LOCK('site_i18n_sync', 2)" );

if ( '1' !== $acquired ) {
    return;
}

Ce mécanisme, natif à MySQL, garantit qu’un seul processus obtient le verrou à un instant donné, sans la fenêtre de risque inhérente à une lecture puis écriture séparées sur une transient.

Prévention : surveiller la santé du planificateur

  • Vérifier régulièrement wp cron event list pour repérer des événements en double ou en retard
  • Sur un site à fort trafic, désactiver le pseudo-cron natif (DISABLE_WP_CRON) et le remplacer par un vrai cron système appelant wp-cron.php à intervalle fixe
  • Ajouter un identifiant d’exécution dans les logs de toute tâche planifiée sensible, dès sa mise en production

Un événement WP-Cron n’est jamais garanti unique par construction : c’est au code qui l’exécute de poser ses propres garde-fous contre le chevauchement.

En résumé

Le chevauchement de deux exécutions du même hook WP-Cron reste un piège classique dès qu’une tâche devient sensible aux effets de bord d’une double exécution. Un verrou applicatif simple — transient ou GET_LOCK selon le niveau de trafic — suffit à éliminer ce genre de résynchronisation à moitié faite, sans devoir remplacer tout le système de planification.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi