18

I have a cron script that executes a PHP script every 10 minutes. The script checks a queue and processes the data in the queue. Sometimes the queue has enough data to last over 10 minutes of processing, creating the potential of two scripts trying to access the same data. I want to be able to detect whether the script is already running to prevent launching multiple copies of the script. I thought about creating a database flag that says that a script is processing, but if the script were ever to crash it would leave it in the positive state. Is there an easy way to tell if the PHP script is already running from withing a PHP or shell script?

user77413
  • 30,205
  • 16
  • 46
  • 52

11 Answers11

47

You can just use a lock file. PHP's flock() function provides a simple wrapper for Unix's flock function, which provides advisory locks on files.

If you don't explicitly release them, the OS will automatically release these locks for you when the process holding them terminates, even if it terminates abnormally.

You can also follow the loose Unix convention of making your lock file a 'PID file' - that is, upon obtaining a lock on the file, have your script write its PID to it. Even if you never read this from within your script, it will be convenient for you if your script ever hangs or goes crazy and you want to find its PID in order to manually kill it.

Here's a copy/paste-ready implementation:

#!/usr/bin/php
<?php

$lock_file = fopen('path/to/yourlock.pid', 'c');
$got_lock = flock($lock_file, LOCK_EX | LOCK_NB, $wouldblock);
if ($lock_file === false || (!$got_lock && !$wouldblock)) {
    throw new Exception(
        "Unexpected error opening or locking lock file. Perhaps you " .
        "don't  have permission to write to the lock file or its " .
        "containing directory?"
    );
}
else if (!$got_lock && $wouldblock) {
    exit("Another instance is already running; terminating.\n");
}

// Lock acquired; let's write our PID to the lock file for the convenience
// of humans who may wish to terminate the script.
ftruncate($lock_file, 0);
fwrite($lock_file, getmypid() . "\n");

/*
    The main body of your script goes here.
*/
echo "Hello, world!";

// All done; we blank the PID file and explicitly release the lock 
// (although this should be unnecessary) before terminating.
ftruncate($lock_file, 0);
flock($lock_file, LOCK_UN);

Just set the path of your lock file to wherever you like and you're set.

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
  • 2
    This should be the best answer. It simply makes sense. [Symfony 2.6 LockHandler](http://symfony.com/blog/new-in-symfony-2-6-lockhandler) is also implementing locks by means of file lock. – datasn.io Oct 30 '15 at 11:41
  • Per the docs: On versions of PHP before 5.3.2, the lock is released also by fclose() (which is also called automatically when script finished). 5.3.2 The automatic unlocking when the file's resource handle is closed was removed. Unlocking now always has to be done manually. – Justin McAleer Jun 15 '16 at 20:14
  • 1
    @JustinMcAleer PHP might not automatically release the locks for you upon closing of a file handle, but the OS still will (at least on Unix OSes); that manual passage is basically incorrect. And even if not, any OS will hopefully release the lock once your process terminates; failure to do so would be pretty broken. – Mark Amery Dec 28 '16 at 19:50
  • @MarkAmery I know this is an old question but, how should I set the $wouldblock variable? – fixmycode Dec 26 '17 at 19:18
  • 1
    @fixmycode you don't have to set it. It's a reference parameter - defined as `&$wouldblock` rather than `$wouldblock` in [the function signature](http://php.net/manual/en/function.flock.php). You just pass in a name, and the *function* sets it to a value which you can check from your calling code. – Mark Amery Dec 26 '17 at 19:20
  • I would like to extend your solutiuon: sometimes script can be aborted (due to timeout or fatal error) - then pid file will not be released. So we should register shutdown function, this way: `register_shutdown_function(function($lock_file){ ftruncate($lock_file, 0); flock($lock_file, LOCK_UN); }, $lock_file); ` – Tomas Šivickas Jan 17 '20 at 10:28
  • @TomasŠivickas Interesting. That's a reasonable thing to do, but also a reasonable thing *not* to do - it's potentially *useful*, when crashing, to leave around a PID file with the crashed process's PID in it. It lets you infer that the crash took place even in the absence of any logging that indicates that, find the last process's PID to cross-reference with any logs you have that might mention it, and maybe even add something to the start of your script that checks for the presence of a PID in the file and alerts the user that previous run crashed (MySQL does this). – Mark Amery Jan 17 '20 at 11:15
9

If you are running Linux, this should work at the top of your script:

$running = exec("ps aux|grep ". basename(__FILE__) ."|grep -v grep|wc -l");
if($running > 1) {
   exit;
}
user3400219
  • 115
  • 1
  • 2
  • 2
    I like the simplicity of this solution. Is there any drawbacks? Also, in case anyone else wants to use it - if you use it in crontab, then check that the number is more than 2 instead of 1. – LauriK Jul 29 '16 at 07:33
  • @LauriK, as per your question. The first script may have stopped internally which isn't obvious from the second script seeing the PID still running. Using something else could help the second script know to kill the first and start over. – Xeoncross Feb 08 '17 at 18:04
  • 1
    If any other process is using that file, it will think it's running. For example, `vim ` will make this think the program is running. If there's any other running program that also contains your filename it will think its running, `/foo/bar.php` and `/biff/bar.php` will conflict. – Schwern May 06 '18 at 01:11
  • Besides @Schwern's more important criticism, one other minor flaw with this approach as written is that since you're concatenating the filename into a shell command without escaping it, the command may fail (or maybe even run some subprocess you don't want) if your script name contains special characters that are meaningful to your shell. To be more robust, escape `basename(__FILE__)` with [`escapeshellarg`](http://php.net/manual/en/function.escapeshellarg.php). – Mark Amery Jan 19 '19 at 14:24
  • Your solution is exaclty what I need as my PHP script is called by crontab using "php -f ..." so it works just fine to ensure the script is always running and avoid to coll it twice if previous istance was still running. Thanks! – Power Engineering May 28 '23 at 14:30
9

If you need it to be absolutely crash-proof, you should use semaphores, which are released automatically when php ends the specific request handling.

A simpler approach would be to create a DB record or a file at the beginning of the execution, and remove it at the end. You could always check the "age" of that record/file, and if it's older than say 3 times the normal script execution, suppose it crashed and remove it.

There's no "silver bullet", it just depends on your needs.

MartinodF
  • 8,157
  • 2
  • 32
  • 28
3

I know this is an old question but in case someone else is looking here I'll post some code. This is what I have done recently in a similar situation and it works well. Put put this code at the top of your file and if the same script is already running it will leave it be and end the new one.

I use it to keep a monitoring system running at all times. A cron job starts the script every 5 minutes but unless the other has stopped from some reason (usually if it has crashed, which is very rare!) the new one will just exit itself.

// The file to store our process file
define('PROCESS_FILE', 'process.pid');

// Check I am running from the command line
if (PHP_SAPI != 'cli') {
    log_message('Run me from the command line');
    exit;
}

// Check if I'm already running and kill myself off if I am
$pid_running = false;
if (file_exists(PROCESS_FILE)) {
    $data = file(PROCESS_FILE);
    foreach ($data as $pid) {
        $pid = (int)$pid;
        if ($pid > 0 && file_exists('/proc/' . $pid)) {
            $pid_running = $pid;
            break;
        }
    }
}
if ($pid_running && $pid_running != getmypid()) {
    if (file_exists(PROCESS_FILE)) {
        file_put_contents(PROCESS_FILE, $pid);
    }
    log_message('I am already running as pid ' . $pid . ' so stopping now');
    exit;
} else {
    // Make sure file has just me in it
    file_put_contents(PROCESS_FILE, getmypid());
    log_message('Written pid with id '.getmypid());
}

It will NOT work without modification on Windows, but should be fine in unix based systems.

Luke Cousins
  • 2,068
  • 1
  • 20
  • 38
  • -1 for the same reason as Brenton's answer above; because most Unixes recycle PIDs, the fact that there is a running process whose PID matches the one written to your PID file by a previous incarnation of your script does NOT mean that it is actually your script that is still running. If your script crashes, and then some unrelated long-running process spawns and takes the PID that your script was using before the crash, then when your script next runs it will incorrectly kill itself off; your script may well stay dead forever! Use `flock()` instead to protect yourself from this edge case. – Mark Amery Oct 30 '15 at 15:10
  • This works on windows too, you yust have to remove the '/proc/' - check. – Radon8472 Jan 12 '16 at 16:39
3

You can use new Symfony 2.6 LockHandler. Source

$lock = new LockHandler('update:contents');
if (!$lock->lock()) {
    echo 'The command is already running in another process.';
}
Nicklasos
  • 566
  • 3
  • 8
  • Could you post a link where I can download the only LockHandler class without the rest of Synfony ? – Radon8472 Jan 13 '16 at 07:32
  • 1
    Note: `LockHandler` is removed in Symfony 4, replaced by [the `Lock` component](https://symfony.com/doc/current/components/lock.html). – Mark Amery Jan 19 '19 at 14:15
3

A common way for *nix daemons (though not necessarily PHP scripts, but it will work) is to use a .pid file.

When the script starts check for the existence of a .pid file named for the script (generally stored in /var/run/). If it doesn't exist, create it setting its contents to the PID of the process running the script (using getmypid) then continue with normal execution. If it does exist read the PID from it and see if that process is still running, probably by running ps $pid. If it is running, exit. Otherwise, overwrite its contents with your PID (as above) and continue normal execution.

When execution finished, delete the file.

Brenton Alker
  • 8,947
  • 3
  • 36
  • 37
  • 2
    I guess this will probably work fine on a typical *nix OS as long as the time gap between runs of your daemon is modest, but beware that [PIDs get recycled](http://unix.stackexchange.com/questions/26677/will-process-ids-be-recycled-what-if-you-reach-the-maximal-id) and so you may not want to use them as part of a locking system. There is at least a theoretically possible failure mode here where your daemon crashes, some completely unrelated long-running process takes the same PID, and subsequent launches of your daemon all wrongly believe that it's already running, leaving it dead permanently. – Mark Amery Jul 20 '14 at 09:39
0

This worked for me. Set a database record with a lock flag and a time stamp. My script should complete well within 15min so added that as a last locked feild to check:

       $lockresult = mysql_query("
       SELECT *
       FROM queue_locks
       WHERE `lastlocked` > DATE_SUB(NOW() , INTERVAL 15 MINUTE)    
       AND `locked` = 'yes'
       AND `queid` = '1'
       LIMIT 1
        "); 
$LockedRowCount = mysql_num_rows($lockresult);

if($LockedRowCount>0){
    echo "this script is locked, try again later";
    exit;   
}else{
//Set the DB record to locked and carry on son
$result = mysql_query("
            UPDATE `queue_locks` SET `locked` = 'yes', `lastlocked` = CURRENT_TIMESTAMP WHERE `queid` = 1;
            ");

}

Then unlock it at the end of the script:

    $result = mysql_query("UPDATE `queue_locks` SET `locked` = 'no' WHERE `queid` = 1;");
Ralph Vugts
  • 425
  • 5
  • 14
  • Sad to see this got a downvote. Because in 2021, it's commonplace to have a whole cluster of webservers that don't share a filesystem or process list, and saving this sort of thing to the database might be the only real viable solution. Perhaps this answer was ahead of its time in 2014. – Eric Seastrand Sep 21 '21 at 04:29
0

I know this is an old question, but there's an approach which hasn't been mentioned before that I think is worth considering.

One of the problems with a lockfile or database flag solution, as already mentioned, is that if the script fails for some reason other than normal completion it won't release the lock. And therefore the next instance won't start until the lock is either manually cleared or cleared by a clean-up function.

If, though, you are certain that the script should only ever be running once, then it's relatively easy to check from within the script whether it is already running when you start it. Here's some code:

function checkrun() {
    exec("ps auxww",$ps);
    $r = 0;
    foreach ($ps as $p) {
        if (strpos($p,basename(__FILE__))) {
            $r++;
            if ($r > 1) {
                echo "too many instances, exiting\n";
                exit();
            }
        }
    }
}

Simply call this function at the start of the script, before you do anything else (such as open a database handler or process an import file), and if the same script is already running then it will appear twice in the process list - once for the previous instance, and once for this one. So, if it appears more than once, just exit.

A potential gotcha here: I'm assuming that you will never have two scripts with the same basename that may legitimately run simultaneously (eg, the same script running under two different users). If that is a possibility, then you'd need to extend the checking to something more sophisticated than a simple substring on the file's basename. But this works well enough if you have unique filenames for your scripts.

Mark Goodge
  • 577
  • 5
  • 7
  • -1 not for the approach you propose here but rather for the claim that Unix file locks won't be released if the process terminates abnormally. This is untrue. See http://stackoverflow.com/questions/12651068/release-of-flock-in-case-of-errors for a demonstration of this. – Mark Amery Nov 03 '15 at 01:47
0

Assuming this is a linux server and you have cronjobs available

///Check for running script and run if non-exist///

#! /bin/bash

check=$(ps -fea | grep -v grep | grep script.php | wc -l)
date=$(date +%Y-%m%d" "%H:%M:%S)
if [ "$check" -lt 1 ]; then
echo "["$date"] Starting script" >> /path/to/script/log/
/sbin/script  ///Call the script here - see below///
fi

script file

#/usr/bin/php /path/to/your/php/script.php
Tiago Martins Peres
  • 14,289
  • 18
  • 86
  • 145
James F
  • 169
  • 10
  • -1; this, like other approaches on this page that naively look for the filename in the output of `ps`, will wrongly think the script is running if you, say, happen to be looking at `script.php` in `less` at the time that your wrapper script runs, or if you happen to be running `someotherscript.php` (whose name *contains* `script.php`) at the same. It's probably good enough for most use cases, but personally I'd rather avoid laying these little traps for myself. – Mark Amery Jan 19 '19 at 14:34
0

Home / Check if a PHP script is already running

Check if a PHP script is already running If you have long running batch processes with PHP that are run by cron and you want to ensure there’s only ever one running copy of the script, you can use the functions getmypid() and posix_kill() to check to see if you already have a copy of the process running. This post has a PHP class for checking if the script is already running.

Each process running on a Linux/Unix computer has a pid, or process identifier. In PHP this can be retrieved using getmypid() which will return an integer number. This pid number can be saved to a file and each time the script is run a check made to see if the file exists. If it is the posix_kill() function can be used to see if a process is running with that pid number.

My PHP class for doing this is below. Please feel free to use this and modify to suit your individual requirements.

class pid {

    protected $filename;
    public $already_running = false;
   
    function __construct($directory) {
       
        $this->filename = $directory . '/' . basename($_SERVER['PHP_SELF']) . '.pid';
       
        if(is_writable($this->filename) || is_writable($directory)) {
           
            if(file_exists($this->filename)) {
                $pid = (int)trim(file_get_contents($this->filename));
                if(posix_kill($pid, 0)) {
                    $this->already_running = true;
                }
            }
           
        }
        else {
            die("Cannot write to pid file '$this->filename'. Program execution halted.n");
        }
       
        if(!$this->already_running) {
            $pid = getmypid();
            file_put_contents($this->filename, $pid);
        }
       
    }

    public function __destruct() {

        if(!$this->already_running && file_exists($this->filename) && is_writeable($this->filename)) {
            unlink($this->filename);
        }
   
    }
   
}

Use Class below

$pid = new pid('/tmp');
if($pid->already_running) {
    echo "Already running.n";
    exit;
}
else {
    echo "Running...n";
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
Manomite
  • 199
  • 1
  • 5
0

Inspired by Mark Amery's answer I created this class. This might help someone. Simply change the "temp/lockFile.pid" to where you want the file placed.

class ProcessLocker
{
    private $lockFile;
    private $gotLock;
    private $wouldBlock;

    function __construct()
    {
        $this->lockFile = fopen('temp/lockFile.pid', 'c');

        if ($this->lockFile === false) {
            throw new Exception("Unable to open the file.");
        }

        $this->gotLock = flock($this->lockFile, LOCK_EX | LOCK_NB, $this->wouldBlock);
    }

    function __destruct()
    {
        $this->unlockProcess();
    }

    public function isLocked()
    {
        if (!$this->gotLock && $this->wouldBlock) {
            return true;
        }

        return false;
    }

    public function lockProcess()
    {
        if (!$this->gotLock && !$this->wouldBlock) {
            throw new Exception("Unable to lock the file.");
        }

        ftruncate($this->lockFile, 0);
        fwrite($this->lockFile, getmypid() . "\n");
    }

    public function unlockProcess()
    {
        ftruncate($this->lockFile, 0);
        flock($this->lockFile, LOCK_UN);
    }
}

Simply use the class as such in the beginning of your script:

$locker = new ProcessLocker();
        
if(!$locker->isLocked()){
    $locker->lockProcess();
} else{
    // The process is locked
    exit();
}