In PHP, how can I test if a file has already been locked with flock? For example, if another running script has called the following:
$fp = fopen('thefile.txt', 'w');
flock($fp, LOCK_EX);
if (!flock($fp, LOCK_EX|LOCK_NB, $wouldblock)) {
if ($wouldblock) {
// another process holds the lock
}
else {
// couldn't lock for another reason, e.g. no such file
}
}
else {
// lock obtained
}
As described in the docs, use LOCK_NB to make a non-blocking attempt to obtain the lock, and on failure check the $wouldblock argument to see if something else holds the lock.
if(stream_get_meta_data($fp)['blocked'])
echo 'file is locked';
else
echo 'file is not locked';
Check it like this:
if (!flock($file, LOCK_EX)) {
throw new Exception(sprintf('File %s is locked', $file));
}
fwrite($file, $write_contents);
Related
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:
Is there a way to wait until the file is not locked anymore, while keeping the option to give this a time limit?
Does fclose() automatically unlock all locks when there would be another process that had locked the file?
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:
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.
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;
}
}
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
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.
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
Here is my Code with filename
it does work without problems if lets say i just use
update.php?pokemon=pikachu
it updates pikachu value in my found.txt +0.0001
But now my problem, when i have multiple threads running and randomly
2 threads are
update.php?pokemon=pikachu
and
update.php?pokemon=zaptos
i see the found.txt file
is empty than!!
so nothing is written in it then anymore.
So i guess its a bug when the php file is opened and another request is posted to the server.
How can i solve this problem this does accour often
found.txt
pikachu:2.2122
arktos:0
zaptos:0
lavados:9.2814
blabla:0
update.php
<?php
$file = "found.txt";
$fh = fopen($file,'r+');
$gotPokemon = $_GET['pokemon'];
$users = '';
while(!feof($fh)) {
$user = explode(':',fgets($fh));
$pokename = trim($user[0]);
$infound = trim($user[1]);
// check for empty indexes
if (!empty($pokename)) {
if ($pokename == $gotPokemon) {
if ($gotPokemon == "Pikachu"){
$infound+=0.0001;
}
if ($gotPokemon == "Arktos"){
$infound+=0.0001;
}
if ($gotPokemon == "Zaptos"){
$infound+=0.0001;
}
if ($gotPokemon == "Lavados"){
$infound+=0.0001;
}
}
$users .= $pokename . ':' . $infound;
$users .= "\r\n";
}
}
file_put_contents('found.txt', $users);
fclose($fh);
?>
I would create an exclusive lock after open the file and then release the lock before closing the file:
For creating an exclusive lock over the file:
flock($fh, LOCK_EX);
To delete it:
flock($fh, LOCK_UN);
Anyway you will need to check if other threads hot already the lock, so the first idea coming up is to try a few attempts to get the lock and if it's not finally possible, to inform the user, throw an exception or whatever other action to avoid an infinite loop:
$fh = fopen("found.txt", "w+");
$attempts = 0;
do {
$attempts++;
if ($attempts > 5) {
// throw exception or return response with http status code = 500
}
if ($attempts != 1) {
sleep(1);
}
} while (!flock($fh, LOCK_EX));
// rest of your code
file_put_contents('found.txt', $users);
flock($fh, LOCK_UN); // release the lock
fclose($fh);
Update
Probably the issue still remains because the reading part, so let's create also a shared lock before start reading and also let's simplify the code:
$file = "found.txt";
$fh = fopen($file,'r+');
$gotPokemon = $_GET['pokemon'];
$users = '';
$wouldblock = true;
// we add a shared lock for reading
$locked = flock($fh, LOCK_SH, $wouldblock); // it will wait if locked ($wouldblock = true)
while(!feof($fh)) {
// your code inside while loop
}
// we add an exclusive lock for writing
flock($fh, LOCK_EX, $wouldblock);
file_put_contents('found.txt', $users);
flock($fh, LOCK_UN); // release the locks
fclose($fh);
Let's see if it works
so i want to lock a file so i can see that a php process is already running. The example code looks as follows:
<?php
$file = fopen("test.txt","w+");
if (flock($file,LOCK_EX))
{
fwrite($file,"Write something");
sleep(10);
flock($file,LOCK_UN);
}
else
{
echo "Error locking file!";
}
fclose($file);
?>
But the problem is that when i executed this file, and execute the file again the second waits for the first one to be done with the lock. So then both are successfully executed. But only the first one has to be executed successfully. Anyknow know how to do this?
It sounds like you don't want flock to be blocking? You just want the first process to obtain the lock and the second one to fail?
To do that, you can use the LOCK_NB flag to stop the flock call from blocking:
$file = fopen("test.txt","r+");
if (flock($file,LOCK_EX | LOCK_NB))
{
fwrite($file,"Write something");
sleep(10);
flock($file,LOCK_UN);
}
else
{
echo "Error locking file!";
}
fclose($file);
More info is available on the PHP flock doc page - http://php.net/manual/en/function.flock.php
You could use a second file called e.g. "test.txt.updated" and maintain the state there -- i.e., whether "test.txt" was already updated or not. Something like the following. NOTE, I've opened "text.txt" in append-mode to see whether two concurrent runs really wrote only once to the file.
<?php
if (! ($f = fopen("text.txt", "a")) )
print("Cannot write text.txt\n");
elseif (! flock($f, LOCK_EX))
print("Error locking text.txt\n");
else {
print("Locked text.txt\n");
if (file_exists("text.txt.updated"))
print("text.txt already updated\n");
else {
print("Updating text.txt and creating text.txt.updated\n");
fwrite($f, "Write something\n");
if ($stamp = fopen("text.txt.updated", "w"))
fwrite($stamp, "whatever\n");
else
print("Oooooops, can't create the updated state file\n");
sleep(10);
}
flock($f, LOCK_UN);
}
?>
I have an issue I can't seem to find the solution for. I am trying to write to a flat text file. I have echoed all variables out on the screen, verified permissions for the user (www-data) and just for grins set everything in the whole folder to 777 - all to no avail. Worst part is I can call on the same function from another file and it writes. I can't see to find the common thread here.....
function ReplaceAreaInFile($AreaStart, $AreaEnd, $File, $ReplaceWith){
$FileContents = GetFileAsString($File);
$Section = GetAreaFromFile($AreaStart, $AreaEnd, $FileContents, TRUE);
if(isset($Section)){
$SectionTop = $AreaStart."\n";
$SectionTop .= $ReplaceWith;
$NewContents = str_replace($Section, $SectionTop, $FileContents);
if (!$Handle = fopen($File, 'w')) {
return "Cannot open file ($File)";
exit;
}/*
if(!flock($Handle, LOCK_EX | LOCK_NB)) {
echo 'Unable to obtain file lock';
exit(-1);
}*/
if (fwrite($Handle, $NewContents) === FALSE) {
return "Cannot write to file ($File)";
exit;
}else{
return $NewContents;
}
}else{
return "<p align=\"center\">There was an issue saving your settings. Please try again. If the issue persists contact your provider.</p>";
}
}
Try with...
$Handle = fopen($File, 'w');
if ($Handle === false) {
die("Cannot open file ($File)");
}
$written = fwrite($Handle, $NewContents);
if ($written === false) {
die("Invalid arguments - could not write to file ($File)");
}
if ((strlen($NewContents) > 0) && ($written < strlen($NewContents))) {
die("There was a problem writing to $File - $written chars written");
}
fclose($Handle);
echo "Wrote $written bytes to $File\n"; // or log to a file
return $NewContents;
and also check for any problems in the error log. There should be something, assuming you've enabled error logging.
You need to check for number of characters written since in PHP fwrite behaves like this:
After having problems with fwrite() returning 0 in cases where one
would fully expect a return value of false, I took a look at the
source code for php's fwrite() itself. The function will only return
false if you pass in invalid arguments. Any other error, just as a
broken pipe or closed connection, will result in a return value of
less than strlen($string), in most cases 0.
Also, note that you might be writing to a file, but to a different file that you're expecting to write. Absolute paths might help with tracking this.
The final solution I ended up using for this:
function ReplaceAreaInFile($AreaStart, $AreaEnd, $File, $ReplaceWith){
$FileContents = GetFileAsString($File);
$Section = GetAreaFromFile($AreaStart, $AreaEnd, $FileContents, TRUE);
if(isset($Section)){
$SectionTop = $AreaStart."\n";
$SectionTop .= $ReplaceWith;
$NewContents = str_replace($Section, $SectionTop, $FileContents);
return $NewContents;
}else{
return "<p align=\"center\">There was an issue saving your settings.</p>";
}
}
function WriteNewConfigToFile($File2WriteName, $ContentsForFile){
file_put_contents($File2WriteName, $ContentsForFile, LOCK_EX);
}
I did end up using absolute file paths and had to check the permissions on the files. I had to make sure the www-data user in Apache was able to write to the files and was also the user running the script.
I am locking files but then when I read them I cannot, my code...
// ACQUIRE READ LOCK
if(flock($file, LOCK_SH)) {
// READ HASHES FILE
if($contents = file('haasdas.txt')) {
// RELEASE READ LOCK
flock($file, LOCK_UN);
} else {
echo 'errrrrrorzzzer';
}
}
What is going on here?
As the docs mention, flock() works not on a filename, but on a file descriptor:
$fd=fopen($filename,'rb');
while (!flock($fd,LOCK_SH)) usleep(500);
$fs=fstat($fd);
$contents=fread($fd,$fs['size']);
flock($fd,LOCK_UN);
fclose($fd);
error handling is left as an exercise to the reader ...