phplocking

flock call within function always 'succeeds', ignoring previous lock


To prevent multiple instances of a PHP-based daemon I wrote from ever running simultaneously, I wrote a simple function to acquire a lock with flock when the process starts, and called it at the start of my daemon. A simplified version of the code looks like this:

#!/usr/bin/php
<?php

    function acquire_lock () {
        $file_handle = fopen('mylock.lock', 'w');
        $got_lock_successfully = flock($file_handle, LOCK_EX);
        if (!$got_lock_successfully) {
            throw new Exception("Unexpected failure to acquire lock.");
        }
    }

    acquire_lock(); // Block until all other instances of the script are done...

    // ... then do some stuff, for example:
    for ($i=1; $i<=10; $i++) {
        echo "Sleeping... $i\n";
        sleep(1);
    }

?>

When I execute the script above multiple times in parallel, the behaviour I expect to see - since the lock is never explicitly released throughout the duration of the script - is that the second instance of the script will wait until the first has completed before it proceeds past the acquire_lock() call. In other words, if I run this particular script in two parallel terminals, I expect to see one terminal count to 10 while the other waits, and then see the other count to 10.

This is not what happens. Instead, I see both scripts happily executing in parallel - the second script does not block and wait for the lock to be available.

As you can see, I'm checking the return value from flock and it is true, indicating that the (exclusive) lock has been acquired successfully. Yet this evidently isn't preventing another process from acquiring another 'exclusive' lock on the same file.

Why, and how can I fix this?


Solution

  • Simply store the file pointer resource returned from fopen in a global variable. In the example given in the question, $file_handle is automatically garbage collected and destroyed upon going out of scope when acquire_lock() returns, and this releases the lock taken out by flock.

    For example, here is a modified version of the script from the question which exhibits the desired behaviour (note that the only change is storing the file handle returned by fopen in a global):

    #!/usr/bin/php
    <?php
        
        function acquire_lock () {
            global $lock_handle;
            $lock_handle = fopen('mylock.lock', 'w');
            $got_lock_successfully = flock($lock_handle, LOCK_EX);
            if (!$got_lock_successfully) {
                throw new Exception("Unexpected failure to acquire lock.");
            }
        }
    
        acquire_lock(); // Block until all other instances of the script are done...
    
        // ... then do some stuff, for example:
        for ($i=1; $i<=10; $i++) {
            echo "Sleeping... $i\n";
            sleep(1);
        }
    
    ?>
    

    This is now the documented behaviour of flock, whose docs page says (emphasis mine):

    The lock is released also by fclose(), or when stream is garbage collected.

    (At the time that I originally asked and answered this question, the docs did not say this, and in fact implicitly stated the opposite. I reported this as a bug in 2014 and it was resolved by changing the docs in 2021.)