Connexion utilisateur
Sommaire
Commentaires récents
 
Changer le thème de drupal, avec le cache
Le 14 mars 2008, à 10:58 par Ulhume...

Dans le volet précédent de la saga "Comment changer dynamiquement de thème Drupal, notre module souffrait d'un défaut de taille : Il fallait désactiver le cache pour qu'il fonctionne. Nous allons voir maintenant comment le modifier pour que la même chose soit possible sans perte de performances.

Gestion du cache

Le cache de page sous Drupal est un mécanisme bête et efficace. S'il est actif, on commence avant d'afficher quoi que ce soit, par vérifier qu'il n'existe pas un enregistrement correspondant à l'URL demandée dans la table cache_page. Si c'est le cas, les données de cet enregistrement sont directement expédiées sur le navigateur client et le traitement s'arrête là. Le cache de page contient donc toute la page, thème compris. Et c'est bien là notre problème.

Ce cache peut être soit désactivé, soit en mode "normal", soit en mode "agressif". En mode agressif, aucun module ne peut intervenir et le site est quasiment "statique". C'est pour cela qu'il est assez rarement utilisé. En mode normal, certains modules dits "bootstrap" (voir l'article précédent) peuvent avoir une action sur le contenu, c'est ce que nous allons exploiter.

Bien évidement, une page mise en cache n'est pas valide à vie. Il y a un paramètre que vous pouvez modifier dans les performances indiquant de durée de validité des enregistrements. Passé ce temps, la page est refabriquée et vient écraser l'ancienne version en base.

Un autre aspect important à connaître sur ce cache de page, c'est qu'il est désactivé pour tout utilisateur connecté. Ce qui peut poser un problème de performance dans le cas où votre site n’a que des utilisateurs authentifiés. En gros, le cache n'agit que sur la version anonyme du site et c'est un aspect à ne pas négliger car lorsque l'on développe, nous sommes souvent connectés, et le comportement n'est plus du tout le même...

Donc pour créer un thème dynamique en fonction du navigateur cible, avec un cache en mode "normal", on va tomber sur le problème du "premier qui gagne". Si un navigateur IE6 passe avant les autres, c'est le thème IE6 qui va finir dans le cache et tout le monde sera logé à la même enseigne. Pire, vu qu'il y a un enregistrement par page, et donc à la rubrique/billet affiché, on aurait rapidement un site "arlequin" avec un thème qui change de manière aléatoire d'une page à l'autre.

La seule solution pour régler ce problème est donc de modifier la stratégie de gestion du cache.

Stratégie de cache

Malheureusement il n'y a pas de hooks que l'on puisse implémenter pour changer la manière dont fonctionne le cache. Il va donc nous falloir modifier les sources du coeur de Drupal. Prenez donc soin d'en faire une copie préalable.

Le principe général est assez simple. Drupal retrouve le bon enregistrement d'une page dans le cache en fonction d'une clef. Cette clef est simplement l'URL de la page à afficher. Il va donc nous falloir changer un peu cela pour que la nouvelle clef soit cette fois l'URL additionnée du thème $custom_theme.

La stratégie du cache de page est pris en charge dans le module includes/bootstrap.inc, dans la fonction _drupal_cache_init. C'est là que sont appelés nos kooks _init et _exit. Et c'est aussi là qu'est fait la distinction entre les trois modes du cache.

Le problème est que le hook _init est appelé après récupération de la page en cache. Et du coup notre clef va être fausse. Il faut donc modifier la fonction pour faire appel à notre module avant que le cache ne soit lu. Malheureusement il n'existe pas de fonction dans l'API de cache pour vérifier qu'un enregistrement existe avant de le charger effectivement. Et si l'on continue d'utiliser le hook _init, on prend le risque qu'il soit appelé deux fois de suite : une fois avant la lecture en cache, et une fois de plus s'il n'y avait pas d'enregistrement en cache pour cette page. Ce n'est pas un problème pour notre module mais cela risque d'en devenir un pour les autres modules "bootstrap" utilisant init/exit. Nous allons donc éviter le problème en créant notre propre hook, _preinit, et en l'invoquant avant la récupération des données en cache. Cela nous donne la fonction de remplacement suivante :

includes/bootstrap.inc
  1. function _drupal_cache_init($phase) {
  2.   require_once variable_get('cache_inc', './includes/cache.inc');
  3.  
  4.   if ($phase == DRUPAL_BOOTSTRAP_EARLY_PAGE_CACHE && variable_get('page_cache_fastpath', 0)) {
  5.     if (page_cache_fastpath()) {
  6.       exit();
  7.     }
  8.   }
  9.   elseif ($phase == DRUPAL_BOOTSTRAP_LATE_PAGE_CACHE) {
  10.       if (variable_get('cache', CACHE_DISABLED) == CACHE_AGGRESSIVE) {
  11.         if ($cache = page_get_cache()) {
  12.           drupal_page_cache_header($cache);
  13.           exit();
  14.         }
  15.       }
  16.       elseif (variable_get('cache', CACHE_DISABLED) == CACHE_NORMAL) {
  17.         require_once './includes/module.inc';
  18.         bootstrap_invoke_all('preinit');
  19.         if ($cache = page_get_cache()) {
  20.           bootstrap_invoke_all('init');
  21.           drupal_page_cache_header(page_get_cache());
  22.           bootstrap_invoke_all('exit');
  23.           exit();
  24.         }
  25.         return;
  26.       }
  27.     require_once './includes/module.inc';
  28.   }
  29. }

Ensuite, nous allons modifier le module de l'article précédent pour renommer _init en _preinit, et créer une nouvelle fonction _init qui sera cette fois vide.

Lecture et écriture du cache

Maintenant nous allons modifier les fonctions qui lisent et qui écrivent dans le cache pour ajouter le thème dans la clef. La lecture tout d'abord, se trouve dans includes/bootstrap.inc, fonction page_get_cache. La modification est enfantine, elle consiste juste à déclarer en global notre variable $custom_theme et ajouter cette référence dans la clef de lecture du cache :

includes/bootstrap.inc, fonction page_get_cache
  1. function page_get_cache() {
  2.   // { Premiere modification
  3.   global $user, $base_root, $custom_theme;
  4.   // }
  5.  
  6.   $cache = NULL;
  7.  
  8.   if (!$user->uid && $_SERVER['REQUEST_METHOD'] == 'GET' && count(drupal_set_message()) == 0) {
  9.     // { Deuxième modification
  10.     $cache = cache_get($base_root . request_uri()."___".$custom_theme, 'cache_page');
  11.     // }
  12.  
  13.     if (empty($cache)) {
  14.       ob_start();
  15.     }
  16.   }
  17.  
  18.   return $cache;
  19. }

Enfin, il ne nous reste plus qu'à modifier la fonction de sauvegarde de la page, qui elle se trouve dans includes/common.inc, fonction page_set_cache. Et c'est le même type d'altération :

includes/bootstrap.inc, fonction page_get_cache
  1. function page_set_cache() {
  2.   // { première modification
  3.   global $user, $base_root, $custom_theme;
  4.   // }
  5.  
  6.   if (!$user->uid && $_SERVER['REQUEST_METHOD'] == 'GET' && count(drupal_get_messages(NULL, FALSE)) == 0) {
  7.     // This will fail in some cases, see page_get_cache() for the explanation.
  8.     if ($data = ob_get_contents()) {
  9.       $cache = TRUE;
  10.       if (function_exists('gzencode')) {
  11.         // We do not store the data in case the zlib mode is deflate.
  12.         // This should be rarely happening.
  13.         if (zlib_get_coding_type() == 'deflate') {
  14.           $cache = FALSE;
  15.         }
  16.         else if (zlib_get_coding_type() == FALSE) {
  17.           $data = gzencode($data, 9, FORCE_GZIP);
  18.         }
  19.         // The remaining case is 'gzip' which means the data is
  20.         // already compressed and nothing left to do but to store it.
  21.       }
  22.       ob_end_flush();
  23.       if ($cache && $data) {
  24.         // { seconde modification
  25.         cache_set($base_root . request_uri()."___".$custom_theme, 'cache_page', $data, CACHE_TEMPORARY, drupal_get_headers());
  26.         // }
  27.       }
  28.     }
  29.   }
  30. }

Et voilà, c'est terminé. Il ne reste plus qu'à vider le "vieux" cache en passant par votre base de donnée et en exécutant un delete from cache_page. Maintenant, vous pouvez ré-activer le cache (en mode normal, pas agressif !!) et tester. En base de donnée, un select cid from page_cache vous confirme que les nouvelles clefs sont prise en compte.

Conclusion

Outre une meilleure compréhension du système de cache de Drupal, nous avons maintenant un module de changement dynamique de thème qui fonctionne parfaitement avec le cache, sans perte de performances. Le seul impacte est donc un doublement du volume du cache en base, ce qui n'est pas un problème en soit. Alors certes cela s'est fait au prix d'une modification mineure du Coeur mais le résultat est là. Maintenant il est toujours possible de rendre cette modification plus générique en introduisant un hook _get_cache_page_key qui prendrait en paramètre la clef d'origine et qui renverrait la clef modifiée. Cela réduirait considérablement l'impacte sur le coeur de Drupal. Pour une prochaine fois peut-être Wink

Commentaires

Poster un nouveau commentaire

Le contenu de ce champ est gardé secret et ne sera pas montré publiquement.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • To highlight piece of code, just surround them with <code type="language"> Your code &tl;/code>>. Language can be java,c++,bash,etc... Everything Geshi support.
  • Les lignes et les paragraphes vont à la ligne automatiquement.
  • Textual smileys will be replaced with graphical ones.
  • Les adresses de pages web et de messagerie électronique sont transformées en liens automatiquement.

Plus d'informations sur les options de formatage