dimanche 9 novembre 2008

Contrôler le temps d'execution maximum d'un traitement PHP

Il est parfois nécessaire d'exécuter un traitement PHP avec une contrainte de temps maximum à ne pas dépasser. Dans cette article je vais vous proposer une solution à ce problème.

Voici un petit exemple impossible que nous allons tenter d'implémenter : "Faites ce que vous voulez dans une boucle infinie. Si au bout de 5 secondes vous n'avez pas terminé, stoppez le traitement".

Voici une classe qui va s'occuper de ce travail :

class   MissionImpossible
{
  public function start()
  {
    while (42)
      {
        echo "processing nothing...\n";
        sleep(1);
      }
  }

  public function abort()
  {
    throw new Exception("Time limit ! job aborted");
  }
}

Maintenant, passons aux choses intéressantes : comment s'y prendre pour appeler la fonction abort() au bout de 5 secondes, alors que la fonction start() est en train de s'executer ?

Nous allons utiliser le mécanisme de signaux POSIX pour interrompre le processus et l'informer que le temps qui lui était alloué arrive à terme. Le signal SIGALRM est parfaitement adapté à ce cas d'utilisation.

Deux fonctions vont nous être utiles en PHP :

  • pcntl_signal qui permet de definir quelle fonction va etre appelée lorsqu'un signal particulier est reçu.
  • pcntl_alarm qui permet de mettre en place un compte a rebours, qui lorsqu'il est terminé envoie un signal SIGALRM au processus courant.

Voici donc comment résoudre notre problème :
declare(ticks = 1);
require('MissionImpossible.php');

$job = new MissionImpossible();
pcntl_signal(SIGALRM, array($job, "abort"), true);
pcntl_alarm(5);
try {
  $job->start();
}
catch (Exception $e)
{
  echo "Exception caught. Stopping execution\n";
}

Quelques commentaires :

  • Lignes 1 et 2 : l'appel à la fonction declare est indispensable pour que le mécanisme de signaux puisse fonctionner en PHP. Il doit être fait impérativement avant la déclaration de la classe MissionImpossible.
  • Ligne 5 : mise en place du gestionnaire de signaux : la fonction abort() de l'instance $job de la classe MissionImpossible sera appelée dès qu'un signal SIGALRM sera reçu
  • Ligne 6 : mise en place du compte à rebours. Dans 5 secondes, un signal SIGALRM sera envoyé au processus courant
  • Dans la fonction abort(), une exception est lancée. Sans elle, le traitement de la fonction start() aurait continué dès la fin de la fonction abort().

Voilà, c'est tout simple. Mais sachez que les fonctions de l'extension pcntl ont quelques inconvénients : (cf doc officielle)

  • "Cette extension ne doit pas être activée pour une utilisation en serveur web, car les résultats pourraient être inattendus"
  • "Cette extension n'est pas disponible sur les plates-formes Windows."
Si cela vous pose problème, regardez du coté de register_tick_function (qui a lui aussi quelques inconvenients : " [...] ne doit pas être utilisé avec les modules de serveur web threadé. Les ticks ne fonctionnent pas en mode ZTS et peuvent interrompre votre serveur web.")

Enfin, cet exemple était vraiment simpliste mais sachez que c'est applicable pour tout traitement lourd et "réversible" (par exemple une grosse transaction MySQL).