ohho
ohho

Reputation: 51921

How to prevent PHP script running more than once?

Currently, I tried to prevent an onlytask.php script from running more than once:

$fp = fopen("/tmp/"."onlyme.lock", "a+");
if (flock($fp, LOCK_EX | LOCK_NB)) {
  echo "task started\n";
  //
    while (true) {
      // do something lengthy
      sleep(10);
    }
  //
  flock($fp, LOCK_UN);
} else {
  echo "task already running\n";
}
fclose($fp);

and there is a cron job to execute the above script every minute:

* * * * * php /usr/local/src/onlytask.php

It works for a while. After a few day, when I do:

ps auxwww | grep onlytask

I found that there are two instances running! Not three or more, not one. I killed one of the instances. After a few days, there are two instances again.

What's wrong in the code? Are there other alternatives to limit only one instance of the onlytask.php is running?

p.s. my /tmp/ folder is not cleaned up. ls -al /tmp/*.lock show the lock file was created in day one:

-rw-r--r--  1 root root    0 Dec  4 04:03 onlyme.lock

Upvotes: 11

Views: 10785

Answers (7)

RWaters
RWaters

Reputation: 1

Added a check for old stale locks to galimzhan's answer (not enough *s to comment), so that if the process dies, old lock files would be cleared after three minutes and let cron start the process again. That's what I use:

<?php
$lock = '/tmp/myscript.lock';
if(time()-filemtime($lock) > 180){
    // remove stale locks older than 180 seconds
    unlink($lock);
}
$f = fopen($lock, 'x');
if ($f === false) {
  die("\nCan't acquire lock\n");
} else {
    // Do processing
    while (true) {
    echo "Working\n";
        sleep(2);
    }
    fclose($f);
    unlink($lock);
}

You can also add a timeout to the cron job so that the php process will be killed after, let's say 60 seconds, with something like:

* * * * * user timeout -s 9 60 php /dir/process.php >/dev/null

Upvotes: 0

Enyby
Enyby

Reputation: 4420

Never use unlink for lock files or other functions like rename. It's break your LOCK_EX on Linux. For example, after unlink or rename lock file, any other script always get true from flock().

Best way to detect previous valid end - write to lock file few bytes on the end lock, before LOCK_UN to handle. And after LOCK_EX read few bytes from lock files and ftruncate handle.

Important note: All tested on PHP 5.4.17 on Linux and 5.4.22 on Windows 7.

Example code:

set semaphore:

$handle = fopen($lockFile, 'c+');
if (!is_resource($handle) || !flock($handle, LOCK_EX | LOCK_NB)) {
    if (is_resource($handle)) {
        fclose($handle);
    }
    $handle = false;
    echo SEMAPHORE_DENY;
    exit;
} else {
    $data = fread($handle, 2);
    if ($data !== 'OK') {
        $timePreviousEnter = fileatime($lockFile);
        echo SEMAPHORE_ALLOW_AFTER_FAIL;
    } else {
        echo SEMAPHORE_ALLOW;
    }
    fseek($handle, 0);
    ftruncate($handle, 0);
}

leave semaphore (better call in shutdown handler):

if (is_resource($handle)) {
    fwrite($handle, 'OK');
    flock($handle, LOCK_UN);
    fclose($handle);
    $handle = false;
}

Upvotes: 0

Jordan Mack
Jordan Mack

Reputation: 8733

You can use lock files, as some have suggested, but what you are really looking for is the PHP Semaphore functions. These are kind of like file locks, but designed specifically for what you are doing, restricting access to shared resources.

Upvotes: 0

ohho
ohho

Reputation: 51921

Now I check whether the process is running by ps and warp the php script by a bash script:

 #!/bin/bash

 PIDS=`ps aux | grep onlytask.php | grep -v grep`
 if [ -z "$PIDS" ]; then
     echo "Starting onlytask.php ..."
     php /usr/local/src/onlytask.php >> /var/log/onlytask.log &
 else
     echo "onlytask.php already running."
 fi

and run the bash script by cron every minute.

Upvotes: 11

galymzhan
galymzhan

Reputation: 5523

You should use x flag when opening the lock file:

<?php

$lock = '/tmp/myscript.lock';
$f = fopen($lock, 'x');
if ($f === false) {
  die("\nCan't acquire lock\n");
} else {
  // Do processing
  while (true) {
    echo "Working\n";
    sleep(2);
  }
  fclose($f);
  unlink($lock);
}

Note from the PHP manual

'x' - Create and open for writing only; place the file pointer at the beginning of the file. If the file already exists, the fopen() call will fail by returning FALSE and generating an error of level E_WARNING. If the file does not exist, attempt to create it. This is equivalent to specifying O_EXCL|O_CREAT flags for the underlying open(2) system call.

And here is O_EXCL explanation from man page:

O_EXCL - If O_CREAT and O_EXCL are set, open() shall fail if the file exists. The check for the existence of the file and the creation of the file if it does not exist shall be atomic with respect to other threads executing open() naming the same filename in the same directory with O_EXCL and O_CREAT set. If O_EXCL and O_CREAT are set, and path names a symbolic link, open() shall fail and set errno to [EEXIST], regardless of the contents of the symbolic link. If O_EXCL is set and O_CREAT is not set, the result is undefined.

UPDATE:

More reliable approach - run main script, which acquires lock, runs worker script and releases the lock.

<?php
// File: main.php

$lock = '/tmp/myscript.lock';
$f = fopen($lock, 'x');
if ($f === false) {
  die("\nCan't acquire lock\n");
} else {
  // Spawn worker which does processing (redirect stderr to stdout)
  $worker = './worker 2>&1';
  $output = array();
  $retval = 0;
  exec($worker, $output, $retval);
  echo "Worker exited with code: $retval\n";
  echo "Output:\n";
  echo implode("\n", $output) . "\n";
  // Cleanup the lock
  fclose($f);
  unlink($lock);
}

Here goes the worker. Let's raise a fake fatal error in it:

#!/usr/bin/env php
<?php
// File: worker (must be executable +x)
for ($i = 0; $i < 3; $i++) {
  echo "Processing $i\n";
  if ($i == 2) {
    // Fake fatal error
    trigger_error("Oh, fatal error!", E_USER_ERROR);
  }
  sleep(1);
}

Here is the output I got:

galymzhan@atom:~$ php main.php 
Worker exited with code: 255
Output:
Processing 0
Processing 1
Processing 2
PHP Fatal error:  Oh, fatal error! in /home/galymzhan/worker on line 8
PHP Stack trace:
PHP   1. {main}() /home/galymzhan/worker:0
PHP   2. trigger_error() /home/galymzhan/worker:8

The main point is that the lock file is cleaned up properly so you can run main.php again without problems.

Upvotes: 13

Niclas Larsson
Niclas Larsson

Reputation: 1317

<?php

$sLock = '/tmp/yourScript.lock';

if( file_exist($sLock) ) {
 die( 'There is a lock file' );
}

file_put_content( $sLock, 1 );

// A lot of code

unlink( $sLock );

You can add an extra check by writing the pid and then check it within file_exist-statement. To secure it even more you can fetch all running applications by "ps fax" end check if this file is in the list.

Upvotes: 1

k1dbl4ck
k1dbl4ck

Reputation: 186

try using the presence of the file and not its flock flag :

$lockFile = "/tmp/"."onlyme.lock";
if (!file_exists($lockFile)) {

  touch($lockFile); 

  echo "task started\n";
  //
  // do something lengthy
  //

  unlink($lockFile); 

} else {
  echo "task already running\n";
}

Upvotes: 0

Related Questions