template_from_string(), plus de 80 000 fichiers, et un dossier que rien ne purge

Also available in English.

Cet article est nĂ© d’une digression. Pendant l’incident racontĂ© dans 14 minutes de cache:warmup sur EFS, il fallait aussi supprimer un template prĂ©cis dans le cache Twig (var/cache/prod/twig/). En le cherchant je suis tombĂ© sur un dossier de 9 Go.

$ du -sh var/cache/prod/twig
9G    var/cache/prod/twig

C’est beaucoup de noix ça !

Une recherche dans ce dossier a rĂ©vĂ©lĂ© des dizaines de milliers d’entrĂ©es __string_template__.

$ grep -lr '__string_template__' ./var/cache/prod/twig/ 2>/dev/null | wc -l
82173

Premier réflexe : chercher un createTemplate() maison, dans le code custom.

Il n’y en a pas. Aucun module ne fait ça cĂŽtĂ© Twig. Les seuls createTemplate() du pĂ©rimĂštre custom sont des appels Smarty, qui alimentent var/cache/*/smarty, pas var/cache/*/twig. Hors de cause.

Le vrai coupable est le colonel moutarde cƓur de PrestaShop 8 lui-mĂȘme.

Le mécanisme : template_from_string() sur chaque page

Chaque page du back-office basée sur un AdminController legacy (la quasi-totalité du back-office) passe par layout.html.twig, qui ouvre sur :

{% extends(template_from_string(
  getLegacyLayout(
    app.request.attributes.get('_legacy_controller'),
    layoutTitle is defined ? layoutTitle : '',
    ...
  )
)) %}

getLegacyLayout() ne se contente pas d’injecter quelques variables. Elle rĂ©cupĂšre le layout legacy complet, rendu cĂŽtĂ© Smarty, titre de page, fil d’Ariane, boutons de toolbar avec leurs tokens, lien d’aide, URL admin incluse.

Elle dĂ©coupe ce HTML autour du marqueur {$content}, puis rĂ©encode l’en-tĂȘte et le pied en une sĂ©rie de littĂ©raux Twig via escapeSmarty(). Twig impose une limite dure de 8191 caractĂšres (2^13 - 1) par littĂ©ral ; PrestaShop dĂ©coupe en blocs de 2000 pour rester loin en dessous. Tout ce HTML devient l’argument de template_from_string().

Or ce layout n’est pas le mĂȘme d’une page Ă  l’autre. Il embarque le titre de la page, le fil d’Ariane, des boutons dont les URL contiennent un token de sĂ©curitĂ©, et ces Ă©lĂ©ments changent selon la page et l’enregistrement affichĂ©s. Le HTML obtenu est donc rarement identique d’une requĂȘte Ă  l’autre.

Et Twig nomme un template créé Ă  partir d’une chaĂźne d’aprĂšs l’empreinte de cette chaĂźne entiĂšre : createTemplate() produit __string_template__ suivi du hash du texte. Un seul caractĂšre de diffĂ©rence, et c’est un autre nom, donc un autre fichier compilĂ© dans le cache.

template_from_string() compile une nouvelle entrée, __string_template__<hash>, à chaque fois. Quasiment jamais réutilisée tellement elle est spécifique.

Ce n’est pas un bug applicatif. C’est le mĂ©canisme qui fait le pont entre l’ancien systĂšme de contrĂŽleurs et le rendu Twig moderne, sur une bonne partie du back-office de PrestaShop 8, tel quel.

Et rien ne purge ce dossier tout seul : sans intervention manuelle, ce dossier ne fait que grossir.

Solution

Je ne veux pas modifier le cƓur de PrestaShop pour ça. Le plus simple est de nettoyer rĂ©guliĂšrement le dossier de cache avec un cron journalier.

find var/cache/prod/twig -type f -mtime +7 -delete

Vider var/cache/prod/twig se fait sans consĂ©quence : un template manquant se recompile Ă  la volĂ©e, Ă  la prochaine requĂȘte qui en a besoin. Vider le conteneur DI, Ă  l’inverse, peut bloquer tout le back-office le temps d’une recompilation : c’est l’incident racontĂ© dans l’article prĂ©cĂ©dent.

C’est la leçon la moins intuitive de cette sĂ©rie : dans Symfony, « vider le cache » n’est pas une opĂ©ration unique. C’est un ensemble de caches indĂ©pendants, avec des coĂ»ts de reconstruction et des rayons d’impact qui n’ont rien Ă  voir les uns avec les autres. Les connaĂźtre sĂ©parĂ©ment, c’est ce qui permet de cibler l’intervention au lieu de tout purger Ă  l’aveugle.