[spip-dev] gestion du cache - environnement concurent

bonjour

je suis encore assez etonné de voir comment fonctionne les fichiers caches
de SPIP. Je m'y suis intéressé un peu plus suite au message d'un utilisateur
qui m'a envoyé un message personnel me faisant part de ses difficultés similaires.

Du coup je me suis amusé avec les fonctions de lock. Il y a bien sur flock,
mais aussi dans SPIP un mécanisme de lock par requette en BD. Je suppose que
ce mecanisme fonctionne ; je n'ai pas pris la peine de verifier.

Voici la fonction qui permet de générer les fichiers en cache. Il semble
que tout soit OK ; voila pour le coté écriture des fichiers.

function ecrire_fichier_cache($fichier, $contenu) {
         global $flag_flock;

         $fichier_tmp = $fichier.'_tmp';
         $fichier_new = $fichier.'.NEW';

         // Essayer de poser un verrou pour proteger l'ecriture du fichier
         if (!spip_get_lock($fichier_tmp, 1)) return $fichier_new;
         $ok = true;
         $f = fopen($fichier_tmp, "wb");
         if (!$f) $ok = false;
         else {
                 $r = fwrite($f, $contenu);
                 if ($r != strlen($contenu)) $ok = false;
                 if (!fclose($f)) $ok = false;
         }

         // En cas d'erreur d'ecriture, renvoyer le fichier existant
         if (!$ok) {
                 spip_release_lock($fichier_tmp);
                 clearstatcache();
                 return @file_exists($fichier_new) ? $fichier_new : $fichier;
         }

         // Finaliser
         @unlink($fichier_new);
         rename($fichier_tmp, $fichier_new);
         @unlink($fichier);
         spip_release_lock($fichier_tmp);

         if ($GLOBALS['flag_apc']) {
                 apc_rm($fichier_new);
                 apc_rm($fichier);
         }
         return $fichier_new;
}

passons maintenant a la lecture des fichiers ; je vois :

# inc-public.php3
<?php

if (!defined("_INC_PUBLIC")) {
         define("_INC_PUBLIC", "1");
         include("inc-public-global.php3");
}
else {
         $cache_inclus = inclure_fichier($fond, $delais, $contexte_inclus);
         if (!$delais) $cache_supprimes[] = $cache_inclus; // message pour suppression

         include($cache_inclus);
}

?>

la fonction inclure_fichier prend en charge la generation des fichiers de cache.
on pourrait au moins tester qu'apres cette generation le fichier existe bien :

   if(file_exists($cache_inclus))
     include($cache_inclus);

il se trouve que je tombe dans le cas ou le fichier n'existe pas ; en effet, il s'agit
d'un article et sur la meme page un forum associé. j'ai le sentiment que le forum dont
la valeur delais=0 empeche le cache de s'activer est tout de suite effacé. Du coup
y'a plus de fichier cache quand je fais l'include.

Seulement, en environnement concurentiel, entre un if() et la suite des instructions
il peut se passer plein de choses donc :

   if(file_exists($cache_inclus))
     include($cache_inclus);

est une suite d'instruction qui marche quand on a de la chance, mais si de nombreux
prossessus apache s'executent en paralelle, il est fort probable qu'un jour ou
l'autre le fil d'execution soit interrompu. Il serait souhaitable de mettre un verrou
tant pour l'écriture que la lecture des fichiers en cache :

allons-y pour quelque chose comme ca (non testé) :

   $lock = "ce que vous voulez"; # un nom de fichier ou un nom de section
   // debut de section avec un verrou
   if (!spip_get_lock($lock, 1)) {
  # traiter l'erreur ...
   }

   if(file_exists($cache_inclus))
     include($cache_inclus);

   // fin de section verrouillée.
   spip_release_lock($lock);

voila quelques idées a creuser.

C'est fait ici:

http://www.spip-contrib.net/ecrire/articles.php3?id_article=510

esj

Hello,

   // debut de section avec un verrou
   if (!spip_get_lock($lock, 1)) {
  # traiter l'erreur ...
   }

   if(file_exists($cache_inclus))
     include($cache_inclus);

   // fin de section verrouillée.
   spip_release_lock($lock);

Effectivement, ça pourrait être une solution... sauf que cela force une
connexion MySQL à chaque accès en lecture au cache, ce que l'on évite à
tout prix. On pourrait utiliser flock() mais il y a des problèmes de
portabilité (cf. doc PHP).

En pratique, je ne crois pas qu'il y ait de pbs d'accès concurrents sur
ce bout de code particulier, y compris sur de gros sites très visités
(monde diplo, huma). "Your Mileage May Vary", comme disent les
anglais...

a+

Antoine.

Marc Quinton wrote:

function ecrire_fichier_cache($fichier, $contenu) {

        // Essayer de poser un verrou pour proteger l'ecriture du fichier
        if (!spip_get_lock($fichier_tmp, 1)) return $fichier_new;

           ...

        return $fichier_new;
}

# inc-public.php3
<?php

if (!defined("_INC_PUBLIC")) {
        define("_INC_PUBLIC", "1");
        include("inc-public-global.php3");
}
else {
        $cache_inclus = inclure_fichier($fond, $delais, $contexte_inclus);
        if (!$delais) $cache_supprimes = $cache_inclus; // message pour suppression

        include($cache_inclus);
}

je suis toujours dans le cadre de cette erreur ; voici ce que je viens de decouvrir :

j'ai modifié le code ainsi de facon a voir des messages d'erreur et des traces dans
le fichier de log spip :

# inc-public.php3
<?php

if (!defined("_INC_PUBLIC")) {
         define("_INC_PUBLIC", "1");
         include("inc-public-global.php3");
}
else {
         $cache_inclus = inclure_fichier($fond, $delais, $contexte_inclus);
         if (!$delais) $cache_supprimes = $cache_inclus; // message pour suppression

         # MQ
         if(file_exists($cache_inclus)) {
            include($cache_inclus);
         } else {
           echo "<i> erreur : [ fichier cache : $fond / $cache_inclus non trouve ]<br>\n";
           spip_log("erreur acces ficier cache : $fond / $cache_inclus non trouve");
         }
}

?>

# inc-cache.php3
function ecrire_fichier_cache($fichier, $contenu) {
         global $flag_flock;

         $fichier_tmp = $fichier.'_tmp';
         $fichier_new = $fichier.'.NEW';

         // Essayer de poser un verrou pour proteger l'ecriture du fichier
         if (!spip_get_lock($fichier_tmp, 1)) {
             global $fond;
             # MQ trace dans le fichier de log
             spip_log("inc_cache : MQ not lock : $fond");
             return $fichier_new;
         }
         $ok = true;
         $f = fopen($fichier_tmp, "wb");
         if (!$f) $ok = false;
         else {
                 $r = fwrite($f, $contenu);
                 if ($r != strlen($contenu)) $ok = false;
                 if (!fclose($f)) $ok = false;
         }

         // En cas d'erreur d'ecriture, renvoyer le fichier existant
         if (!$ok) {
                 spip_release_lock($fichier_tmp);
                 clearstatcache();
                 return @file_exists($fichier_new) ? $fichier_new : $fichier;
         }

         // Finaliser
         @unlink($fichier_new);
         rename($fichier_tmp, $fichier_new);
         @unlink($fichier);
         spip_release_lock($fichier_tmp);

         if ($GLOBALS['flag_apc']) {
                 apc_rm($fichier_new);
                 apc_rm($fichier);
         }
         return $fichier_new;
}

voici ce qui apparait sur la page HTML ; une bonne partie des inclure SPIP sont OK,
sauf pour 2 fichiers dont l'acces au cache a échoué :

  # sur navigateur :
  erreur : [ fichier cache : _box_forum / CACHE/6/_box_forum-270.365eb6.NEW non trouve ]
  erreur : [ fichier cache : _bottom / CACHE/4/_bottom.2e6ca1.NEW non trouve ]

contenu du fichier spip.log:

Jun 03 14:52:39 143.196.38.143 (pid 21479) inclus (20 ms): CACHE/6/_box_forum-270.365eb6 (1 ko, delai: 0 s)
Jun 03 14:52:39 143.196.38.143 (pid 21479) inc_cache : MQ not lock : _box_forum
Jun 03 14:52:39 143.196.38.143 (pid 21479) erreur acces ficier cache : _box_forum / CACHE/6/_box_forum-270.365eb6.NEW non trouve
Jun 03 14:52:39 143.196.38.143 (pid 21479) inclus (1 ms): CACHE/4/_bottom.2e6ca1 (1 ko, delai: 0 s)
Jun 03 14:52:39 143.196.38.143 (pid 21479) inc_cache : MQ not lock : _bottom
Jun 03 14:52:39 143.196.38.143 (pid 21479) erreur acces ficier cache : _bottom / CACHE/4/_bottom.2e6ca1.NEW non trouve

on peut donc faire une association directe entre un retour = false de spip_get_lock()
et mes erreurs d'inclusion. Que peut-on faire ?

element important que je viens de decouvrir, la base de données via ecrire/inc_cron.php
retourne systématiquement cette erreur "pas de connexion DB" quand je suis dans
le contexte de mon erreur de fichier cache. Et c'est somme toute assez logique
puisque le verrou SPIP est géré par mysql.

Jun 03 15:06:52 143.196.38.143 (pid 19474) inc_cache : MQ not lock : _box_forum
Jun 03 15:06:52 143.196.38.143 (pid 19474) erreur acces ficier cache : _box_forum / CACHE/6/_box_forum-270.365eb6.NEW
non trouve
Jun 03 15:06:52 143.196.38.143 (pid 19474) inclus (1 ms): CACHE/4/_bottom.2e6ca1 (1 ko, delai: 0 s)
Jun 03 15:06:52 143.196.38.143 (pid 19474) inc_cache : MQ not lock : _bottom
Jun 03 15:06:52 143.196.38.143 (pid 19474) erreur acces ficier cache : _bottom / CACHE/4/_bottom.2e6ca1.NEW non trouve
Jun 03 15:06:52 143.196.38.143 (pid 19474) pas de connexion DB pour taches de fond (cron)
Jun 03 15:09:12 143.196.38.143 (pid 21473) inclus (1 ms): CACHE/4/_bottom.2e6ca1 (1 ko, delai: 3600 s)
Jun 03 15:09:12 143.196.38.143 (pid 21473) inc_cache : MQ not lock : _bottom
Jun 03 15:09:12 143.196.38.143 (pid 21473) erreur acces ficier cache : _bottom / CACHE/4/_bottom.2e6ca1.NEW non trouve
Jun 03 15:09:14 143.196.38.143 (pid 21425) mes_fonctions : article