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

Extensions

« Allowed memory size exhausted » sur un cron d’extension trop gourmand

Une tâche planifiée qui tournait sans problème depuis des mois s'arrête net avec une erreur fatale de mémoire. Le coupable : un chargement en masse jamais pensé pour durer.

Par Clément Hadrot • 3 juillet 2024 • 4 min de lecture • Aucun commentaire
« Allowed memory size exhausted » sur un cron d'extension trop gourmand

« Fatal error: Allowed memory size of 268435456 bytes exhausted » : ce message, apparu du jour au lendemain dans le journal d’erreurs, concerne une tâche planifiée qui synchronise chaque nuit le contenu d’une extension vers un service externe. Aucune modification récente du code n’explique, a priori, cette apparition soudaine, ce qui rend le diagnostic initial trompeur.

Symptôme

Le journal PHP affiche l’erreur systématiquement au même point du traitement, sans jamais dépasser la limite fixée à 256 mégaoctets dans wp-config.php les nuits précédentes. La tâche fonctionnait sans incident depuis plusieurs mois, ce qui exclut a priori un défaut de conception initial évident.

Diagnostic

L'essentiel à retenir : get_posts avec numberposts à -1 charge tout en mémoire d'un coup ; La croissance progressive du contenu rend le problème invisible pendant des mois ; Un traitement par lots avec offset résout durablement le symptôme

Le code de la tâche planifiée révèle rapidement l’origine du problème : un appel unique à get_posts() avec numberposts fixé à -1, censé charger l’ensemble du contenu concerné en une seule fois :

add_action( 'extension_synchronisation_nocturne', function() {
    $articles = get_posts( array(
        'post_type'   => 'produit',
        'numberposts' => -1,
        'post_status' => 'publish',
    ) );

    foreach ( $articles as $article ) {
        extension_envoyer_vers_service_externe( $article );
    }
} );

Ce code fonctionnait sans souci quand le catalogue comptait quelques centaines de produits. Mais chaque objet WP_Post chargé en mémoire, avec l’intégralité de son contenu et de ses métadonnées, occupe un espace non négligeable : la croissance progressive et invisible du catalogue, ajoutée semaine après semaine, a fini par dépasser la limite de mémoire allouée, sans qu’aucune modification de code ne soit en cause.

Correctif

La solution ne consiste pas à augmenter la limite de mémoire PHP, ce qui ne ferait que repousser le problème à un volume de contenu légèrement supérieur. Le traitement doit être découpé en lots de taille fixe, avec un décalage (offset) progressif, et les objets traités doivent être libérés explicitement de la mémoire au fur et à mesure :

add_action( 'extension_synchronisation_nocturne', function() {
    $taille_lot = 200;
    $offset     = 0;

    do {
        $articles = get_posts( array(
            'post_type'   => 'produit',
            'numberposts' => $taille_lot,
            'offset'      => $offset,
            'post_status' => 'publish',
            'fields'      => 'ids',
        ) );

        foreach ( $articles as $id ) {
            extension_envoyer_vers_service_externe( get_post( $id ) );
            clean_post_cache( $id );
        }

        $offset += $taille_lot;
    } while ( count( $articles ) === $taille_lot );
} );

Le paramètre fields => 'ids' évite de charger d’emblée l’objet complet pour chaque article, et l’appel à clean_post_cache() après traitement libère la copie mise en cache de l’objet, ce qui empêche l’accumulation silencieuse d’objets en mémoire pendant l’exécution de la boucle.

Une variante avec Action Scheduler

Pour un volume encore plus important, remplacer la boucle unique par plusieurs tâches enchaînées via Action Scheduler évite également le risque de dépassement du temps d’exécution maximal, souvent atteint en même temps que la limite de mémoire sur les gros catalogues : chaque lot devient sa propre tâche planifiée, avec sa propre allocation mémoire repartant de zéro.

Prévention

  • Ne jamais utiliser numberposts => -1 dans une tâche planifiée destinée à durer : ce paramètre convient à un affichage ponctuel sur un volume connu et borné, pas à un traitement récurrent sur un contenu en croissance.
  • Surveiller la mémoire réellement consommée par une tâche planifiée avec memory_get_peak_usage(), journalisée à la fin de chaque exécution, pour détecter une dérive progressive avant qu’elle ne provoque une erreur fatale en production.
  • Systématiser le traitement par lots dès la conception d’une tâche planifiée qui interroge du contenu, même si le volume initial semble anodin : un volume qui double chaque année transforme un code fonctionnel en incident différé, sans qu’aucune ligne n’ait été modifiée entre-temps.

Pour aller plus loin

Ce type d’incident illustre un piège classique des tâches planifiées : un code correct au moment de son écriture peut devenir défaillant uniquement parce que le volume de données qu’il traite a grossi, sans qu’aucune régression de code ne soit responsable. Concevoir dès le départ un traitement par lots, même pour un volume initial modeste, évite d’avoir à réagir en urgence le jour où la limite de mémoire est enfin atteinte.

Partager :

À propos de l'auteur

Clément Hadrot

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

Voir tous ses articles

Dans la même veine

À lire aussi