Alheim

Architecture logicielle- Développement web – Technos et tout un tas de truc…

Comment utiliser les locks avec des tâches CRON en PHP ?

with 9 comments

On utilise souvent les jobs CRON pour effectuer des traitements lourds en tâches de fond : calcul de statistiques, conversion d’image, envoi de mail etc…

L’inconvénient de CRON par rapport à d’autres systèmes de jobs intégrés dans les plateformes J2EE ou .NET, c’est qu’il ne gère pas le “chevauchement” des tâches.

Je m’explique : imaginons une tâche lancée toutes les heures et qui prend 30 min pour traiter un 1Go de données. Si la taille des données augmente, ce que nous souhaitons tous pour nos applications, le traitement s’effectuera peut être en plus d’une heure. Que se passe t-il alors si la tâche est relancée alors que l’occurrence précédente n’est pas terminée ? Au mieux, les données peuvent être corrompues, au pire, la plateforme peut tomber sous le poids des traitements.

Voici donc une façon de gérer les “lock” en PHP, afin de garantir qu’une occurrence de tâche CRON s’exécute si et seulement si la l’occurrence précédente est terminée.

Cron lock helper class

Tout d’abord, une classe helper qui assure la gestion du lock

<?php

	define('LOCK_DIR', '/tmp/cronHelper/');
	define('LOCK_SUFFIX', '.lock');

	class cronHelper {

		private static $pid;

		function __construct() {}

		function __clone() {}

		private static function isrunning() {
			$pids = explode(PHP_EOL, `ps -e | awk '{print $1}'`);
			if(in_array(self::$pid, $pids))
				return TRUE;
			return FALSE;
		}

		public static function lock() {
			global $argv;

			$lock_file = LOCK_DIR.$argv[0].LOCK_SUFFIX;

			if(file_exists($lock_file)) {
				//return FALSE;

				// Is running?
				self::$pid = file_get_contents($lock_file);
				if(self::isrunning()) {
					error_log("==".self::$pid."== Already in progress...");
					return FALSE;
				}
				else {
					error_log("==".self::$pid."== Previous job died abruptly...");
				}
			}

			self::$pid = getmypid();
			file_put_contents($lock_file, self::$pid);
			error_log("==".self::$pid."== Lock acquired, processing the job...");
			return self::$pid;
		}

		public static function unlock() {
			global $argv;

			$lock_file = LOCK_DIR.$argv[0].LOCK_SUFFIX;

			if(file_exists($lock_file))
				unlink($lock_file);

			error_log("==".self::$pid."== Releasing lock...");
			return TRUE;
		}

	}

?>

La fonction lock permet de générer un fichier contenant le PID du processus PHP en cours. La fonction isRunning vérifie l’existence de ce fichier et / ou la présence du PID dans les processus en cours : `ps -e | awk ‘{print $1}’` Enfin, la fonction unlock efface le fichier de lock. Elle doit être appelée en fin d’exécution de la tâche, que celle ci se termine correctement ou non.

Intégration de la classe helper dans le code de la tâche

<?php
 require 'path_to_helper_class';
 if(($pid = cronHelper::lock()) !== FALSE) {
 /*
 * Le code de votre tâche se place ici
 */
 sleep(10); //Ca c'est pour le test... Intéressant non ?

 cronHelper::unlock();
 }
 ?>

Dans un premier temps, le code essaye d’acquérir le lock. S’il y parvient, il déroule le reste du code puis libère le lock.

Test

1- Exécuter le job depuis une ligne de commande :

  1. alheim$ php job.php
  2. ==40818== Lock acquired, processing the job…
  3. ==40818== Releasing lock…

Tout s’est bien passé. Le lock a été acquis pour le processus 40818 puis libéré après 10 secondes de “dodo”, ie sleep(10) 2- Testons maintenant le cas ou le job se vautre lamentablement : pour cela, lancez le puis faites un bon CTRL-C pour l’interrompre.

  1. alheim$ php job.php
  2. ==40830== Lock acquired, processing the job…

Le job s’arrête, donc le PID n’existe plus dans la liste des processus actifs, mais le fichier de lock est toujours présent sur le disque. Si on le relance :

  1. alheim$ php job.php
  2. ==40830== Previous job died abruptly…
  3. ==40835== Lock acquired, processing the job…
  4. ==40835== Releasing lock…

3- Dernier test, la concurrence : lancez le job depuis 2 terminaux :

  1. alheim-2$ php job.php
  2. ==40856== Already in progress…

Voila ce qui apparaitra dans le deuxième terminal. En rédigeant cette note, j’ai vu beaucoup d’applications possible à cette technique. Il y a aussi sûrement des améliorations à apporter à ce code. Qu’en pensez vous ?
Merci à abhinavsingh.com pour l’article orginal.

Written by admin

January 3rd, 2010 at 2:20 pm

Posted in Code

Tagged with , , , ,

9 Responses to 'Comment utiliser les locks avec des tâches CRON en PHP ?'

Subscribe to comments with RSS or TrackBack to 'Comment utiliser les locks avec des tâches CRON en PHP ?'.

  1. Désolé, mais je pense que c’est pas nécessairement la bonne méthode pour gérer un lock. Pour qu’un lock soit efficace il faut s’assurer que le test sur l’existence du verrou soit atomique. Ici ce n’est pas le cas.

    Il est facile de gérer un lock via un fichier en php avec la fonction flock()

    M.

    kolter

    4 Jan 10 at 12:18 am

  2. Article très interessant.

    Sinon je viens de découvrir ton blog. Le thème est sympa. Par contre je te recommande d’utiliser un plugin pour le code. J’utilise syntax highlighter, Il est assez flexible et sympa.

    Bonne continuation

    Greg

    4 Jan 10 at 12:35 am

  3. @kolter : c’est une méthode parmi tant d’autres en effet. L’objet de ce billet était plus d’insister sur la nécessité d’utiliser ce genre de mécanisme avec des jobs CRON. Peux tu développer ce que tu dis sur le “test atomique” ?

    @greg : merci. Je l’ai mis en place hier. Par contre je n’ai pas repris toutes les notes, pas encore.

    admin

    4 Jan 10 at 8:49 am

  4. Entre la ligne 26 (Vérifie si le fichier existe) et la ligne 41 (enregistre le PID du process) de ton exemple, plusieurs process peuvent se chevaucher et donc vérifier en même temps que le fichier n’existe pas et créé en même temps le fichier derrière. (même si dans la pratique c’est peut probable, d’autant plus que le script concerne un cron exécuté toutes les X heures)

    Le mieux est d’ouvrir le fichier (r+), demander un lock (avec LOCK_EX), puis de lire le contenu, faire les tests, et si besoin écrire le nouveau PID dedans. A la fin, des tests et écritures, ne pas oublier de le déverrouiller (LOCK_UN).

    Jérémy

    4 Jan 10 at 9:57 am

  5. En effet, même si c’est peu probable, c’est bien de le préciser. Je mettrai à jour le code. Merci !

    admin

    4 Jan 10 at 10:53 am

  6. Article intéressant mais je trouve la mécanique un peu lourde.

    J’ai été confronté à un cas similaire avec des timings beaucoup plus court.

    La méthode que j’ai retenu est différente avec l’ouverture d’un fichier en mode ‘x’ ou ‘x+’.

    Je devais conserver le fichier qui servait de log, mais dans le cas d’un job cron, il suffirai de supprimer le fichier en fin d’exécution.

    Pierre

    5 Jan 10 at 10:50 am

  7. @Pierre : en effet la mécanique s’apparente à celle utilisée par des process tels que mysql ou autres programmes UNIX. Se baser uniquement sur l’ouverture d’un fichier peut être dangereux, surtout dans le cas où le job se crash sans libérer la ressource. tu as déjà testé ce genre de cas ?

    alheim

    5 Jan 10 at 11:19 am

  8. @alheim
    Dans mon cas d’utilisation, le crash du programme bloque les traitements similaire ultérieurs mais c’est le fonctionnement que l’on m’a demandé.
    Il faudrait je pense revoir un peu la mécanique pour l’appliquer à l’ensemble d’une cron.

    Pierre

    5 Jan 10 at 12:40 pm

  9. Salut, c’est exactement ce que je cherchais.. merci.

Leave a Reply