RSS
 

Simple PHP lockfile/pidfile implementation

30 Oct

We have a some PHP files that run as cron jobs. Some of them run frequently, but occasionally take longer to run than the interval we have them set to. To prevent pile-up, I created a simple lockfile mechanism. I take advantage of posix kill 0 to check if the script is still running, so I think this is only guaranteed to work on linux, definitely does not work on Windows last I checked.

All you need are two functions:

function lockfile_open()
{
    $pidfile = '/tmp/' . basename($_SERVER['PHP_SELF']) . '.pid';
    if (file_exists($pidfile)) {
        $pid = file_get_contents($pidfile);
        $running = posix_kill($pid, 0);
        if ($running) {
            return false;
        } else {
            unlink($pidfile);
        }
    }
    $handle = fopen($pidfile, 'x');
    if (!$handle) {
        return false;
    }
    $pid = getmypid();
    fwrite($handle, $pid);
    fclose($handle);
    register_shutdown_function('lockfile_close');
    return true;
}

function lockfile_close()
{
    $pidfile = '/tmp/' . basename($_SERVER['PHP_SELF']) . '.pid';
    if (file_exists($pidfile)) {
        unlink($pidfile);
    }
}

To use it, simply call lockfile_open(); at the beginning of cron execution and check the return, if it returns false then you have another copy of your script already running and you need to handle appropriately (in our case we log an error and die).

As with my previous post, I’ll break this down line by line to explain what is happening.

$pidfile = '/tmp/' . basename($_SERVER['PHP_SELF']) . '.pid';

this sets the path for the lockfile/pidfile. I am storing them in /tmp and basing the name entirely on the name of the script that is executing. Depending on your system you may need to store them somewhere else and come up with a different naming scheme.

if (file_exists($pidfile)) {
    $pid = file_get_contents($pidfile);


we check to see if our pidfile already exists. If no other copy of the script is running, it should not exist because the shutdown handler deletes the file. If it does exist then we need to read the contents of the file to get the pid, so we can check to see if it’s really still running, or if it’s stale.

$running = posix_kill($pid, 0);

doing a posix kill with signal 0 does not actually send a signal to the process, but still runs error checking, which includes testing to see if the pid is running.

    if ($running) {
        return false;
    } else {
        unlink($pidfile);
    }
}

if posix_kill returned something truthy then the process is in fact still running, so we return false to indicate that. If the pidfile is actually stale, we delete it and then continue as if the file never existed.

$handle = fopen($pidfile, 'x');

We create the pidfile with mode x, this will fail if the pidfile already exists, which means this handles the possible race condition of two scripts executing at the same time, where both check above for the pidfile and both see it as not existing. mode x guarantees that one of those scripts will succeed in creating the file and the other will fail.

if (!$handle) {
    return false;
}

If we failed to create the pidfile then we assume it’s due to the aforementioned race condition, so return false. NOTE: this assumes permissions are set up correctly for wherever you are saving your pidfile, if you set up a path that your crons can’t write to, this will fail to create the pidfile and assume it was due to the race condition even though there is probably not another instance of your script running.

$pid = getmypid();
fwrite($handle, $pid);
fclose($handle);

Here we get our current pid and write to to the pidfile, then close the file.

    register_shutdown_function('lockfile_close');
    return true;
}

If we made it this far then we are the first/only instance of the script currently running. We register our lockfile_close() function to clean up our pidfile when the script stops executing, and we return true to indicate that it’s safe to proceed.

function lockfile_close()
{
    $pidfile = '/tmp/' . basename($_SERVER['PHP_SELF']) . '.pid';
    if (file_exists($pidfile)) {
        unlink($pidfile);
    }
}

lockfile_close gets called automatically by the shutdown handler when the script stops executing. We check to make sure the pidfile still exists and then delete it, so we aren’t leaving stale pidfiles around.

 
 

Leave a Reply