Pour les plus frileux de Drupal, rien que le titre de cet article peut donner mal à la tête ! Pourtant l'idée est simple : dans Drupal, nous définissons parfois des routes dont la réponse sera une JsonResponse
, c'est à dire du contenu JSON. Ces routes sont généralement utilisées en callback, c'est à dire appelées en AJAX, parfois pour récupérer des données en asynchrones (chargement de points sur une carte) ou dans le cadre d'autocomplétion par exemple (barre de recherche).
Parfois également, le résultat de cette page (c'est à dire les données qu'elle renvoie) sont longues à récupérer, créer, mettre en forme, mais une fois fait ne varient pas ou (très peu). Dans ce cas, renvoyer une simple JsonResponse
c'est à dire sans activation de cache, va forcer Drupal à recalculer encore et encore ces mêmes données à chaque appel.
Ce que nous allons voir, c'est donc comment mettre en place le retour de cette route, et donc de ces données.
Définition classique d'un callback JSON
Voici la définition la plus simple que nous pourrions faire d'un callback JSON, à savoir la méthode autocompleteNoCache()
que nous considérons disponible depuis l'URL /api/nocache
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php namespace Drupal\demo\Controller; use Symfony\Component\HttpFoundation\JsonResponse; class AutocompleteController { /** * Returns some JSON content, with no caching. * * @return \Symfony\Component\HttpFoundation\JsonResponse * A JSON response. */ public function autocompleteNoCache() { $results = []; // Build here the $results data. // ... return new JsonResponse($results, 200); } }
Définition d'un callback JSON avec mise en place du cache
Reprenons la définition présentée au paragraphe précédent, et étendons là avec la gestion du cache.
Dans Drupal, le cache se gère grâce aux métadonnées de cache, c'est à dire le trio cache tags, cache contexts et max-age. Le but n'étant pas ici de présenter le fonctionnement du cache en soit, je ne reviendrais pas dessus mais ce sera prochainement l'objet d'un article spécifique.
Quoi qu'il en soit, nous allons substituer la JsonResponse
de notre callback par une CacheableJsonResponse
. Celle-ci va alors être capable de gérer les informations de cache que nous allons appliquer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php namespace Drupal\demo\Controller; use Drupal\Core\Cache\CacheableJsonResponse; use Drupal\Core\Cache\CacheableMetadata; class AutocompleteController { /** * Returns some JSON content, with caching enabled. * * @return \Drupal\Core\Cache\CacheableJsonResponse * A cached JSON response. */ public function autocompleteCache() { $results = []; // Build here the $results data. // ... // Build the appropriate cache metadatas. // You can use cache tags, contexts and max-age as needed $cacheMetadatas = [ '#cache' => [ 'tags' => ['node_list'], // given for example only ] ]; // Substitute the JsonResponse with a cache handled CacheableJsonResponse. $response = new CacheableJsonResponse($results, 200); $response->addCacheableDependency(CacheableMetadata::createFromRenderArray($cacheMetadatas)); return $response; } }
$results
pourrait être un render array il est courant de voir les propriétés de #cache
insérées dans $results
plutôt que dans un tableau séparé. Ce n'est pas gênant du tout, simplement ces données se retrouveront dans la réponse JSON également ce qui peut empêcher son traitement par une librairie jQuery ou JS qui ne s'y attendrait pas.Analyse de performance, invalidation et erreurs
Résultat et invalidation
Supposons que les deux callbacks présentés dans cet article soient retournés via deux URLs : /api/nocache et /api/cache. Grâce à la mise en place du debug des caches, nous pouvons constater les headers suivants.
- Pour la page /api/nocache
cache-control: must-revalidate, no-cache, private x-drupal-cache: UNCACHEABLE (no cacheability) x-drupal-dynamic-cache: UNCACHEABLE (no cacheability)
Les headers de debug du cache montrent une page qui ne peut être mise en cache.
Les headers retournés par la page montrent que celle-ci ne peut être mise en cache. C'est logique, dans ce cas, notre JsonResponse
ne contient aucune information de cache de Drupal. Le header Cache-Control
indique lui aussi que la page est incachable, permettant à un mécanisme externe (CDN, Varnish, etc...) de ne pas la prendre en compte.
- Pour la page /api/cache
cache-control: max-age=86400, public x-drupal-cache: HIT x-drupal-dynamic-cache: MISS x-drupal-cache-contexts x-drupal-cache-max-age: -1 (Permanent) x-drupal-cache-tags: http_response node_list
Les headers de debug du cache montrent une page requêtée en anonyme et servie depuis le cache Drupal.
Ici, les headers montrent que cette page peut être mise en cache une fois pour toute (max-age à -1 permanent), sauf si le cache tags node_list est invalidé (c'est à dire qu'un node peu importe lequel a été ajouté, modifié ou supprimé) ou que l'ensemble des réponses de Drupal sont invalidées (c'est le sens du cache tags http_response ajouté par Drupal à toutes ses réponses).
Dans notre cas, la page a été retournée par Drupal depuis son cache, en une centaine de millisecondes à peine. Nous devinons que cette requête a été faite anonymement, c'est à dire par un visiteur non connecté au site. C'est en effet Internal Cache Page qui a répondu et non Internal Dynamic Page Cache.
Le header Cache-Control
renvoyé correspond bien à une page qu'un mécanisme externe (CDN, Varnish, etc...) sera autorisé à mettre en cache pour une période de 24h tel que configuré sur mon site via /admin/config/development/performance
Performance
La grande question est : le jeu en vaut-il la chandelle ? Et la réponse est OUI !
Pour un but de test, la construction du tableau $results
va prendre au moins 1 seconde, process garanti par l'introduction d'un sleep(1)
. Nous avons donc deux URLs disponibles: /api/cache et /api/nocache.
Grâce à ApacheBench, nous pouvons tester la performance des deux URLs grâce aux commandes suivantes, permettant une moyenne des temps sur 100 essais pour 1 seul visiteur concurrent :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 > ab -n 100 -c 1 drupal.org/api/nocache Connection Times (ms) min mean[+/-sd] median max Connect: 88 129 70.3 111 561 Processing: 1402 1494 131.7 1437 2020 Waiting: 1399 1489 131.3 1434 2020 Total: 1500 1623 155.4 1558 2234 > ab -n 100 -c 1 drupal.org/api/cache Connection Times (ms) min mean[+/-sd] median max Connect: 80 113 48.2 105 493 Processing: 74 109 152.9 83 1525 Waiting: 74 108 152.9 82 1524 Total: 163 222 161.4 189 1638
La différence est assez nette. Dans le premier cas, nous aurons un process qui va encore et encore reconstruire nos données et donc se prendre notre délai d'une seconde. Dans le second, la réponse est renvoyée très tôt et servie directement du cache. Notons que la colonne max de la version cache est sensiblement similaire à la médiane de la version normale. J'ai fait le test en cache à froid et je suppose donc qu'il s'agit de la première occurrence : à savoir quand le résultat n'est pas encore disponible en cache.
Erreur potentielle
Dans de rares cas, vous pourriez être amené à avoir un callback fonctionnel en JsonReponse
, qui se met à renvoyer une erreur en CacheableJsonResponse
. Cette erreur va vous effrayer :
LogicException: The controller result claims to be providing relevant cache metadata, but leaked metadata was detected
D'une certaine façon, la raison est assez technique et provient du fonctionnement même de Drupal, du rendu des pages et de la gestion du cache. Pour approfondir le sujet et bien en comprendre la cause (et donc sa correction), je vous proposerais un article dédié à ce sujet.
D'ici là, pour ne pas vous laisser en plan, sachez que tout code qui passera par le service renderer avant d'être renvoyé dans une CacheableJsonResponse
génèrera cette réponse si les métadonnées associées au rendu ne sont pas récupérées d'une manière ou d'une autre et ajoutées manuellement aux métadonnées de cache de la réponse. En somme, si vous n'avez pas fait vous-même le travail de l'algorithme de bubbling qui est fait lors du rendu dans un render context.
Comme je le disais, ces dernières phrases sont complexes bien que claires si l'on en maîtrise les concepts et je développerais tout cela spécifiquement dans un prochain article.
Commentaires
Salut Dominique !
Quel chemin parcouru depuis le Drupal Camp Montpellier 2015 où tu poussais tes tous premiers commit pour la sortie de Drupal 8 et me demandais des conseils sur comment devenir Freelance :)
Ca fait super plaisir de voir comme tu as pris de la "Bouteille" et c'est un plaisir d'apprendre de nouveaux tips en TE lisant !!
Keep up the great work ;)
Nico
Ajouter un commentaire