Sessions PHP et rêquetes AJAX asynchrones

Suite à la chronique d'un audit technique de mageekguy (épisodes 1, 2, 3, 4 et 5), je me suis intéressé au problème soulevé et ai décidé de l'illustrer avec un peu de code. Pourquoi ? Simplement parce que je suis quelqu'un qui comprend bien mieux avec un exemple concret qu'une simple explication, même si cette dernière est bonne. Je me dis donc que faire ici un petit résumé pourrait aider certains à comprendre, mais aussi leur épargner trop de lecture.

Session PHP et concurrence

Comme vous le savez, en PHP il est possible d'utiliser des sessions. Maintenant, sachant qu'une requête HTTP (effectuée en AJAX ou non) peut prendre un certain temps, que se passe-t-il si deux requêtes provenant d'une même session s'exécutent simultanément ? Si les deux requêtes éditaient simultanément les variables de session, nous aurions une grande chance d'avoir des données corrompues. Heureusement, PHP gère le cas et pose systématiquement un verrou sur les sessions afin d'empêcher plusieurs utilisations simultanées d'une même session. Par défaut ce verrou est libéré à la fin du script.

Le problème

Si le verrou est utile lorsque l'on utilise effectivement les sessions, dans le cas où l'on utilise pas (ou plus) les sessions, il ne sert à rien et pose un gros problème : il est toujours impossible d'exécuter plusieurs requêtes de manière asynchrone. Lorsque la part de code n'utilisant pas les sessions met du temps à s'exécuter, ça peut poser de gros problèmes de performances (cf intro). La solution est alors toute simple : utiliser session_write_close() afin de terminer la session courante et libérer le verrou.

Illustration

Ce code est assez simple pour ne nécessiter qu'une courte explication : la page en html fait 3 appels asynchrones à sleep.php et, juste pour la forme, affiche le retour dans un paragraphe. sleep.php, quant à lui, ne fait que dormir pendant un temps donné en paramètre. Il est possible, via un second paramètre, de lui dire de terminer la session ou non avant de dormir.

<!doctype html>
<html>
  <head>
    <title>PHP session lock test</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
  </head>
  <body>
    <p id="target"></p>

    <script type="text/javascript">
$(document).ready(function() {
      var data = {
          close_session: 0
      },
      cb = function(data) {
          $('#target').html($('#target').html() + data + '<br>');
      };

      data.sleep = 3;
      $.get('sleep.php', data, cb);
      data.sleep = 1;
      $.get('sleep.php', data, cb);
      data.sleep = 2;
      $.get('sleep.php', data, cb);
});
    </script>
  </body>
</html>
<?php

session_start();

if (isset($_GET['close_session']) && isset($_GET['sleep']))
  {
    $_GET['close_session'] = (bool)$_GET['close_session'];
    if ($_GET['close_session'] === true)
      {
        echo 'closing session', PHP_EOL;
        session_write_close();
      }
    sleep($_GET['sleep']);
  }

echo 'sleep(', $_GET['sleep'], ')', PHP_EOL;

Sans fermer la session

Et bien bam, c'est le drame ! Comme nous l'avions prédit, à cause du verrou de session les appels ne peuvent se faire que un par un. Notez que, bien entendu, l'ordre d'appel ne préjuge pas de l'ordre d'exécution. Le temps d'exécution du dernier appel est donc le total du temps d'exécution de tous les appels, soit ici environ 6 secondes (3 + 1 + 2 pour ceux qui ne suivraient pas).

with lock

En fermant la session

Passons close_session à 1 afin que PHP ferme la session avant de dormir. Le changement est radical : maintenant le verrou libéré, les trois requêtes s'exécutent bien simultanément. Le temps total n'est plus la somme des trois, mais la valeur de l'appel le plus long, soit environ 3 secondes.

without lock