2

I have a process that writes a file using file_put_contents():

file_put_contents ( $file, $data, LOCK_EX );

I have added the LOCK_EX parameter to prevent concurrent processes from writing to the same file, and prevent trying to read it when it's still being written to.

I'm having difficulties testing this properly due to the concurrent nature, and I'm not sure how to approach this. I've got this so far:

if (file_exists($file)) {
    $fp = fopen($file, 'r+');
    if (!flock($fp, LOCK_EX|LOCK_NB, $wouldblock)) {
        if ($wouldblock) {
            // how can I wait until the file is unlocked?
        } else {
            // what other reasons could there be for not being able to lock?
        }
    }
    // does calling fclose automatically close all locks even is a flock was not obtained above?
    fclose($file);
}

Questions being:

  1. Is there a way to wait until the file is not locked anymore, while keeping the option to give this a time limit?
  2. Does fclose() automatically unlock all locks when there would be another process that had locked the file?
Ewout
  • 2,348
  • 1
  • 20
  • 24
  • 1
    To answer your 2nd question, no, close does not clear a lock. See the 2nd entry in the Changelog for [flock](http://php.net/manual/en/function.flock.php). – Dave Mar 12 '19 at 13:47
  • maybe you forgot `flock($fp, LOCK_UN)` – Deadooshka Mar 12 '19 at 14:35

4 Answers4

1

I often use a small class... that is secure and fast, basically you have to write only when you obtain exclusive lock on the file otherwise you should wait until is locked...

lock_file.php

<?php
  /*
  Reference Material
  http://en.wikipedia.org/wiki/ACID
  */
  class Exclusive_Lock {
    /* Private variables */
    public $filename; // The file to be locked
    public $timeout = 30; // The timeout value of the lock
    public $permission = 0755; // The permission value of the locked file
    /* Constructor */
    public function __construct($filename, $timeout = 1, $permission = null, $override = false) {
      // Append '.lck' extension to filename for the locking mechanism
      $this->filename = $filename . '.lck';
      // Timeout should be some factor greater than the maximum script execution time
      $temp = @get_cfg_var('max_execution_time');
      if ($temp === false || $override === true) {
        if ($timeout >= 1) $this->timeout = $timeout;
        set_time_limit($this->timeout);
      } else {
        if ($timeout < 1) $this->timeout = $temp;
        else $this->timeout = $timeout * $temp;
      }
      // Should some other permission value be necessary
      if (isset($permission)) $this->permission = $permission;
    }
    /* Methods */
    public function acquireLock() {
      // Create the locked file, the 'x' parameter is used to detect a preexisting lock
      $fp = @fopen($this->filename, 'x');
      // If an error occurs fail lock
      if ($fp === false) return false;
      // If the permission set is unsuccessful fail lock
      if (!@chmod($this->filename, $this->permission)) return false;
      // If unable to write the timeout value fail lock
      if (false === @fwrite($fp, time() + intval($this->timeout))) return false;
      // If lock is successfully closed validate lock
      return fclose($fp);
    }
    public function releaseLock() {
      // Delete the file with the extension '.lck'
      return @unlink($this->filename);
    }
    public function timeLock() {
      // Retrieve the contents of the lock file
      $timeout = @file_get_contents($this->filename);
      // If no contents retrieved return error
      if ($timeout === false) return false;
      // Return the timeout value
      return intval($timeout);
    }
  }
?>

Simple use as follow:

  include("lock_file.php");
  $file = new Exclusive_Lock("my_file.dat", 2);
  if ($file->acquireLock()) {
    $data = fopen("my_file.dat", "w+");
    $read = "READ: YES";
    fwrite($data, $read);
    fclose($data);
    $file->releaseLock();
    chmod("my_file.dat", 0755);
    unset($data);
    unset($read);
  }

If you want add more complex level you can use another trick... use while (1) to initialize a infinite loop that breaks only when the exclusive lock is acquired, not suggested since can block your server for an undefined time...

  include("lock_file.php");
  $file = new Exclusive_Lock("my_file.dat", 2);
  while (1) {
    if ($file->acquireLock()) {
      $data = fopen("my_file.dat", "w+");
      $read = "READ: YES";
      fwrite($data, $read);
      fclose($data);
      $file->releaseLock();
      chmod("my_file.dat", 0755);
      unset($data);
      unset($read);
      break;
    }
  }

file_put_contents() is very fast and writes directly into file but as you say has a limit... race condition exists and may happen even if you try to use LOCK_EX. I think that a php class is more flexible and usable...

See you this thread that treats a similar question: php flock behaviour when file is locked by one process

Alessandro
  • 900
  • 12
  • 23
  • That's quite an elaborate solution! If I understand correctly this creates an additional .lck file that serves as a separate checking mechanism. Wouldn't that be more sensitive to race conditions (because it's more operations) than using LOCK_EX + flock? Similar question is informative indeed, thanks! – Ewout Mar 12 '19 at 14:31
  • Glad to help :) – Alessandro Mar 12 '19 at 15:24
1

I wrote a small test that uses sleep() so that I could simulate concurrent read/write processes with a simple AJAX call. It seems this answers both questions:

  1. when the file is locked, a sleep that approximates estimated write duration and subsequent lock check allow for waiting. This could even be put in a while loop with an interval.
  2. fclose() does indeed not remove the lock from the process that's already running as confirmed in some of the answers.

PHP5.5 and lower on windows does not support the $wouldblock parameter according to the docs, I was able to test this on Windows + PHP5.3 and concluded that the file_is_locked() from my test still worked in this scenario: flock() would still return false just not have the $wouldblock parameter but it would still be caught in my else check.

if (isset($_POST['action'])) {
    $file = 'file.txt';
    $fp = fopen($file, 'r+');
    if ($wouldblock = file_is_locked($fp)) {
        // wait and then try again;
        sleep(5);
        $wouldblock = file_is_locked($fp);
    }
    switch ($_POST['action']) {
        case 'write':
            if ($wouldblock) {
                echo 'already writing';
            } else {
                flock($fp, LOCK_EX);
                fwrite($fp, 'yadayada');
                sleep(5);
                echo 'done writing';
            }
            break;
        case 'read':
            if ($wouldblock) {
                echo 'cant read, already writing';
            } else {
                echo fread($fp, filesize($file));
            }
            break;
    }

    fclose($fp);
    die();
}

function file_is_locked( $fp ) {
    if (!flock($fp, LOCK_EX|LOCK_NB, $wouldblock)) {
        if ($wouldblock) {
            return 'locked'; // file is locked
        } else {
            return 'no idea'; // can't lock for whatever reason (for example being locked in Windows + PHP5.3)
        }
    } else {
        return false;
    }
}
Ewout
  • 2,348
  • 1
  • 20
  • 24
0

The first question is answered here How to detect the finish with file_put_contents() in php? and beacuse PHP is single-threaded, only solution is to use extension of core PHP using PTHREADS and one good simple article about it is https://www.mullie.eu/parallel-processing-multi-tasking-php/

The second question is answered here Will flock'ed file be unlocked when the process die unexpectedly?

The fclose() will unlock only valid handle that is opened using fopen() or fsockopen() so if handle is still valid, yes it will close file and release lock.

  • The answer about detecting `file_put_contents()` being finished you link to is about a single request if I understand correctly. But I can have a parallel request (even the same PHP file) trying to read or write to the same file. Unless I misunderstand, this doesn't involve PTHREADS? – Ewout Mar 12 '19 at 14:03
0

Here is a fix for @Alessandro answer to work correctly and not lock the file forever lock_file.php

    <?php
  /*
  Reference Material
  http://en.wikipedia.org/wiki/ACID
  */
  class Exclusive_Lock {
    /* Private variables */
    public $filename; // The file to be locked
    public $timeout = 30; // The timeout value of the lock
    public $permission = 0755; // The permission value of the locked file
    /* Constructor */
    public function __construct($filename, $timeout = 1, $permission = null, $override = false) {
      // Append '.lck' extension to filename for the locking mechanism
      $this->filename = $filename . '.lck';
      // Timeout should be some factor greater than the maximum script execution time
      $temp = @get_cfg_var('max_execution_time');
      if ($temp === false || $override === true) {
        if ($timeout >= 1) $this->timeout = $timeout;
        set_time_limit($this->timeout);
      } else {
        if ($timeout < 1) $this->timeout = $temp;
        else $this->timeout = $timeout ;
      }
      
      // Should some other permission value be necessary
      if (isset($permission)) $this->permission = $permission;
      if($this->timeLock()){
          $this->releaseLock();
      }

    }
    /* Methods */
    public function acquireLock() {
      // Create the locked file, the 'x' parameter is used to detect a preexisting lock
      $fp = @fopen($this->filename, 'x');
      // If an error occurs fail lock
      if ($fp === false) return false;
      // If the permission set is unsuccessful fail lock
      if (!@chmod($this->filename, $this->permission)) return false;
      // If unable to write the timeout value fail lock
      if (false === @fwrite($fp, time() + intval($this->timeout))) return false;
      // If lock is successfully closed validate lock
      return fclose($fp);
    }
    public function releaseLock() {
      // Delete the file with the extension '.lck'
      return @unlink($this->filename);
    }
    private function timeLock() {
      // Retrieve the contents of the lock file
      $timeout = @file_get_contents($this->filename);
      // If no contents retrieved return true
      if ($timeout === false) return true;
      // Return the timeout value
      return (intval($timeout) < time());
    }
  }

use as follow:

  include("lock_file.php");
  $file = new Exclusive_Lock("my_file.dat", 2);
  if ($file->acquireLock()) {
    $data = fopen("my_file.dat", "w+");
    $read = "READ: YES";
    fwrite($data, $read);
    fclose($data);
    $file->releaseLock();
    chmod("my_file.dat", 0755);
    unset($data);
    unset($read);
  }

hope that save some else time

M.Ali El-Sayed
  • 1,608
  • 20
  • 23