2006.02.26 - 2006.04.01 - 2006.04.19
Updated documentation may be found at www.insulae.net/doc > QLCK.
This software, collectively named QLCK as QLOCK was taken by a lot of other applications, is free and must remain free (under the terms of the following copyright and license note), all I ask is a valid email address from each user. I distribute it in the hope it may be useful and other people can contribute to make it stronger, but forgive me if it doesn't work as expected.
In particular, the queueing and locking scheme described in this document doesn't thoroughly care for security, assuming that security issues – e.g. that a malicious attacker could interfere with the content of the $tmpDir – are demanded to another layer. If you are concerned with security, you have to add measures in order to achieve it.
Please report and/or correct any bug you find, or errors in this documentation. Also report any method to optimize these functions for consuming less time and system resources.
Race conditions
PHP flock() function
Is QLCK actually useful?
QLCK and its working principle
The working scheme
start.php
qwrite.php
Auxiliary functions
QLCK functions
pWrite()
qWrite()
qLock()
qLockDR()
The repeated instances problem
qCheckR()
The interrupted instances problem
qwrite.php's main code
Content
How to test these scripts
How to use QLCK functions
How to register
Debug and log files
Tests
The race test
The loop torture test
Samples
Copyright and license
Software license
Documentation license
Digital signature
Changelog
In the following, lines starting with # are optional instructions to enable removing #, while lines starting with // are comments.
A race condition occurs when many scripts or instances of the same script aim at accessing the same resource at the same or near-same time, in a way that may cause conflicts between the operations performed. Each instance may interrupt each other between any two instructions, while they are manipulating the same shared resource, causing the program not to work as expected.
In this document, instance means everything attempts to access the resource.
As an example, two instances may simultaneously open the same file to update its content: the first instance reads the current content; the second reads the current content too, before it has been updated by the first one. Now the first instance writes its new content to the file; then the second instance writes its new content too, overwriting the file updated by the first: all changes made by the first instance are lost.
So, it seems that a safe custom locking system to manage race conditions is needed and, whether it is true or not, since I didn't find one that suited my needs, I wrote my own functions.
A race condition manager has to deal with many issues (from comments at php.net):
"If a browser starts a request and interrupts it fast enough by using many F5-key refreshes, Apache will KILL the PHP process in the middle of the update operation (because it will detect an unexpected socket close before the PHP script is complete), leaving an incomplete or truncated file!"
R. "Proper method to do this would be to write into a temporary file, then copy that file over the original in a single operation. If the execution stops, only the temp file is corrupt."
RR. "A poster above (below?) has the right idea with writing to a tempory file, then copying. However, achieving the 'right' to use the temp file in the first place is still a race condition issue"
RRR. [Not if the temporary file has a random name unique for each instance.]
R. To solve most of the "PHP quit writing my file and messed it up" problems just add: ignore_user_abort(true); before fopen and: ignore_user_abort(false); after fclose"
"Creating lock files and testing for their existence is absolutely wrong, NFS or not. The only file operations that are guaranteed to be atomic are mkdir() and symlink(). And those aren't atomic over NFS (or other network filesystems) either.
Short answer: create lock directories, not files. Don't stat() the directory to test, simply try to create it and see if it fails."
"For PHP newbies: you don't need to use clearstatcache() because its cached values are stored only for the single request, not among various requests"
"If mkdir() is atomic, then we do not need to worry about race conditions while trying to make the lockdir, unless of course were writing to NFS, for which this function
will be useless."
In addition, a functional locking scheme must avoid deadlocks, i.e. situations in which two instances wait indefinitely for each other to release a locked resource.
flock() functionPHP provides the function flock() to lock a file, preventing access to it, and unlock it to make it available to other instances.
It works as advertised, (fopen truncates the file to zero length in some situations) can be used to lock a file and, combined with a semaphore file, to lock the access to every resources, exactly as QLCK does. flock() is built in, relies on atomic operations to safely lock a file,
There is some lack of understanding about flock(), perhaps due to the fact it is a bit underexplained in its PHP manual page.
flock() use
flock() use with semaphore files
I'm writing a note about flock().
I wrote the QLCK library persuaded by user comments, reported in the function's manual page, that made me believe flock() presented some inconveniences. QLCK actually works, but a discussion with dranger at export dash japan dot com has definitely convinced me that flock() can do quite everything QLCK does and, being a built-in PHP function implemented at a deeper level, is a better solution everywhere can be used.
As a built-in function, flock() is obviously faster. It allows a process to sleep until a lock is released, while qLock() keeps the process running in a while loop, consuming CPU time as well as forcing the calling process to do the same. Then, flock() doesn't require writing to the filesystem, which imposes a speed penalty. Finally, flock() locks are implemented at machine level, so can be accessed from any program on the machine, not just PHP scripts including the QLCK library.
It's possible that QLCK may keep some residual usefulness in situations where is important to queue instances as they come in, on a first-come, first served basis. At present, however, I have no examples of such a situation.
The qLock() and qWrite() functions defined below should allow a number of simultaneous instances to access the same file, by ordering their access operations in a queue.
Each instance creates a temporary directory that, detected by forthcoming instances, suspends their execution, locking the access to the resource, until it is removed by the instance that created it. These temporary locking directories have names starting with a number: each instance takes a number greater than those of pre-existing locking directories, is suspended by directories with numbers less than its own and ignores directories with greater numbers, so that locking directories establish a queue where first-come instances get precedence over incoming new ones.
The current number in the queue is basically generated incrementing the greater number found in existing locking directories, with some adjustements.
Using timecodes as queuing numbers.
I was thinking about using timecodes in microseconds generated by microtime() as natural queuing numbers instead of numbers calculated by qLock() as explained, that should automatically order incoming instances on a first-come, first-served basis.
microtime() returns a string "$musec $sec" where $sec is a 10-digit timestamp and $musec is also 10 digits long, 0.dcmdcm00, '0.' plus 8 decimal digits. The last two digits are for decimillionths and centimillionths, and are set to zeroes: if needed, these might allow to increment codes by smaller units, to make them different in case two instances eventually got the same timecode.
This solution has been discarded as resulting 18-digit timecodes are too big for PHP to be manipulated as integers (and comparing float values is unsafe: PHP, like most languages, is vunerable to problems of floating point precision) so one should use BCMath or GMP instead, but these are not available on all systems. Not on mine, anyway: I should write my own comparing and incrementing functions for numbers represented as strings.
Taking shorter timecodes, on the other hand, wouldn't allow to represent a timespan long enough to cover the entire queue life in case of many simultaneous instances, busy or slow server and a long queue.
A typical working environment for QLCK is that a calling page (e.g. start.php) calls a script qwrite.php to write a string $wString to a file specified by $filePath. The called script includes qlock.php with QLCK functions, and uses them to implement queuing and writing to the file, according the following scheme:
// if (qWrite($wString, $filePath, $accessMode, $lockSuffix, $tmpDir, $maxTries)) {
//
// # Output results
//
// } else {
//
// # Manage failure
//
// }
The qWrite() function calls qLock() to take a place in queue and returns true if writing is successful, false otherwise; within it qLock() tries to create a temporary locking directory under $tmpDir, returning its name or false in case of failure.
Locking directories names related to a queue for a particular resource contain $lockSuffix as an identifier for that resource.
Writing to the file is provided by pWrite(), a function that writes $wString to a temporary file and renames it as $filePath after some integrity check.
After successful writing or failure, different actions may be taken, e.g. different landing scripts called or included to display the results.
Variables used as arguments in qWrite(), shown in the above scheme, are defined at the beginning of qwrite.php. Their use and meaning is explained below in function descriptions.
The starting page may call qwrite.php through a link, by submitting a form, by header(), setting JavaScript location, or include it. In this document and in the samples provided, qlock.php's code is enclosed in qwrite.php.
The generated HTML page asks for a nickname, to identify the instance, and at a prescribed time, defined within JavaScript code, submits a form to call qwrite.php. Opening several copies of this page in different browser windows or tabs results in multiple simultaneous calls at the specified time.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Manage Race Condition</title>
</head>
Once loaded the page, focus is given to the nickname input field for a quick compile.
<body onLoad="document.getElementById('nickname').focus()">
If you plan to test the script asking other people to open many different copies of start.php to generate a huge number of qwrite.php instances at the same time, each tester must set his system clock accurately to make calls simultaneous.
<h2>Manage Race Condition</h2>
<p>Please set your system's clock exactly, for instance looking <a href="http://wwp.greenwichmeantime.com" target="_blank">here</a>, and reload.</p>
Entering a nickname or numeric identifier allows matching each launched start.php copy, and related instance(s), with its log file, for testing purposes. The form also sends a unique auto-generated instance identifier, but the identifier entered by the user is intended to be more mnemonic.
<p>Enter an optional nickname (a-zA-Z0-9) to identify yourself:</p>
<p><form id="sendform" name="sendform" method="POST" action="qwrite.php">
<input id="nickname" name="nickname" type="text" size=24 maxlength=30>
<input id="iid" name="iid" type="hidden" value="<? echo md5(uniqid(rand(), true)) ?>">
</form></p>
Here are displayed the current local time, according the system clock, the time at which qwrite.php will be automatically launched (set it below), and the auto-generated instance identifier. When opening many start.php copies in different windows, some browsers reload the same copy from their cache, with the same instance identifier, instead of a new copy for each window: if this happen, you see that the instance identifier of the current opened page is the same of the prevoius one, and have to reload the page to refresh it.
<p><form>
<input id="yyyy" name="yyyy" type="text" size=4>
<input id="mo" name="mo" type="text" size=2>
<input id="dd" name="dd" type="text" size=2>
<input id="hh" name="hh" type="text" size=7>
<input id="mm" name="mm" type="text" size=2>
<input id="ss" name="ss" type="text" size=2>
</form></p>
<p>This instance is n. <script>document.write(document.getElementById('iid').value)</script>.</p>
<p>At <span id="zero"></span> this page will launch a script<br>
which aim is to write to a file, managing the eventual race condition<br>
involved in writing to the same file by many simultaneous attempts.</p>
The following buttons allow to launch qwrite.php immediately once (just as it's clicked, the first button is disabled), or to try submitting the form many times cli-cli-reclicking the second button, to produce repeated instances with the same data sent to qwrite.php. Repeated instances can be massively created using repeat.php instead of start.php.
<p><form>
<input id="subb" name="subb" type="button" value="Or run it NOW!" onClick="document.getElementById('sendform').submit(); this.disabled = true;">
</form></p>
<p>Or you can try generating repeated instances by clicking hysterically on this button,<br>
as most users do to to speed-up submission:</p>
<p><form>
<input id="suba" name="suba" type="button" value="Tickle me, Baby!" onClick="document.getElementById('sendform').submit()">
</form></p>
The following JavaScript script initializes the above clock and refreshes it every second, and calls qwrite.php at the launch time specified in the code (here at 20:00:00):
<script language="Javascript">
<!-- // adapted from a clock by Kevin Roth - www.kevinroth.com
var showMilitaryTime = true;
function showHours(theHour) {
if (showMilitaryTime || (theHour > 0 && theHour < 13))
return (theHour);
else if (theHour == 0)
return 12;
else return (theHour - 12)
}
function fillZeros(inValue) {
if (inValue > 9)
return "" + inValue;
else return "0" + inValue;
}
function showAmPm() {
if (showMilitaryTime)
return ("");
if (now.getHours() < 12)
return (" AM");
else return (" PM");
}
function showTime() {
now = new Date;
document.getElementById('yyyy').value = now.getYear();
document.getElementById('mo').value = fillZeros(now.getMonth() + 1);
document.getElementById('dd').value = fillZeros(now.getDate());
document.getElementById('hh').value = showHours(now.getHours()) + showAmPm();
document.getElementById('mm').value = fillZeros(now.getMinutes());
document.getElementById('ss').value = fillZeros(now.getSeconds());
var currentYear = now.getYear(); // 2006 on IE, 106 on Opera, Firefox
var currentMonth = now.getMonth() + 1; // 0-11 + 1
var currentMonthDay = now.getDate(); // 1-31
var currentHour = now.getHours(); // 0-23
var currentMinute = now.getMinutes(); // 0-59
var currentSecond = now.getSeconds(); // 0-59
var zeroYear = currentYear;
var zeroMonth = currentMonth;
var zeroMonthDay = currentMonthDay;
var zeroHour = 20;
var zeroMinute = 0;
var zeroSecond = 0;
// Set these
if (currentYear == zeroYear
&& currentMonth == zeroMonth
&& currentMonthDay == zeroMonthDay
&& currentHour == zeroHour
&& currentMinute == zeroMinute
&& currentSecond == zeroSecond) {
document.getElementById('sendform').submit();
}
var zeroTime = zeroYear + '.' + fillZeros(zeroMonth) + '.' + fillZeros(zeroMonthDay) + ' ' + showHours(zeroHour) + showAmPm() + ':' + fillZeros(zeroMinute) + ':' + fillZeros(zeroSecond);
document.getElementById('zero').innerHTML = zeroTime;
setTimeout("showTime()", 1000);
}
showTime();
// -->
</script>
</body>
</html>
Note that the instructions
// if (currentYear == zeroYear
// && currentMonth == zeroMonth
// && currentMonthDay == zeroMonthDay
// && currentHour == zeroHour
// && currentMinute == zeroMinute
// && currentSecond == zeroSecond) {
// document.getElementById('sendform').submit();
// }
call qwrite.php at the launch time specified by zeroHour, zeroMinute and zeroSecond, while
// if (currentYear == zeroYear
// && currentMonth == zeroMonth
// && currentMonthDay == zeroMonthDay
// && currentHour >= zeroHour
// && currentMinute >= zeroMinute
// && currentSecond >= zeroSecond) {
// document.getElementById('sendform').submit();
// }
repeat calling qwrite.php at the launch time and every second after it has expired. Since user abort is ignored in qwrite.php, these result in multiple repeated qwrite.php instances with the same form data, that have been used for testing purposes.
Another script, repeat.php, is provided to generate repeated instances of qwrite.php with the same data, used to study the repeated instances problem. Both scripts are based on code made publicly available by third parties and are not subject to the copyright and license note at the end of this document.
This script defines QLCK functions and auxiliary functions used to generate timecodes registered in log files, define parameters used by them, then attempts to add a line to myfile.txt, taking its order in an eventual queue formed by other instances that try to access the same file.
Eventual sessions must be started and recovered before any header is sent – i.e. at the beginning of the script that includes QLCK functions.
<?
This function microtime_float() provides a replacement for microtime() called with the get_as_float parameter, available only since PHP 5.0.0, to get the current time with microseconds as a floating point number:
function microtime_float() {
list($usec, $sec) = explode(' ', microtime());
return ((float)$usec + (float)$sec);
}
Alternate code for this task might be
// $floattime = (float) array_sum(explode(' ', microtime()));
// $mutime = array_sum(explode(' ', microtime()));
microtime() returns a "$musec $sec" string, where $sec is the current UNIX timestamp (10 digits long, might become longer in some future) and $musec is a 10-digit string 0.dcmdcm00, '0.' plus 8 decimal digits, to represent microseconds (millionths of second) to add to the current timestamp. The last two digits are for decimillionths and centimillionths and are set as zeroes.
It is used in the following instruction, and at the end of execution, to calculate start and final instants and get the total time taken to get a place in the queue and write to the file.
$time_start = microtime_float();
The following functions are to generate timecodes to register, in the log files created by running instances, the execution instant of each step when it is performed:
mut() creates a timecode like 1139829573.10558700 - seconds since Unix Epoch + microseconds,
function mut() {
list($musec, $sec) = explode(' ', microtime());
return $sec . substr($musec, 1, strlen($musec) - 1);
}
while microseconds() creates a timecode string like 113982957310558700 - microseconds since Unix Epoch,
function microseconds() {
list($musec, $sec) = explode(' ', microtime());
return $sec . substr($musec, 2, strlen($musec) - 2);
}
The functions shown above do not depend on the lengths of $sec, $musec strings.
You can see that microseconds timecode strings are too long to be compared and used as integers or floats.
The following rlg() function has been created to display and register messages to a log file for testing and debugging purposes,
function rlg ($s) {
$s = mut().$s;
$h = fopen($logfile, 'a+'); fwrite ($h, $s); fclose($h);
echo "$s<br>";
}
although I preferred to obtain the same result repeating explicitly the same instructions each time, as it is more flexible. The $logfile variable, defined at the beginning of the action, must be declared global within functions.
Finally, azAZ09() removes non-alphanumeric characters from the argument string:
function azAZ09($str) {
$length = strlen($str);
for($a = 0; $a < $length; $a++) {
$code = ord($str[$a]);
(($code > 47 && $code < 58) || ($code > 64 && $code < 91) || ($code > 96 && $code < 123)) ? $acc .= $str[$a] : $acc .= '';
}
return $acc;
}
It is used to filter the entered nickname to avoid code injection, allowing only alphanumeric characters in it.
The functions shown above are considered trivial, thus not covered by the copyright and license note at the end of this document.
The functions described below create and look for temporary locking directories in a directory specified by the $tmpDir path relative to the current directory. If the directory $tmpDir doesn't exists, functions silently use the current directory, which existence is guaranteed.
Debug / log instructions have been removed to make the code more readable. You can find them in the qwrite.php script provided with the package.
Single quotes are used instead of double quotes whenever possible, to avoid frequent calls to the PHP string parser that can result in slower, more resource-consuming scripts. Also kept an eye on limiting the number of variables.
Once locked the resource to access, you can write to it using PHP fwrite() function. For added safety a pWrite() function is provided to write first to a temporary file, then do some integrity check before renaming it as the file to write in, according some suggestions expressed by PHP users.
pWrite() (Prudent/Paranoid file Write) writes $wString to a file specified by $filePath – if the file doesn't exist, creates it – and returns true or false: therefore, it is suitable for use in instructions as
// if (pWrite($wString, $filePath, 'a', '', '')) {
// # do something
// } else {
// # do something else
// }
$accessMode is a fopen() mode that allows writing: r+, w, w+, a, a+ are supported.
$tempWriteId is the unique portion of the temporary file name, may be user-defined or random as default.
$tempSuffix is the suffix, or extension, of the temporary file name, default 'wrt'. Be careful not to use the same suffix used by other functions/features like qLock().
pWrite() itself does not prevent a race condition with access to the file to write in: use together with qLock(), as in qWrite().
function pWrite($wString, $filePath, $accessMode, $tempWriteId, $tempSuffix) {
If $filePath is not empty, goes on. Defines default values of unprovided parameters, and creates a string to add to $filePath to compose the name of the temporary file:
if ($filePath != '') {
if (empty($accessMode)) $accessMode = 'w';
if ($tempWriteId == '') $tempWriteId = md5(uniqid(rand(), true));
if ($tempSuffix == '') $tempSuffix = 'wrt';
$tempWriteId = $tempWriteId.'.'.$tempSuffix;
As the temporary file's name is composed adding "$tempWriteId.$tempSuffix" to $filePath, this file is created under the same directory of $filePath.
Accidentally unrenamed temporary files do not affect operations, and are deleted after 10 hours (36000 seconds, or another configurable time) by the next instance using pWrite() for the same working directory.
$tfs = glob('*.'.$tempSuffix) or array(); foreach($tfs as $tf) { if ((time() - @filemtime($tf)) > 36000) @unlink($tf);// TESTED}
N.B. $array = glob(...) or array() is a valid expression,
but foreach ((glob() or array()) as $tf) or count(glob() or array()) do not give the expected results.
If the file specified by $filePath doesn't exist, it is created:
if (!is_file($filePath)) {
$hl = fopen($filePath, 'w'); fwrite ($hl, ''); fclose($hl);
}
The following sections manage writing to the file through a temporary file, with integrity ckeck, according supported fopen() access modes.
If $filePath has to be written with the content of $wString, this is written to a temporary file; then, if the content of this temporary file coincides with $wString, it is renamed as $filePath and the function returns true. If not, the temporary file is not renamed and remains in the directory (will be deleted after its expiry age, as set) or may eventually be removed, the original file is left untouched and the function returns false.
if ($accessMode == 'w' or $accessMode == 'w+') {
$handle = fopen($filePath.'.'.$tempWriteId, $accessMode);
fwrite ($handle, $wString);
fclose($handle);
// Integrity check
if ($wString == file_get_contents($filePath.'.'.$tempWriteId)) {
rename($filePath.'.'.$tempWriteId, $filePath);
return true;
} else {
# @unlink($filePath.'.'.$tempWriteId);
return false;
}
The @ prevents error or warning messages in case the comparison failed because the temporary file had not been created. This case, a @ would be likely required also before file_get_contents(), fopen(), fwrite(), fclose(), and proper measures should be taken, e.g. displaying an error message. According the user's needs, pWrite() can be customized various ways.
Note. Some people report that file_get_contents() appends /n to its output string, so one should rtrim() it before checking; TESTED, this did not happen to me: $wString == file_get_contents([...]) is true without manipulations when [...] has been previously written with $wString.
If $wString has to be appended to $filePath, instructions are the same except one has to append $wString to a $content string to compare with the temporary file's content.
} elseif ($accessMode == 'a' or $accessMode == 'a+') {
$content = file_get_contents($filePath);
$content .= $wString;
copy($filePath, $filePath.'.'.$tempWriteId);
$handle = fopen($filePath.'.'.$tempWriteId, $accessMode);
fwrite ($handle, $wString);
fclose($handle);
// Integrity check
if ($content == file_get_contents($filePath.'.'.$tempWriteId)) {
rename($filePath.'.'.$tempWriteId, $filePath);
return true;
} else {
# @unlink($filePath.'.'.$tempWriteId);
return false;
}
N.B. One could save a variable defining $wString = file_get_contents($filePath).$wString and modifying instructions accordingly.
If you have to overwrite $filePath with $wString in r+ mode, instructions are similar, except you have to provide specific integrity check according operations performed on the file, as a general criterion doesn't exist. For instance, you can use r+ mode to write $wString to the file and then truncate it at the written string length, as an alternative to w, w+.
} elseif ($accessMode == 'r+') {
copy($filePath, $filePath.'.'.$tempWriteId);
$handle = fopen($filePath.'.'.$tempWriteId, $accessMode);
fwrite ($handle, $wString);
fclose($handle);
// Places the pointer at the beginning and overwrites existing content:
// Don't know a general criterion to check integrity for 'r+' mode:
// checking depends on what you are doing with the file,
// so you have to provide here specific instructions to check integrity.
rename($filePath.'.'.$tempWriteId, $filePath);
return true;
} else {
return false;
}
If $filePath is empty, there is nothing to do and the function returns false.
} else {
return false;
}
}
This function may be modified to adopt specific measures for each failure case, e.g. an error message in case $filePath is empty, and so on.
This function can be rewritten using switch-case-default instead of if-then-else. It is said to be faster, although not so useful as pWrite() is not used in loops or other control structures that repeat it many times.
qWrite() (Queue and Write) writes $wString to a file specified by $filePath calling qLock() to form a queue of the instances eventually concurring for access the same file, get a place in the queue, lock the file, use pWrite() to write in it, release the lock. Returns true in case of success or false for failure, so its natural use is in contexts like the following:
// if (qWrite($wString, $filePath, 'a+', $lockSuffix, $tmpDir, 100)) {
// # do something
// } else {
// # do something else
// }
$accessMode is a fopen() mode as used by pWrite().
$lockSuffix is a resource identifier, used by qLock().
$tmpDir is qLock()'s working directory.
$maxTries is the maximum number of qLock() calls to try before renouncing. For instance, qLock() might be blocked by an accidentally unremoved lock – not common, but may occur – so a maximum number of tries is provided to avoid a loop.
It's essential that $tmpDir is the same for all the functions that use it for the same resource within the same script: thus, defining this variable once at the beginning of the script is recommended to avoid errors.
function qWrite($wString, $filePath, $accessMode, $lockSuffix, $tmpDir, $maxTries) {
If the $tmpDir is not specified or doesn't exist, assumes the current directory ('.' instead of '' prevents path expressions like 'one//two', while 'one/./two' is correct). Then defines a default $maxTries value, in case it were missing:
if ($tmpDir == '' or !is_dir($tmpDir)) $tmpDir = '.';
$maxTries = (int) $maxTries; if ($maxTries <= 0) $maxTries = 10;
If the file to write in, or its resource identifier are undefined, quits immediately returning false:
if ($filePath == '' or $lockSuffix == '') {
return false;
else, goes on trying qLock() until the lock is successfully acquired, or $maxTries expire.
qLock() returns the name of its created locking directory, or false if for some reason the lock is not acquired. A first try gives a variable $ld with a name, or false, and a while loop goes on trying qLock(), counting the number of tries, until it quits returning false.
} else {// qLock($lockSuffix, $tmpDir, $maxWait, $padLength)$i = 0; $ld = qLock($lockSuffix, $tmpDir, 5, 6); while (!$ld and $i < $maxTries - 1) { $ld = qLock($lockSuffix, $tmpDir, 5, 6); $i++; }
Usually qLock() gets a place in queue and locks the file at its first try, except when a race condition is present with a high number of simultaneous instances, and some of them have to wait a bit.
The while loop ends when $ld is not false, or $i + 1 equals $maxTries ($i = 0, before the loop, is the first try).
Once got out of the loop, $ld may be still false, and the maximum number of tries expired; or give a locking directory name, with $i either less than or equal to $maxTries - 1 (a last $i++ may bring $i to $maxTries - 1 while $ld has just become not false): $ld is the indicator for success or failure, whether there is a locking directory or not.
If $ld is still false, qWrite() returns false; otherwise, calls pWrite() to write to the file and returns true or false according pWrite()'s result:
if (!$ld) { return false; } else {// pWrite($wString, $filePath, $accessMode, $tempWriteId, $tempSuffix)if (pWrite($wString, $filePath, $accessMode, '', '')) { # if (file_exists($tmpDir.'/'.$ld)) rmdir($tmpDir.'/'.$ld); if (file_exists($tmpDir.'/'.$ld)) { rmdir($tmpDir.'/'.$ld); } else { }// Expanded instruction has been used with log/debugreturn true; } else { return false; } } } // filepath $lockSuffix }
This qLock() (Queue Lock) function may be called by qWrite(), or any suitable set of instructions, to try locking access to a resource specified by $lockSuffix.
$lockSuffix is a code assigned by the developer to a specific resource to form a queue with instances that are trying to access it, allowing queues related to different resources not to interfere. As examples, if the resource is a certain log file, $lockSuffix may be 'log'; if a counter file, 'counter' or 'cnt' or 'cnt21'; or if the resource is a user directory, the suffix might be the username plus '.DIR', and so on. In this demo script the file myfile.txt is identified by 'ggg'.
The developer defines the suffixes used in his applications, being careful to use different suffixes for different resources.
This function attempts to create a locking directory number-uniqueid.lockSuffix (e.g. 000003-sjD7f4hKlxiJOh62iu.ggg) in $tmpDir to take a place in a queue of concurrent instances accessing the resource $lockSuffix: once created the directory, waits for its turn, related to the number the directory name starts with – see the QLCK working principle.
Returns the name of the locking directory when successfully finished, false if something (predicted) goes wrong, so can be used under conditional instructions,
// if (!qLock($lockSuffix, $tmpDir, 5, 6)) {
// # manage failure
// } else {
// # use the acquired lock
// }
or retried up to success
// while (!$ld and $i < $maxTries - 1) {
// $ld = qLock($lockSuffix, $tmpDir, 5, 6);
// $i++;
// }
as in qWrite().
$tmpDir is a path, relative to the current directory (e.g. 'somedir' or '../somedir'), where qLock() creates and looks for locking directories. Default is the current directory. Define it once at the beginning of the script and pass the defined value as an argument to qWrite(), which will do the same with qLock().
$maxWait is the maximum waiting time in the queue, provided to avoid infinite loops. Once expired, the function renounces and returns false.
$padLength is the length of the string representing the order in the queue. As an example, if the current instance gets the third place in queue, its queuing number is 3 and the corresponding string is 000003, while the locking directory name is something like 000003-sjD7f4hKlxiJOh62iu.ggg. A value of 6 allows theoretical 1,000,000 instances in a queue, far exceeding actual needs and server capabilities.
function qLock($lockSuffix, $tmpDir, $maxWait, $padLength) {
Defines the parameters' default values:
if ($tmpDir == '' or !is_dir($tmpDir)) $tmpDir = '.';
$maxWait = (int) $maxWait; if ($maxWait <= 0) $maxWait = 60;
$padLength = (int) $padLength; if ($padLength <= 0) $padLength = 6;
If the resource is not specified, there's nothing to do:
if ($lockSuffix == '') {
return false;
otherwise, action begins changing directory to $tmpDir, to get glob() give locking directory names without path:
} else {
$currentdir = getcwd();
chdir($tmpDir); // relative to the current directory
Now, the current instance takes its number $n in the queue. To be sure that the taken number is greater than those of yet existing directories, this is accomplished in two steps.
As a first quick approximation, an array is compiled with locking directories in $tmpDir, that is directories which names end with $lockSuffix: $n is set to equal the maximum of detected directories' numbers, plus 1, and $npad is a string representing $n with $padLength digits.
$n = 0;
$lockdirs = glob('*-*.'.$lockSuffix, GLOB_ONLYDIR) or array();
foreach($lockdirs as $lockdir) {
# $parts = explode('-', $lockdir); // done as below saves a variable
# $m = intval($parts[0]);
list($m, ) = explode('-', $lockdir);
$m = intval($m);
if ($m > $n) $n = $m + 1;
}
$npad = str_pad((string) $n, $padLength, '0', STR_PAD_LEFT);
This ensures $n to be greater than every number carried by a directory in the array. It's a good basis to get close to the value we need, but between array creation with glob() and the end of foreach() other incoming instances may have taken the same value of $n, or greater values: so, it must be adjusted.
The following while() loop checks if the $n previously obtained is still free, if not increments it. As this check is now performed at each loop, eventual new locking directories created by incoming instances in the meanwhile simply make the cycle repeat and correct $n.
while (glob($npad.'-*.'.$lockSuffix, GLOB_ONLYDIR)) {
$n++;
$npad = str_pad((string) $n, $padLength, '0', STR_PAD_LEFT);
}
Before while(), foreach() was placed not to start incrementing $n one by one from zero, but it's not strictly needed.
The value of $n just calculated should be correct, except for that between the end of the while() loop and the creation of the locking directory of this instance, in the following instruction, another instance might snipe in and take our same $n again. Until a locking directory is created for the current instance, $n's uniqueness is not guaranteed. This case never occurred in normal race conditions, but has been observed a few times during loop torture tests, and is taken into account below to prevent deadlocks.
Now qLock() creates the locking directory. Once created our locking directory, we are sure that our hardly earned $n cannot be taken any more by other instances.
The name of the locking directory starts with "$npad-", to correspond to a place in the queue created for the resource specified by $lockSuffix, ends with ".$lockSuffix" to relate to that resource, and contains a unique portion. Dash and dot are separators to split locking directory names into their parts. Giving the locking directory a unique name makes very unlikely that other instances may create a race condition on the same locking directory:
$lockname = $npad.'-'.md5(uniqid(rand(), true)).'.'.$lockSuffix;
Note. The string variable $npad is not necessary and could be discarded to save memory, together with $padLength, putting $n in locking directory names. I introduced it at an early stage to sort directories by number for testing purposes, and keep it for eventual future testing needs.
To see if the locking directory is created, we directly check mkdir() that returns true for success, false if fails.
if (@mkdir($lockname)) {
If the locking directory has been created, at this point $n is prenoted and any following instance should avoid it, detecting the presence of the corresponding directory, and prenote a higher number.
As stated before, another instance may have taken the same $n of the current one during the time between the end of the previous while() loop and the directory creation (and, mkdir() is said not to be atomic on network file systems): if this is the case, that is, if there is more than one locking directory for $n in $tmpDir, qLock() deletes its locking directory and ends, returning false.
This way, if any concurrent instances are so simultaneous to prenote the same $n, all of them (or all but one, depending on how strictly simultaneous they are) detect the presence of more than one locking directory with this number, and cancel their actions, avoiding the risk of a deadlock. This does not mean a write failure, as qWrite() will try qLock() again up to $maxTries times.
// if (count(glob($npad.'-*.'.$lockSuffix, GLOB_ONLYDIR) or array()) != 1) { // wrong
// if (count(glob($npad.'-*.'.$lockSuffix, GLOB_ONLYDIR)) != 1) { // wrong too
$nlockdirs = glob($npad.'-*.'.$lockSuffix, GLOB_ONLYDIR) or array();
if (count($nlockdirs) != 1) {
# Waiting for a random time may be inserted before return, to break simultaneity
@rmdir($lockname);
return false;
Note. This case of two directories with the same $n occurred a few times, let's say about five per mille, during intense loop torture testing, causing qLock() to be tried twice. So, a value of 100 for $maxTries looks highly precautional against qWrite() faults. I could not see how frequent this case may be on systems where mkdir() atomicity is not guaranteed.
If there is only one locking directory with $n, this number is properly prenoted. There may be still other locking directories with numbers $m < $n created by instances with precedence over the current one in the queue, and removed as they access the resource and end. The current instance waits until it is first in the queue, that is no locking directories are found with precedence over it – or until $maxWait maximum waiting time is reached, to avoid infinite loops caused by unremoved locking directories left by old instances interrupted by accident.
An array $prelockdirs is compiled with locking directories with $m < $n, and repeatedly updated during a cycle that ends when this array has no more elements, or allowed waiting time is expired.
} else { $prelockdirs = array(); $lockdirs = glob('*-*.'.$lockSuffix, GLOB_ONLYDIR) or array(); foreach($lockdirs as $lockdir) { list($m, ) = explode('-', $lockdir); $m = intval($m); if ($m < $n) $prelockdirs[] = $lockdir; } $t0 = time(); $t = 0; while (count($prelockdirs) != 0 and $t < $maxWait) {// (both arrays are recalculated each loop, to keep them updated)$prelockdirs = array(); $lockdirs = glob('*-*.'.$lockSuffix, GLOB_ONLYDIR) or array(); foreach($lockdirs as $lockdir) { list($m, ) = explode('-', $lockdir); $m = intval($m); if ($m < $n) $prelockdirs[] = $lockdir; } $t = time() - $t0; }
At the end of this cycle the current instance is first in the queue (no more $prelockdirs exist), or the maximum allowed waiting time has expired. Both conditions may happen: $prelockdirs may get empty before $maxWait, making the while() cycle end, or may get empty and at the same time $t may reach or exceed $maxWait.
Different combinations of count($prelockdirs) and $t may occur:
count($prelockdirs) == 0 and $t < $maxWait;
count($prelockdirs) == 0 and $t >= $maxWait;
count($prelockdirs) != 0 and $t >= $maxWait.
The remaining combination count($prelockdirs) != 0 and $t < $maxWait can't occur because the while() cycle requires, to end, that either count($prelockdirs) == 0 or $t >= $maxWait.
In the first two cases, no more locking directories have precedence over the current one: the current instance has gained the first place in the queue, so qLock() can return the name of the locking directory created above to advise qWrite() that the lock has been acquired, and the resource can be accessed safely, as other eventual instances are detecting the locking directory and waiting for their turn.
The third combination corresponds to expired waiting time before preceding locking directories have run out: this case, qLock() must stop and return false, to tell qWrite() to try again.
Thus, the condition to ckeck for success is count($prelockdirs) == 0 and for failure count($prelockdirs) != 0 or > 0.
if (count($prelockdirs) != 0) {// or# if ($t >= $maxWait and count($prelockdirs) != 0) { @rmdir($lockname); return false; } else { return $lockname; } } // one n
If for some reason mkdir() fails creating the locking directory or otherwise returns false, qLock() returns false removing the locking directory eventually created:
} else {
// This case has never occurred yet
@rmdir($lockname);
return false;
} // makedir failed
Note. This @rmdir() has been thought to deal also with a highly hypothetical case where mkdir() creates the directory but returns false for any unknown reason. Quite excessive.
Return to the previous working directory:
chdir($currentdir);
} // sfx is defined
}
If interrupted by server fault or other unpredictable accidents, the script using qLock() may leave in $tmpDir unremoved locking directories, that force other instances to wait indefinitely for their removal, since the instances that created them are dead and nobody else will remove them.
To prevent an unremoved directory locking access to a resource forever, undeleted locking directories expire after $maxLockAge seconds and are removed by the qLockDR() function as they are detected in $tmpDir. A typical value is 10-20 minutes.
Since, once started, instances set unlimited execution time and ignore user abort, paving their own way to a virtual immortality, to prevent an instance blocked by unremoved locking directories to run, consuming system resources, until $maxLockAge is reached and another instance deletes the directory that blocks it, a maximum waiting time in qLock() and a maximum number of tries in qWrite() are provided as parameters to stop an instance after some time, or number of tries, when it looks too long.
$maxWait is the maximum qLock() waiting time in the queue, provided to avoid infinite loops. Once expired, qLock() renounces and returns false.
$maxTries is the maximum number of qLock() calls tried by qWrite() before renouncing and returning false.
$maxWait and $maxTries combined give an approximate allowed execution time of $maxWait * $maxTries, and are to be configured together to stop the script after a time that is considered excessive, probable signal of a loop caused by an unremoved lock, still leaving the script enough time to let it complete its operations under normal conditions, even when the server is busy. Reasonable tested values are 5-10 seconds for both parameters, although while testing a high number of repeated instances (now prevented) qWrite() has been allowed to try qLock() up to 100 times. However, under normal conditions qLock() is usually not tried more than once, as it succeeds at its first try.
Unremoved locking directories are left by previous irregularly interrupted instances, while .rep temporary directories are left by previous primary instances that successfully accessed the resource.
If interrupted by unpredictable accidents, the script using qLock() may leave in $tmpDir unremoved locking directories. To prevent an unremoved directory locking access to a resource forever, undeleted locking directories expire after $maxLockAge seconds and are removed by qLockDR() in every running instance.
Also, repeated instance detection, explained below, creates temporary directories, removed after $maxLockAge seconds.
This qLockDR() function (Queue Lock Directories Remove)
function qLockDR($lockSuffix, $tmpDir, $maxLockAge) {
removes from $tmpDir any directory older than $maxLockAge, which name ends in ".$lockSuffix".
qLockDR() must be called once in a script for each resource identifier, more is unuseful.
$lockSuffix is a string that acts as an identifier for the directories to remove.
$tmpDir is a path, relative to the current directory, where to look for directories to remove. Default is the current directory.
$maxLockAge is the number of seconds after which directories are removed. Default 1000 s.
if ($tmpDir == '' or !is_dir($tmpDir)) $tmpDir = '.';
$maxLockAge = (int) $maxLockAge; if ($maxLockAge <= 0) $maxLockAge = 1000;
An array of "*.$lockSuffix" directories in $tmpDir is compiled, then those older than $maxLockAge are removed:
$lockdirs = glob($tmpDir.'/*.'.$lockSuffix, GLOB_ONLYDIR) or array(); foreach($lockdirs as $lockdir) { # if ((time() - @filemtime($lockdir)) > $maxLockAge) @rmdir($lockdir); if ((time() - @filemtime($lockdir)) > $maxLockAge) { @rmdir($lockdir); }// Expanded instruction is to use with log/debug} }
Note. I tried
// foreach((glob($tmpDir.'/*-*.'.$lockSuffix, GLOB_ONLYDIR) or array()) as $lockdir) {
//
// if ((time() - filemtime($lockdir)) > $maxLockAge) @rmdir($lockdir);
//
// }
but (glob([...]) or array())is not accepted as array expression in foreach().
A race condition also occurs in directory removal. Between $lockdirs creation, age check and directory removal, or perhaps even while the array is created, a $lockdir may be removed by another instance, either because has expired or is regularly removed by its creating instance. The current instance may happen to remove, or check the age of, a directory that doesn't exist any more, so @ is prepended to related instructions to suppress error messages.
In a number of tests made launching 20 simultaneous instances of start.php, QLCK has demonstrated to properly span 30-70 qwrite.php instances over 5-10 seconds.
Fine, but why more log files were created than opened instances?
It looked that each opened copy of start.php made repeated requests, generating multiple instances. Due to ignore_abort(true) each instance is performed, resulting in a log file.
This was due to JavaScript instructions in start.php that submitted the form under the condition that current time is greater than, instead of equal to, the time set to trigger the event, at every clock refresh: but initially I thought repeated instances were caused by the client-server request and answer mechanism. I presumed that
"When a high number of quasi-simultaneous concurrent requests is made, the server may become slow while trying to answer all of them, delaying answers. If the client does not receive a proper, prompt answer, it seems to repeat the request aborting its current instance and starting a new one, even several times. But all started instances continue running on server side because of the ignore_abort(true) instruction, thus resulting in more running instances than those started in browser windows or tabs."
This was my first attempt to explain the presence of more logfiles than opened instances, and looked to me a very reasonable hypothesis. Of course, it was wrong.
However, that led me to solve the problem, writing a qCheckR() function to detect repeated instances and prevent them writing to the file. This function is not unuseful, as repeated instances can be generated also, as an example, when a nervous user clicks and clicks on a button to speed up submitting a form. You can try yourself pressing Tickle me, Baby! in start.php.
When accessing a file to update its content, repeated instances result in repeated file updates with the same data, so are not really a problem, except for the waste of server memory and machine time.
When updating a counter register, repeated accesses result in the count being incremented more than by the actual hits. This is annoying, as results in erroneous information.
When adding entries to a file, e.g. a log file, repeated instances result in repeated entries. This is undesirable as can make log files much bigger.
We can put the previous qWrite() scheme under a check with a qCheckR() function that returns true if the instance is detected as repeated, false if primary, as follows:
// # Define parameters $wString, $filePath, $accessMode,
// $lockSuffix, $tmpDir, $maxTries, $maxLockAge
//
// if(qCheckR($repSuffix, $tmpDir)) {
//
// # Instance is repeated
// # Don't access the resource
// # Possibly recover the primary instance output some way
//
// } else {
//
// # Instance is primary
// # Ignore user abort and unset execution time limit
// # Remove expired locks
// # Access the resource
//
// qLockDR($repSuffix, $tmpDir, $maxLockAge);
// qLockDR($lockSuffix, $tmpDir, $maxLockAge);
//
// if (qWrite($wString, $filePath, $accessMode, $lockSuffix, $tmpDir, $maxTries)) {
//
// # Output results
//
// } else {
//
// # Manage failure
//
// }
//
// }
Script operations are performed under the condition that qCheckR() returns false.
Within this scheme, if you want to disable repeated instance detection, for experimental purpose or because you don't need it, you may change qCheckR() to return always false.
Note. If repeated instances occur, the first-occurring primary instance performs the desired operation, but the last one writes its output to the browser: the output or result of the operating primary instance must be some way recoverable by/from this one.
Repeated instances form an additional racing condition, where only the primary instance has to complete its execution, to be managed through locking directories too.
The primary instance of qwrite.php, which has to be executed, corresponds to the first request made by the calling page. Repeated instances correspond to requests made after the first one, by the same copy of the calling page. So, if each instance of start.php generates a unique identifier and propagate it via POST to qwrite.php, all its instances generated by the same calling page will have the same value of this identifier.
Each instance looks if a locking directory, which name contains the identifier, is present in $tmpDir: if not found, creates it and goes on; if found, doesn't create it and stops running. So the instance starting as first creates its locking directory, while repeated instances detect it and terminate without operations.
Note. Since all instances would perform the same operations, it's not strictly needed that the performing instance is actually the one started as first: it is sufficient that one instance does it, and the others end without repeating the same thing. Although directory check and creation is one of the first instructions executed, it may be possible that the first started instance is interrupted before creating the directory, and a second one creates it: this case, the first instance then detects the directory and terminate itself, while the second completes the operation.
Note. This mechanism relies on mkdir() as an atomic operation, to check for the directory and make it at the same time – if the directory to be created already exists, mkdir() doesn't create it and returns false. 1) This is not clearly stated in the manual page, but I found it in a user comment and tested it. Hope it is a general behaviour on all systems. 2) mkdir() is said not to be atomic on network file systems, I can do nothing for it: this is the best I could.
These temporary locking directories are unique for each primary instance, so primary instances don't interfere – they don't block instances with other identifiers, and are removed by other instances when older than $maxLockAge.
function qCheckR($repSuffix, $tmpDir) {
$repSuffix is a string that, as name extension, acts as an identifier for the directories to remove. Conflicts with other resource suffixes must be avoided choosing a different suffix or $tmpDir.
$tmpDir is a path, relative to the current directory, where to check and create the locking directory. It hasn't to be necessarily the same used by other functions. Default is the current directory.
The unique instance identifier generated and sent by the calling page is received as a global variable $_POST['iid']: if not empty, checking and making the directory can occur:
if (isset($_POST['iid']) and $_POST['iid'] != '') {
if ($tmpDir == '' or !is_dir($tmpDir)) $tmpDir = '.';
qCheckR() looks for a locking directory $_POST['iid'].".$repSuffix": if found, returns true to let the script end operations, otherwise creates it and returns false to let execution go on. Checking the presence of the locking directory assumes that mkdir() returns false, and presumingly doesn't create the directory, when the directory to make already exists, otherwise creates it returning true. See 1) in the previous note.
if (@mkdir($tmpDir.'/'.$_POST['iid'].'.'.$repSuffix)) {
return false;
} else {
return true;
}
If $_POST['iid'] is empty or not set, qCheckR() returns true to end execution, as if the current instance were a repeated one.
} else {
return true; // TESTED calling qwrite.php directly
}
}
Note. An early qCheckR() attempt tried to detect repeated instances by a session variable. In that version, detecting repeated instances was based on the assumption that they are started by the same browser window and thus have the same session identifier. The session identifier played the role of the locking directory, but for some unknown reason this method didn't work. See description within script comments if interested.
ignore_user_abort(true) and set_time_limit(0) instructions, originally in qWrite(), have been moved to the main scheme to prevent instance interruptions just after qWrite() start, sometimes occurred during tests.
Some log files looked like this:
// 1140787542.87847800 START qCheckR() for rep:
// 1140787542.87948400 Instance d8b1a7fc926222491b39b2e2c5ff1c93 detected as primary.
// 1140787542.87971700 START qLockDR() for rep;
// 1140787542.88034800 START qLockDR() for ggg;
// 1140787542.88069900 START qWrite() to myfile.txt;
showing that corresponding instances had been mysteriously interrupted.
Such interrupted instances didn't of course write to the destination file, so were a problem if primary instances.
While listening to two songs by Simon Mahler, www.fotone.net (so I recommend his music if you are mumbling over a problem) I noticed that log files showed interruptions occur just before ignore_user_abort(true), and this led me to presume that an interruption, invoked by a client-repeated request before telling the script to ignore user abort, aborted execution.
Right or wrong, moving ignore_user_abort(true) outside of qWrite() I observed no more interruptions.
Additionally, preventing repeated instances to be launched by start.php also removes this problem.
After defining the functions described above, qwrite.php starts a web page to display the results – in an actual situation: here, mainly debug information.
The code below includes instructions that write to the log file of the current instance to help debugging. I decided to leave them as they show how each log starts. Of course, remove log/debug instructions before use.
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Write to File</title>
</head>
<body>
<p><?
In this page, all output except total time is contained in a paragraph. First are defined initialization variables, which values need to be set at the beginning of execution. This is useful, for example, if you want to use the same directory $tmpdir for locking directories related to file access and repeated instance detection.
$id = mut() . '-' . azAZ09($_POST['nickname']) . '-' . md5(uniqid(rand(), true)); ### DEBUG ###
$logfile = "$id.log.txt"; ### DEBUG ###
$wString = "\n\n" . gmdate('Y.m.d H:i:s ') . $id; ### DEBUG ###
# $filePath = 'myfile.txt';
# $accessMode = 'a+';
$repSuffix = 'rep';
$lockSuffix = 'ggg';
$tmpDir = '';
# tries = 100;
# $maxLockAge = 1000;
Here some values are shown commented, for instance if you wanted to set different $maxLockAges for locking directories related to file access and repeated instance detection.
Then, the main execution scheme. If the current instance is not detected as repeated by qCheckR(), user abort is ignored and maximum execution time unset, since instance interruption may occur in case of a long queue and give a corrupt destination file:
if(qCheckR($repSuffix, $tmpDir)) {
# unlink($logfile); ### DEBUG ###
} else {
ignore_user_abort(true);
$logstr = mut()." Ignoring user abort...\n"; ### DEBUG ###
$h = fopen($logfile, 'a+'); fwrite ($h, $logstr); fclose($h); ### DEBUG ###
# echo "$logstr<br>"; ### DEBUG ###
set_time_limit(0);
$logstr = mut()." Unsetting time limit...\n"; ### DEBUG ###
$h = fopen($logfile, 'a+'); fwrite ($h, $logstr); fclose($h); ### DEBUG ###
# echo "$logstr<br>"; ### DEBUG ###
then, expired locking directories are removed, both for the access race condition and for that related to repeated instance detection:
qLockDR($repSuffix, $tmpDir, 1000);
qLockDR($lockSuffix, $tmpDir, 1000);
finally, qWrite() calls qLock() to take a place in queue and pWrite() to access the file, and writes in the page if it has been successfully accessed or not:
if (qWrite($wString, 'myfile.txt', 'a+', $lockSuffix, $tmpDir, 100)) echo '<br>OK.'; else echo '<br>Failed.'; // TESTED
Now, user abort and time limit may be reset, if needed,
# ignore_user_abort(true);
# set_time_limit(0);
and the execution time is calculated
}
$time_end = microtime_float();
$time2 = $time_end - $time_start;
?></p>
and displayed:
<p>Total time: <? echo $time2 ?> seconds.</p>
</body>
</html>
End of qwrite.php.
The qlck.06.02.24.zip package contains a starting page start.php, a commented ready-to-run script qwrite.php which uses QLCK functions to take a place in an eventual queue and write to a file myfile.txt, a sample operation log sample.log.txt and copy of this documentation. For the loop torture test, start.loop.php and qwrite.loop.php are also provided.
Comments in qwrite.php are equivalent to this documentation, but this is more consistently written, checked and proofread. On the other hand, comments might be sometimes more detailed over some points, having been written during development to trace what I was doing.
The following sections tell how to test the scripts included in the package and use them in a working environment.
These functions have been tested, according the following procedure, on a virtual server – a paid hosting service – and tuned until they worked. Thus, I can say they work on the system I tried them. Other systems may split execution time differently between threads, assign each instance a different amount of memory, and so on, yielding unexpected behaviors. I recommend every user to thoroughly test these functions on his system before any use in a real working environment.
To obtain many simultaneous calls to qwrite.php, multiple copies of a page start.php are opened in different browser windows or tabs. The script calls qwrite.php at a prescribed trigger time, so opened copies of start.php result in qwrite.php instances launched and running quasi-simultaneously, asking for concurrent accesses to the same file myfile.txt.
Opening start.php in tabs of the same browser window, rather than in different windows, seems to result in instances of qwrite.php being more simultaneous.
To generate repeated instances in a single window test, click repeatedly Tickle me, Baby! in start.php. Rather than convulse cli-cli-click, one half second distanced click, click, click seem to produce more repeated instances.
The script repeat.php instead of start.php generates multiple repeated instances for each browser window or tab, allowing to test how and how much repeated instances are detected and deactivated.
Each instance tries to detect itself as a repeated instance, and if it is, not to write.
Each instance checks with qCheckR() whether to execute qWrite() that calls qLock() and pWrite() to add to a file myfile.txt a line with an instance identifier, and writes to a log file a report of its operations.
You may also remove the @ error message suppressor where needed.
Also, read about the loop torture test below.
N.B. Be careful when opening start.php copies in IE windows with CTRL-N: each new window loads the content of the parent one without refreshing the page, reproducing previously entered/calculated FORM values. All start.php instances opened this way have the same instance identifier and are detected as repetitions of the same instance, unless you refresh them one by one reloading the page.
N.B. Also Opera 8 loads start.php from its cache, so in order to have different instance identifiers for different instances one must reload each opened starting page to refresh its instance identifier.
Once read how this software works and after testing it in your working environment, you can use QLCK in any context where a script A calls a script B sending it some FORM data to write into a file, or adapt it to other uses. Please read the license.
qCheckR(), qLockDR(), qWrite(), qLock(), pWrite(). This script is suitable to be included everywhere QLCK functions are needed to manage a race condition.include() qlck.inc in your operating script B.qWrite() scheme under qCheckR() recognition or not, according your needs.Note. On NFS and other network file systems, as observed by some user comments at PHP.net, non atomicity of mkdir() may affect these functions' usefulness. These functions may also show other weak points, where different instances may interlace in an unpredicted way. Please test for bugs and report eventual solutions.
Support is discontinued.
For debugging purposes, qwrite.php contains instructions that output to screen, and log in a file, entries related to monitored operations preceded by a microsecond timecode. Each instance has its log file. Logged information may be variable values, tell which branch of a conditional structure is executed, or contain other relevant data.
Generally, log instructions follow this scheme:
within the main scheme, a $logfile variable is created with the name of the log file, unique for each instance, and declared global in all the functions using it,
global $logfile;
at each logging point, an entry string is generated prepending a timecode to a message,
$logstr = mut()." START qLockDR() for $lockSuffix;\n";
then written to the $logfile
$h = fopen($logfile, 'a+'); fwrite ($h, $logstr); fclose($h);
and optionally displayed to screen,
# echo "$logstr<br>";
I preferred to repeat the same instructions along the script, rather than write a function, for greater flexibility, to allow to modify each block according local needs.
As an example, the following code shows log/debug instructions in qLockDR().
function qLockDR($lockSuffix, $tmpDir, $maxLockAge) {
global $logfile; ### DEBUG ###
$logstr = mut()." START qLockDR() for $lockSuffix;\n"; ### DEBUG ###
$h = fopen($logfile, 'a+'); fwrite ($h, $logstr); fclose($h); ### DEBUG ###
# echo "$logstr<br>"; ### DEBUG ###
if ($tmpDir == '' or !is_dir($tmpDir)) $tmpDir = '.';
$maxLockAge = (int) $maxLockAge; if ($maxLockAge <= 0) $maxLockAge = 1000;
$lockdirs = glob($tmpDir.'/*.'.$lockSuffix, GLOB_ONLYDIR) or array();
foreach($lockdirs as $lockdir) {
# if ((time() - @filemtime($lockdir)) > $maxLockAge) @rmdir($lockdir);
if ((time() - @filemtime($lockdir)) > $maxLockAge) {
@rmdir($lockdir);
$logstr = mut()." remove expired locking directory $lockdir;\n"; ### DEBUG ###
$h = fopen($logfile, 'a+'); fwrite ($h, $logstr); fclose($h); ### DEBUG ###
# echo "$logstr<br>"; ### DEBUG ###
}
}
# $logstr = mut()." removed expired locking directories;\n"; ### DEBUG ###
# $h = fopen($logfile, 'a+'); fwrite ($h, $logstr); fclose($h); ### DEBUG ###
# echo "$logstr<br>"; ### DEBUG ###
}
produce an output such as this:
1140910120.16319900 START qLockDR() for rep; 1140910120.16440800 remove expired locking directory ./aa6466d74e05c2a9ea5b6d6fc299a61a.rep; 1140910120.16490300 remove expired locking directory ./ace47eaf22152a6b0efb423b8a4f0655.rep; 1140910120.16537500 remove expired locking directory ./b1c039bd4ee9a6c088c8015f9e01a9fe.rep; 1140910120.16588000 remove expired locking directory ./b5fcbc8d3084b22ad75106535f93556c.rep; 1140910120.16632600 remove expired locking directory ./bfe93500978a876236dfc40e0dd1968c.rep; 1140910120.16676800 remove expired locking directory ./c7d97c599959e2d8469b5dc1d1912c7a.rep; 1140910120.16722000 remove expired locking directory ./d91879eeb87ab1c11189212a13f4c6ca.rep; 1140910120.16767000 remove expired locking directory ./d92590c8e3263619150ad5ead996aaec.rep; 1140910120.16811800 remove expired locking directory ./db0a82f079d5015347e6745be503cc01.rep; 1140910120.16856200 remove expired locking directory ./dc4306b89ce58d1bac9692e3d1fae368.rep; 1140910120.16900700 remove expired locking directory ./de7066cf5f2548919e347e176034ce32.rep; 1140910120.16950200 remove expired locking directory ./df4e2cbb977adaeb031587f044fec838.rep; 1140910120.16996700 remove expired locking directory ./e4c51854b7c9016d8d0c6b9bfe6cb550.rep; 1140910120.17043100 remove expired locking directory ./eafb75ec193e662c2e15ce84463002f5.rep; 1140910120.17591700 remove expired locking directory ./ed7f62bf3676abedba312e5befd128d0.rep; 1140910120.17637500 remove expired locking directory ./f6911fdc9f37992b94d8f9d565807f64.rep;
that shows removed locking directories related to repeated instance detection, or simply
1140910120.17660300 START qLockDR() for ggg;
in the common situation where are no expired locking directories to remove, created by qLock().
### DEBUG ### at the end of each line allows to easily locate logging instructions in the script to comment or remove them.
The following tests have been run to ensure that QLCK works. QLCK passes these tests, but please note that the PHP flock() function, when properly used with or without semaphore files, passes them too.
Numerous tests have been run during and after development, using Internet Explorer 5.0, Opera 8.5 and Mozilla Firefox 1.0.7. The one reported here is significant as a huge quantity of repeated instances have been produced – IE 5.0 seems to create more repeated instances than other tested browsers.
Explorer seems to produce a high number of repeated instances, and primary instances look strongly interlaced. Opera looks fast and its instances are likely to be more simultaneous than those generated by other browsers. Firefox makes instances more distanced in time than other browsers, producing weakly interlaced instances, as if opened starting pages didn't actually launch their qwrite.php instances at the same time.
06.02.24 17.40 Launched 20 instances of repeat.php in 20 IE 5.0 windows:
216 qwrite.php instances run.
196 qwrite.php instances detected as repeated.
20 qwrite.php primary instances wrote to myfile.txt.
20 entries in myfile.txt.
20 temporary .rep directories created.
NO primary instances blocked, no repeated instances blocked.
Total time: log file names report timestamps from 1140799063 to 1140799075, taking 12-13 s from beginning to end.
Timestamps from 1140799063 to 1140799064 correspond to primary writing instances, giving 1-2 s to queue 20 instances and made them write; timestamps from 1140799064 to 1140799075 correspond to repeated instances, taking a time of 11-12 s to detect and stop all 196 repeated instances.
Assuming that in an actual working situation repeated instances are normally not generated, 2 seconds to take care of 20 writing instances gives a measure of the server's performances.
Instances opened with repeat.php have been numbered entering mnemonic identifiers from 01 to 20. Each nn number corresponds to a repeat.php copy opened in a browser window. The table below shows that all opened starting pages produced a primary qwrite.php instance that successfully wrote to the file, and displays how many qwrite.php repeated instances were generated for each opened repeat.php page.
started 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20
written 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20
repeated 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 17 18 19 20
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 17 18 19 20
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 17 18 19 20
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 17 18 19 20
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 17 18 19 20
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 17 18 19 20
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 17 18 19 20
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 18 19 20
01 03 04 05 06 07 08 09 10 11 12 13 14 15 18 19 20
01 08 09 10 11 15 20
11
20 is the maximum number of IE windows that can be opened on my system before it gets unstable. All primary instances wrote to the file, and only once, while repeated instances were all detected and ended: so the functions behaved correctly.
Below there are sample log files related to a similar test.
Those furry guys, used to read the code of the Matrix directly, will also enjoy the file sample.log.txt with sample log file entries related to a race ordered by time, to display how several concurring instances interlace one with each other forming a queue to get access to the shared file.
A poster at php.net suggested to test lock-and-release schemes, alternative to PHP's flock() built-in function, putting them at work within a continuous loop,
# lock() and unlock() function definitions here
while(1) {
if(lock("test")) {
$f = fopen("importantfile", "w") or die;
$pid = getmypid();
$string = "Important Information! From $pid";
fwrite($f, $string);
fclose($f);
$check = file_get_contents("importantfile");
if($check != $string) {
echo "THIS LOCK FAILED!\n";
}
unlock("test");
}
}
(mutatis mutandis) then start two scripts running this loop and see if any alert occurs.
I created a modified start.loop.php that launchs a modified qwrite.loop.php with the following main code, to repeat running qWrite() in a loop that can be stopped making a stop directory (some other minor changes may be necessary),
#
if(qCheckR($repSuffix, $tmpDir)) {
#
} else {
ignore_user_abort(true);
set_time_limit(0);
qLockDR($repSuffix, $tmpDir, 1000);
qLockDR($lockSuffix, $tmpDir, 1000);
while(!is_dir('stop')) {
$wString = gmdate('Y.m.d H:i:s ') . mut() . ' ' . $id; ### DEBUG ###
if (qWrite($wString, 'myfile.txt', 'w', $lockSuffix, $tmpDir, 100)) echo mut()." Write OK.<br>\n"; else echo mut()." Write FAILED.<br>\n";
$logstr = "\n"; ### DEBUG ###
$h = fopen($logfile, 'a+'); fwrite ($h, $logstr); fclose($h); ### DEBUG ###
}
}
#
and placed file check in qWrite() just before unlocking:
#
if (pWrite($wString, $filePath, $accessMode, '', '')) {
// Added to check file in w fopen mode during loop torture test
if ($wString == file_get_contents($filePath)) {
$logstr = mut()." CHECKING $filePath: OK.\n"; ### DEBUG ###
$h = fopen($logfile, 'a+'); fwrite ($h, $logstr); fclose($h); ### DEBUG ###
echo mut()." Check OK,<br>\n";
} else {
$logstr = mut()." CHECKING $filePath: FAILED.\n"; ### DEBUG ###
$h = fopen($logfile, 'a+'); fwrite ($h, $logstr); fclose($h); ### DEBUG ###
echo mut()." Check FAILED,<br>\n";
}
# if (file_exists($tmpDir.'/'.$ld)) rmdir($tmpDir.'/'.$ld);
#
}
Then I launched through the modified starting page 5 instances of qwrite.loop.php (more than five was difficult) and let them run for a while, until they wrote to and checked the file about a thousand times producing outputs formed by rows like
1143826414.87165300 Check OK, 1143826414.87270200 Write OK.
repeated over and over. No FAILED message has been displayed.
More than 5000 writing operations spanned over 680 seconds by five concurring instances, without an error.
Note. As a counter-experiment, placing checking instructions after unlocking the file, when another instance may have altered its content, yielded a certain amount of FAILED alerts – surprisingly, quite less than expected.
The name of the log file 1140910122.43453000-28-b5d99cef254fcf60382287aee47a7f62.log.txt starts with a timecode formed as seconds.microseconds, followed by an identifier entered by the user in the starting page (here a simple number for the window or tab), followed by a unique code, suffix and extension. The log file contains lines as the following,
1140910122.43506400 START qCheckR() for rep: 1140910122.43632200 Instance a1fb3db5f1de6be70b771dd1c8675aec detected as primary. 1140910122.43661900 Ignoring user abort... 1140910122.43683700 Unsetting time limit... 1140910122.43701700 START qLockDR() for rep; 1140910122.43786300 START qLockDR() for ggg; 1140910122.43822900 START qWrite() to myfile.txt; 1140910122.43865600 CALL qLock() to lock ggg; 1140910122.43896500 START qLock() to lock ggg, maximum waiting time 5 seconds; 1140910122.43990400 approximate n = 0 : 000000; 1140910122.44064700 refreshed n = 0 : 000000; 1140910122.44146000 000000-9ef438a458c53442ae7528a1f527939b.ggg locking directory created; 1140910122.44196700 000000-9ef438a458c53442ae7528a1f527939b.ggg is the only locking directory with 0; 1140910122.44248800 0 locking directory have precedence; 1140910122.44285500 END qLock() to lock ggg: OK. 1140910122.44312600 TRIED qLock() 1 times; 1140910122.44337500 LOCKING ggg successful with 000000-9ef438a458c53442ae7528a1f527939b.ggg; 1140910122.44367500 CALL pWrite() to write to myfile.txt in 'a+' mode; 1140910122.51507600 UNLOCKING ggg: removed 000000-9ef438a458c53442ae7528a1f527939b.ggg; 1140910122.51543100 END qWrite(): pWrite() successful. 1140910122.51566900 2006.02.25 23:28:42 1140910122.43453000-28-b5d99cef254fcf60382287aee47a7f62 written to myfile.txt.
or with temporary .rep directory removal,
1140910119.97764900 START qCheckR() for rep: 1140910120.16250500 Instance 90345c18abf3a0cb677f087e6f99d305 detected as primary. 1140910120.16279000 Ignoring user abort... 1140910120.16301600 Unsetting time limit... 1140910120.16319900 START qLockDR() for rep; 1140910120.16440800 remove expired locking directory ./aa6466d74e05c2a9ea5b6d6fc299a61a.rep; 1140910120.16490300 remove expired locking directory ./ace47eaf22152a6b0efb423b8a4f0655.rep; 1140910120.16537500 remove expired locking directory ./b1c039bd4ee9a6c088c8015f9e01a9fe.rep; 1140910120.16588000 remove expired locking directory ./b5fcbc8d3084b22ad75106535f93556c.rep; 1140910120.16632600 remove expired locking directory ./bfe93500978a876236dfc40e0dd1968c.rep; 1140910120.16676800 remove expired locking directory ./c7d97c599959e2d8469b5dc1d1912c7a.rep; 1140910120.16722000 remove expired locking directory ./d91879eeb87ab1c11189212a13f4c6ca.rep; 1140910120.16767000 remove expired locking directory ./d92590c8e3263619150ad5ead996aaec.rep; 1140910120.16811800 remove expired locking directory ./db0a82f079d5015347e6745be503cc01.rep; 1140910120.16856200 remove expired locking directory ./dc4306b89ce58d1bac9692e3d1fae368.rep; 1140910120.16900700 remove expired locking directory ./de7066cf5f2548919e347e176034ce32.rep; 1140910120.16950200 remove expired locking directory ./df4e2cbb977adaeb031587f044fec838.rep; 1140910120.16996700 remove expired locking directory ./e4c51854b7c9016d8d0c6b9bfe6cb550.rep; 1140910120.17043100 remove expired locking directory ./eafb75ec193e662c2e15ce84463002f5.rep; 1140910120.17591700 remove expired locking directory ./ed7f62bf3676abedba312e5befd128d0.rep; 1140910120.17637500 remove expired locking directory ./f6911fdc9f37992b94d8f9d565807f64.rep; 1140910120.17660300 START qLockDR() for ggg; 1140910120.17697200 START qWrite() to myfile.txt; 1140910120.18370400 CALL qLock() to lock ggg; 1140910120.18405700 START qLock() to lock ggg, maximum waiting time 5 seconds; 1140910120.18473300 approximate n = 0 : 000000; 1140910120.18529000 refreshed n = 1 : 000001; 1140910120.18606100 000001-98dbc408a4edb0d1730e43c2bd48acf0.ggg locking directory created; 1140910120.18654000 000001-98dbc408a4edb0d1730e43c2bd48acf0.ggg is the only locking directory with 1; 1140910120.18703800 1 locking directory have precedence; 1140910120.18757500 1 locking directory have precedence; 1140910120.18804300 1 locking directory have precedence; 1140910120.18850000 1 locking directory have precedence; 1140910120.18895300 1 locking directory have precedence; 1140910120.18940500 1 locking directory have precedence; 1140910120.18985600 1 locking directory have precedence; 1140910120.19030600 1 locking directory have precedence; 1140910120.19075200 1 locking directory have precedence; 1140910120.19120200 1 locking directory have precedence; 1140910120.19165200 1 locking directory have precedence; 1140910120.19210200 1 locking directory have precedence; 1140910120.19255100 1 locking directory have precedence; 1140910120.19300100 1 locking directory have precedence; 1140910120.19357200 1 locking directory have precedence; 1140910120.25035000 0 locking directory have precedence; 1140910120.25073300 END qLock() to lock ggg: OK. 1140910120.25102000 TRIED qLock() 1 times; 1140910120.25128200 LOCKING ggg successful with 000001-98dbc408a4edb0d1730e43c2bd48acf0.ggg; 1140910120.25156100 CALL pWrite() to write to myfile.txt in 'a+' mode; 1140910120.25332400 UNLOCKING ggg: removed 000001-98dbc408a4edb0d1730e43c2bd48acf0.ggg; 1140910120.31363200 END qWrite(): pWrite() successful. 1140910120.31399000 2006.02.25 23:28:39 1140910119.97721200-42-706dfcd1dd71417c73af06647539828c written to myfile.txt.
that might also contain lines such as
1140013025.88323600 START qLockDR() for ggg; 1140013025.98632900 remove expired locking directory ./000034-3e1db47d2ea72612d7b1cb515b8c3614.47785e50441515d950c28ca36e8670a1.ggg; 1140013026.06133000 remove expired locking directory ./000035-38f628f3980b3be4375b01b22bbe8c2a.77e8f82e44b9f40c87e7a28654dddaf7.ggg;
displaying expired locking directory removal, or
1140910120.00514400 START qCheckR() for rep: 1140910120.20350600 Instance bfbe3d332b296b1099bffe829232aa04 detected as primary. 1140910120.20379900 Ignoring user abort... 1140910120.20402000 Unsetting time limit... 1140910120.20420000 START qLockDR() for rep; 1140910120.20488600 START qLockDR() for ggg; 1140910120.20529700 START qWrite() to myfile.txt; 1140910120.25375500 CALL qLock() to lock ggg; 1140910120.25412100 START qLock() to lock ggg, maximum waiting time 5 seconds; 1140910120.25487600 approximate n = 5 : 000005; 1140910120.25530100 refreshed n = 5 : 000005; 1140910120.25609900 000005-ee9ed4374a8d87ee29233b110fbc22c9.ggg locking directory created; 1140910120.25660300 000005-ee9ed4374a8d87ee29233b110fbc22c9.ggg is the only locking directory with 5; 1140910120.25717700 3 locking directory have precedence; 1140910120.25778200 3 locking directory have precedence; 1140910120.25831500 3 locking directory have precedence; 1140910120.25883600 3 locking directory have precedence; 1140910120.25935600 3 locking directory have precedence; 1140910120.25987300 3 locking directory have precedence; 1140910120.26039000 3 locking directory have precedence; 1140910120.26090400 3 locking directory have precedence; 1140910120.26141800 3 locking directory have precedence; 1140910120.26193500 3 locking directory have precedence; 1140910120.26244900 3 locking directory have precedence; 1140910120.26296500 3 locking directory have precedence; 1140910120.26351300 3 locking directory have precedence; 1140910120.32005200 2 locking directory have precedence; 1140910120.32085800 2 locking directory have precedence; 1140910120.32145200 2 locking directory have precedence; 1140910120.32200400 2 locking directory have precedence; 1140910120.32255300 2 locking directory have precedence; 1140910120.32309600 2 locking directory have precedence; 1140910120.32367200 2 locking directory have precedence; 1140910120.35415500 1 locking directory have precedence; 1140910120.35474100 1 locking directory have precedence; 1140910120.35525900 1 locking directory have precedence; 1140910120.35577400 1 locking directory have precedence; 1140910120.35628800 1 locking directory have precedence; 1140910120.35680300 1 locking directory have precedence; 1140910120.35731800 1 locking directory have precedence; 1140910120.35783300 1 locking directory have precedence; 1140910120.35834900 1 locking directory have precedence; 1140910120.35905400 1 locking directory have precedence; 1140910120.35967200 1 locking directory have precedence; 1140910120.36020000 1 locking directory have precedence; 1140910120.36071900 1 locking directory have precedence; 1140910120.36123600 1 locking directory have precedence; 1140910120.36175200 1 locking directory have precedence; 1140910120.36226700 1 locking directory have precedence; 1140910120.36278200 1 locking directory have precedence; 1140910120.36329800 1 locking directory have precedence; 1140910120.38396000 0 locking directory have precedence; 1140910120.38432100 END qLock() to lock ggg: OK. 1140910120.38460500 TRIED qLock() 1 times; 1140910120.38486800 LOCKING ggg successful with 000005-ee9ed4374a8d87ee29233b110fbc22c9.ggg; 1140910120.38514100 CALL pWrite() to write to myfile.txt in 'a+' mode; 1140910120.38681600 UNLOCKING ggg: removed 000005-ee9ed4374a8d87ee29233b110fbc22c9.ggg; 1140910120.38715400 END qWrite(): pWrite() successful. 1140910120.38739600 2006.02.25 23:28:40 1140910120.00469800-15-7030f109f361f72378bd4d1f5dd3142e written to myfile.txt.
corresponding to different operations performed by qCheckR(), qLockDR(), qLock() and qWrite(). One can read if .rep temporary directories and undeleted locking directories are removed, see how many running instances have precedence over the current one, what number it takes in the queue and the name of the related locking directory, and how many times qLock() is executed before success (under normal conditions one time is enough to acquire a lock).
Each operation comes with its timecode to see when it occurs in relation to other operations performed by other instances. Usually instances don't run on the server one at a time, but operations pertaining to different instances are interlaced, as shown in the hand-compiled sample.log.txt log included with the package.
A sample myfile.txt (spaces between lines have been removed):
2006.02.25 23:28:39 1140910119.99202300-41-b852b5a1f4664f9122465ec9bc34dd4a 2006.02.25 23:28:39 1140910119.97721200-42-706dfcd1dd71417c73af06647539828c 2006.02.25 23:28:40 1140910120.03393300-21-bf4d7fe7687cfa2b2e89d4199283d64d 2006.02.25 23:28:40 1140910120.03864700-24-35657cd82ffa97f2e78da1e613e06f6f 2006.02.25 23:28:40 1140910120.04149400-29-ed328c0e20c941044f0c44b48d719bf3 2006.02.25 23:28:40 1140910120.00469800-15-7030f109f361f72378bd4d1f5dd3142e 2006.02.25 23:28:39 1140910119.96416600-20-1756bf42e49a8d80224742ef8e0ab2f7 2006.02.25 23:28:39 1140910119.96640300-11-0b6c05048951df1d6a6fbf5d0e33a7f2 2006.02.25 23:28:42 1140910122.35962000-31-8e346ab5c671ce2a59c7a58a4775ed48 2006.02.25 23:28:42 1140910122.43453000-28-b5d99cef254fcf60382287aee47a7f62
Each row added to myfile.txt contains a portion of the related log file name, to match with the instance that created it.
If you would like a custom license applied to you, feel free to ask for.
qCheckR(), qLockDR(), qLock(), qWrite(), pWrite() rev. 06.02.24
(c) 2006 Gatto Selvaggio, wildcat at insulae dot net. All rights reserved.
The aforementioned functions defined in this file, and their copies and modifications, collectively referred as "this/the software" in the following, are subject to a Creative Commons GNU LGPL License - CC-Gnu LGPL.
As a remuneration, I ask every user of the software to register to provide me contact information including a valid, updated email address. Please read here for information about what and why.
No liability. This software is distributed in the hope it may be useful; however, it is provided "as is" and any expressed or implied warranties are disclaimed, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. In no event shall I or my contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damage (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. You use the software at your own risk.
If any part of this license is held invalid or unenforceable, that portion shall be construed in a manner consistent with applicable law to reflect, as nearly as possible, the original intentions of the owner of this software, and the remaining portions shall remain in full force and effect.
(c) 2006 Gatto Selvaggio. The license for this documentation is a Creative Commons Attribution-NonCommercial-ShareAlike, chosen to allow developers to derive documentation for derived works.
Please note that the code is distributed under the license detailed above, different from the license for the documentation.
The digital signature for this software is 9b101fa42f087595f1e7c19ec73fac6d.
2006.04.19 Some notes added about flock() and its usage.
2006.04.01 Documentation finished.
2006.02.26 Documentation started.
2006.02.24 QLCK working release, written and tested.
540 hits since May 2, 2010 — Made with MetaPad and a perpetually evolving doc.css style sheet.