$theTitle=wp_title(" - ", false); if($theTitle != "") { ?> } else { ?> } ?>
by Andrew Johnstone
In: General
2 Jan 2010I just read “How to use locks in PHP cron jobs to avoid cron overlaps” and I thought I would elaborate on this and provide some more examples. In order for a lock to work correctly it must handle, Atomicity / Race Conditions, and Signaling.
I use the following bash script to create locks for crontabs and ensure single execution of scripts.
“The clever bit is to get a lock file test and creation (if needed) to be atomic, that is done without interruption. The set -C stops a redirection from over writing a file. The : > touches a file. In combination, the effect is, when the lock file exists, the redirection fails and exits with an error. If it does not exist, the redirection creates the lock file and exits without an error.The final part is to make sure that the lock file is cleaned up. To makes sure it is removed even if the script is terminated with a ctrl-c, a trap is used. Simply, when the script exits, the trap is run and the lock file is deleted.”, The Lab Book Pages
In addition it also checks the process list and tests whether the pid within the lock file is active.
#!/bin/bash LOCK_FILE=/tmp/my.lock CRON_CMD="php /var/www/..../fork.php -t17" function check_lock { (set -C; : > $LOCK_FILE) 2> /dev/null if [ $? != "0" ]; then RUNNING_PID=$(cat $LOCK_FILE 2> /dev/null || echo "0"); if [ "$RUNNING_PID" -gt 0 ]; then if [ `ps -p $RUNNING_PID -o comm= | wc -l` -eq 0 ]; then echo "`date +'%Y-%m-%d %H:%M:%S'` WARN [Cron wrapper] Lock File exists but no process running $RUNNING_PID, continuing"; else echo "`date +'%Y-%m-%d %H:%M:%S'` INFO [Cron wrapper] Lock File exists and process running $RUNNING_PID - exiting"; exit 1; fi else echo "`date +'%Y-%m-%d %H:%M:%S'` CRIT [Cron wrapper] Lock File exists with no PID, wtf?"; exit 1; fi fi trap "rm $LOCK_FILE;" EXIT } check_lock; echo "`date +'%Y-%m-%d %H:%M:%S'` INFO [Cron wrapper] Starting process"; $CRON_CMD & CURRENT_PID=$!; echo "$CURRENT_PID" > $LOCK_FILE; trap "rm -f $LOCK_FILE 2> /dev/null ; kill -9 $CURRENT_PID 2> /dev/null;" EXIT; echo "`date +'%Y-%m-%d %H:%M:%S'` INFO [Cron wrapper] Started ($CURRENT_PID)"; wait; # remove the trap kill so it won't try to kill process which took place of the php one in mean time (paranoid) trap "rm -f $LOCK_FILE 2> /dev/null" EXIT; rm -f $LOCK_FILE 2> /dev/null; echo "`date +'%Y-%m-%d %H:%M:%S'` INFO [Cron wrapper] Finished process";
With the implementation described in the post at abhinavsingh.com, it will fail if you put it as a background process as an example see below.
andrew@andrew-home:~/tmp.lock$ php x.php ==16169== Lock acquired, processing the job... ^C andrew@andrew-home:~/tmp.lock$ php x.php ==16169== Previous job died abruptly... ==16170== Lock acquired, processing the job... ^C andrew@andrew-home:~/tmp.lock$ php x.php ==16170== Previous job died abruptly... ==16187== Lock acquired, processing the job... ^Z [1]+ Stopped php x.php andrew@andrew-home:~/tmp.lock$ ps aux | grep php andrew 16187 0.5 0.5 50148 10912 pts/2 T 09:53 0:00 php x.php andrew 16192 0.0 0.0 3108 764 pts/2 R+ 09:53 0:00 grep --color=auto php andrew@andrew-home:~/tmp.lock$ php x.php ==16187== Already in progress...
You can use pcntl_signal to trap interruptions to the application and handle cleanup of the process. Here is a slightly modified implementation to handle cleanup. Just to highlight the register_shutdown_function will not help to cleanup on any signal/interruption.
<?php class lockHelper { protected static $_pid; protected static $_lockDir = '/tmp/'; protected static $_signals = array( // SIGKILL, SIGINT, SIGPIPE, SIGTSTP, SIGTERM, SIGHUP, SIGQUIT, ); protected static $_signalHandlerSet = FALSE; const LOCK_SUFFIX = '.lock'; protected static function isRunning() { $pids = explode(PHP_EOL, `ps -e | awk '{print $1}'`); return in_array(self::$_pid, $pids); } public static function lock() { self::setHandler(); $lock_file = self::$_lockDir . $_SERVER['argv'][0] . self::LOCK_SUFFIX; if(file_exists($lock_file)) { 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() { $lock_file = self::$_lockDir . $_SERVER['argv'][0] . self::LOCK_SUFFIX; if(file_exists($lock_file)) { error_log("==".self::$_pid."== Releasing lock..."); unlink($lock_file); } return TRUE; } protected static function setHandler() { if (!self::$_signalHandlerSet) { declare(ticks = 1); foreach(self::$_signals AS $signal) { if (!pcntl_signal($signal, array('lockHelper',"signal"))) { error_log("==".self::$_pid."== Failed assigning signal - '{$signal}'"); } } } return TRUE; } protected static function signal($signo) { if (in_array($signo, self::$_signals)) { if(!self::isrunning()) { self::unlock(); } } return FALSE; } }
As an example:
andrew@andrew-home:~/tmp.lock$ php t.php ==16268== Lock acquired, processing the job... ^Z==16268== Releasing lock...
Whilst the implementation above simply uses files, it could be implemented with shared memory (SHM/APC), distributed caching (memcached), or a database. If over a network, factors such as packet loss, latency etc can cause race conditions and should be taken into account. Depending on the application it maybe better to implement as a daemon. If your looking to distribute tasks amongst servers, take a look at Gearman
I have been a developer for roughly 10 years and have worked with an extensive range of technologies. Whilst working for relatively small companies, I have worked with all aspects of the development life cycle, which has given me a broad and in-depth experience.
2 Responses to Lock Files in PHP & Bash
magic_touch
July 25th, 2011 at 11:03 pm
Hello, what does mean the -t17 in CRON_CMD=”php /var/www/…./fork.php -t17″
andrew.johnstone
July 27th, 2011 at 5:25 am
It is simply an argument to a php script I wrote, which forked itself and acted as a time interval for a certain check. There was no significance to the post at all, just something which I had left in from a cron on one of our production servers.