How to avoid race hazard with multiple requests? - php

In order to protect script form race hazard, I am considering approach described by code sample
$file = 'yxz.lockctrl';
// if file exists, it means that some other request is running
while (file_exists($file))
{
sleep(1);
}
file_put_contents($file, '');
// do some work
unlink($file);
If I go this way, is it possible to create file with same name simultaneously from multiple requests?
I know that there is php mutex. I would like to handle this situation without any extensions (if possible).
Task for the program is to handle bids in auctions application. I would like to process every bid request sequentially. With most possible latency.

From what I understand you want to make sure only a single process at a time is running a certain piece of code. A mutex or similar mechanism could be used for this. I myself use lockfiles to have a solution that works on many platforms and doesn't rely on a specific library only available on Linux etc.
For that, I have written a small Lock class. Do note that it uses some non-standard functions from my library, for instance, to get where to store temporary files etc. But you could easily change that.
<?php
class Lock
{
private $_owned = false;
private $_name = null;
private $_lockFile = null;
private $_lockFilePointer = null;
public function __construct($name)
{
$this->_name = $name;
$this->_lockFile = PluginManager::getInstance()->getCorePlugin()->getTempDir('locks') . $name . '-' . sha1($name . PluginManager::getInstance()->getCorePlugin()->getPreference('EncryptionKey')->getValue()).'.lock';
}
public function __destruct()
{
$this->release();
}
/**
* Acquires a lock
*
* Returns true on success and false on failure.
* Could be told to wait (block) and if so for a max amount of seconds or return false right away.
*
* #param bool $wait
* #param null $maxWaitTime
* #return bool
* #throws \Exception
*/
public function acquire($wait = false, $maxWaitTime = null) {
$this->_lockFilePointer = fopen($this->_lockFile, 'c');
if(!$this->_lockFilePointer) {
throw new \RuntimeException(__('Unable to create lock file', 'dliCore'));
}
if($wait && $maxWaitTime === null) {
$flags = LOCK_EX;
}
else {
$flags = LOCK_EX | LOCK_NB;
}
$startTime = time();
while(1) {
if (flock($this->_lockFilePointer, $flags)) {
$this->_owned = true;
return true;
} else {
if($maxWaitTime === null || time() - $startTime > $maxWaitTime) {
fclose($this->_lockFilePointer);
return false;
}
sleep(1);
}
}
}
/**
* Releases the lock
*/
public function release()
{
if($this->_owned) {
#flock($this->_lockFilePointer, LOCK_UN);
#fclose($this->_lockFilePointer);
#unlink($this->_lockFile);
$this->_owned = false;
}
}
}
Usage
Now you can have two process that run at the same time and execute the same script
Process 1
$lock = new Lock('runExpensiveFunction');
if($lock->acquire()) {
// Some expensive function that should only run one at a time
runExpensiveFunction();
$lock->release();
}
Process 2
$lock = new Lock('runExpensiveFunction');
// Check will be false since the lock will already be held by someone else so the function is skipped
if($lock->acquire()) {
// Some expensive function that should only run one at a time
runExpensiveFunction();
$lock->release();
}
Another alternative would be to have the second process wait for the first one to finish instead of skipping the code.
$lock = new Lock('runExpensiveFunction');
// Process will now wait for the lock to become available. A max wait time can be set if needed.
if($lock->acquire(true)) {
// Some expensive function that should only run one at a time
runExpensiveFunction();
$lock->release();
}
Ram disk
To limit the number of writes to your HDD/SSD with the lockfiles you could crate a RAM disk to store them in.
On Linux you could add something like the following to /etc/fstab
tmpfs /mnt/ramdisk tmpfs nodev,nosuid,noexec,nodiratime,size=1024M 0 0
On Windows you can download something like ImDisk Toolkit and create a ramdisk with that.

Related

PHP cURL Timing Issue

I have a PHP script that is used to query an API and download some JSON information / insert that information into a MySQL database, we'll call this scriptA.php. I need to run this script multiple times as minute, preferably as many times in a minute that I can without allowing two instances to run at the same exact time or with any overlap. My solution to this has been to create scriptB.php and put in on a one minute cron job. Here is the source code of scriptB.php...
function next_run()
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, "http://somewebsite.com/scriptA.php");
curl_exec($curl);
curl_close($curl);
unset($curl);
}
$i = 0;
$times_to_run = 7;
$function = array();
while ($i++ < $times_to_run) {
$function = next_run();
sleep(3);
}
My question at this point is to how cURL performs when used in a loop, does this code trigger scriptA.php and THEN once it has finished loading it at that point start the next cURL request? Does the 3 second sleep even make a difference or will this literally run as fast as the time it takes each cURL request to complete. My objective is to time this script and run it as many times as possible in a one minute window without two iterations of it being run at the same time. I don't want to include the sleep statement if it is not needed. I believe what happens is cURL will run each request upon finishing the last, if I am wrong is there someway that I can instruct it to do this?
preferably as many times in a minute that I can without allowing two instances to run at the same exact time or with any overlap. - then you shouldn't use a cronjob at all, you should use a daemon. but if you for some reason have to use a cronjob (eg, if you're on a shared webhosting platform that doesn't allow daemons), guess you could use the sleep hack to run the same code several times a minute?
* * * * * /usr/bin/php /path/to/scriptA.php
* * * * * sleep 10; /usr/bin/php /path/to/scriptA.php
* * * * * sleep 20; /usr/bin/php /path/to/scriptA.php
* * * * * sleep 30; /usr/bin/php /path/to/scriptA.php
* * * * * sleep 40; /usr/bin/php /path/to/scriptA.php
* * * * * sleep 50; /usr/bin/php /path/to/scriptA.php
should make it execute every 10 seconds.
as for making sure it doesn't run in paralell if the previous execution hasn't finished yet, add this to the start of scriptA
call_user_func ( function () {
static $lock;
$lock = fopen ( __FILE__, "rb" );
if (! flock ( $lock, LOCK_EX | LOCK_NB )) {
// failed to get a lock, probably means another instance is already running
die ();
}
register_shutdown_function ( function () use (&$lock) {
flock ( $lock, LOCK_UN );
} );
} );
and it will just die() if another instance of scriptA is already running. however, if you want it to wait for the previous execution to finish, instead of just exiting, remove LOCK_NB... but that could be dangerous, if every, or even just a majority of the executions use more than 10 seconds, you'll have more and more processes waiting for the previous execution to finish, until you run out of ram.
as for your curl questions,
My question at this point is to how cURL performs when used in a loop, does this code trigger scriptA.php and THEN once it has finished loading it at that point start the next cURL request, that is correct, curl waits until the page has completely loaded, usually meaning the entire scriptA has completed. (you can tell scriptA to finish the pageload prematurely with the fastcgi_finish_request() function if you really want, but that's unusual)
Does the 3 second sleep even make a difference or will this literally run as fast as the time it takes each cURL request to complete - yes, the sleep will make the loop 3 seconds slower per iteration.
My objective is to time this script and run it as many times as possible in a one minute window without two iterations of it being run at the same time - then make it a daemon that never exits, rather than a cronjob.
I don't want to include the sleep statement if it is not needed. - it's not needed.
I believe what happens is cURL will run each request upon finishing the last - this is correct.
I need to run this script multiple times as minute, preferably as many times in a minute that I can without allowing two instances to run
Your in luck as I wrote a class to handle just such a thing. You can find it on my github here
https://github.com/ArtisticPhoenix/MISC/blob/master/ProcLock.php
I'll also copy the full code at the end of this post.
The basic idea is to create a file, I will call it afile.lock for this example. In this file is recorded the PID, or the process ID of the current process that is ran by cron. Then when cron attempts to run the process again, it checks this lock file and sees if there is a PHP process running that is using this PID.
if there is it updates the modified time of the file (and throws an exception)
if there is not then you are free to create a new instance of the "worker".
As a bonus th modified time of the lock file can be used by the script (whose PID we are tracking) as a way of shutting down in the event the file is not updated, so for example: if cron is stopped, or if the lock file is manually deleted you can set in in such a way that the running script will detect this and self destruct.
So not only can you keep multiple instances from running, you can tell the current instance to die if cron is turned off.
The basic usage is as follows. In the cron file that starts up the "worker"
//define a lock file (this is actually optional)
ProcLock::setLockFile(__DIR__.'/afile.lock');
try{
//if you didn't set a lock file you can pass it in with this method call
ProcLock::lock();
//execute your process
}catch(\Exception $e){
if($e->getCode() == ProcLock::ALREADY_LOCKED){
//just exit or what have you
}else{
//some other exception happened.
}
}
It's basically that easy.
Then in the running process you can every so often check (for example if you have a loop that runs something)
$expires = 90; //1 1/2 minute (you may need a bit of fudge time)
foreach($something as $a=>$b){
$lastAccess = ProcLock::getLastAccess()
if(false == $lastAccess || $lastAccess + $expires < time()){
//if last access is false (no lock file)
//or last access + expiration, is less then the current time
//log something like killed by lock timeout
exit();
}
}
Basically what this says is that either the lock file was deleted wile the process was running, or cron failed to update it before the expiration time. So here we are giving it 90 seconds and cron should be updating the lock file every 60 seconds. As I said the lock file is updated automatically if it's found when calling lock(), which calls canLock() which if it returns true meaning we can lock the process because its not currently locked, then it runs touch($lockfile) which updates the mtime (modified time).
Obviously you can only self kill the process in this way if it is actively checking the access and expiration times.
This script is designed to work both on windows and linux. On windows under certain circumstances the lock file won't properly be deleted (sometimes when hitting ctrl+c in the CMD window), however I have taken great pains to make sure this does not happen, so the class file contains a custom register_shutdown_function that runs when the PHP script ends.
When running something using the ProcLoc in the browser please note that the process id will always be the same no matter the tab its ran in. So if you open one tab that is Process locked, then open another tab, the process locker will see it as the same process and allow it to lock again. To properly run it in a browser and test the locking it must be done using two separate browsers such as crome and firefox. It's not really intended to be ran in the browser but this is one quirk I noticed.
One last note this class is completely static, as you can have only one Process ID per process that is running, which should be obvious.
The tricky parts are
making sure the lock file is disposed of in the event of even critical PHP failures
making sure another process didn't pick up the pid number when it was freed from PHP. This can be done with relative accuracy, in that we can tell if a PHP process is using it, and if so we assume its the process we need, there is much less chance a re-used PID would show up for another process very quickly, even less that it would be another PHP process
making all this work on both Linux and Windows
Lucky for you I have already invested sufficient time in this to do all these things, this is a more generic version of an original lock script I made for my job that we have used in this way successfully for 3 years in maintaining control over various synchronous cron jobs, everything from sFTP upload scanning, expired file clean up to RabbitMq message workers that run for an indefinite period of time.
In anycase here is the full code, enjoy.
<?php
/*
(c) 2017 ArtisticPhoenix
For license information please view the LICENSE file included with this source code GPL3.0.
Proccess Locker
==================================================================
This is a pseudo implementation of mutex since php does not have
any thread synchronization objects
This class uses files to provide locking functionality.
Lock will be released in following cases
1 - user calls unlock
2 - when this lock object gets deleted
3 - when request or script ends
4 - when pid of lock does not match self::$_pid
==================================================================
Only one Lock per Process!
-note- when running in a browser typically all tabs will have the same PID
so the locking will not be able to tell if it's the same process, to get
around this run in CLI, or use 2 diffrent browsers, so the PID numbers are diffrent.
This class is static for the simple fact that locking is done per-proces, so there is no need
to ever have duplate ProcLocks within the same process
---------------------------------------------------------------
*/
final class {
/**
* exception code numbers
* #var int
*/
const DIRECTORY_NOT_FOUND = 2000;
const LOCK_FIRST = 2001;
const FAILED_TO_UNLOCK = 2002;
const FAILED_TO_LOCK = 2003;
const ALREADY_LOCKED = 2004;
const UNKNOWN_PID = 2005;
const PROC_UNKNOWN_PID = 2006;
/**
* process _key
* #var string
*/
protected static $_lockFile;
/**
*
* #var int
*/
protected static $_pid;
/**
* No construction allowed
*/
private function __construct(){}
/**
* No clones allowed
*/
private function __clone(){}
/**
* globaly sets the lock file
* #param string $lockFile
*/
public static function setLockFile( $lockFile ){
$dir = dirname( $lockFile );
if( !is_dir( dirname( $lockFile ))){
throw new Exception("Directory {$dir} not found", self::DIRECTORY_NOT_FOUND); //pid directroy invalid
}
self::$_lockFile = $lockFile;
}
/**
* return global lockfile
*/
public static function getLockFile() {
return ( self::$_lockFile ) ? self::$_lockFile : false;
}
/**
* safe check for local or global lock file
*/
protected static function _chk_lock_file( $lockFile = null ){
if( !$lockFile && !self::$_lockFile ){
throw new Exception("Lock first", self::LOCK_FIRST); //
}elseif( $lockFile ){
return $lockFile;
}else{
return self::$_lockFile;
}
}
/**
*
* #param string $lockFile
*/
public static function unlock( $lockFile = null ){
if( !self::$_pid ){
//no pid stored - not locked for this process
return;
}
$lockFile = self::_chk_lock_file($lockFile);
if(!file_exists($lockFile) || unlink($lockFile)){
return true;
}else{
throw new Exception("Failed to unlock {$lockFile}", self::FAILED_TO_UNLOCK ); //no lock file exists to unlock or no permissions to delete file
}
}
/**
*
* #param string $lockFile
*/
public static function lock( $lockFile = null ){
$lockFile = self::_chk_lock_file($lockFile);
if( self::canLock( $lockFile )){
self::$_pid = getmypid();
if(!file_put_contents($lockFile, self::$_pid ) ){
throw new Exception("Failed to lock {$lockFile}", self::FAILED_TO_LOCK ); //no permission to create pid file
}
}else{
throw new Exception('Process is already running[ '.$lockFile.' ]', self::ALREADY_LOCKED );//there is a process running with this pid
}
}
/**
*
* #param string $lockFile
*/
public static function getPidFromLockFile( $lockFile = null ){
$lockFile = self::_chk_lock_file($lockFile);
if(!file_exists($lockFile) || !is_file($lockFile)){
return false;
}
$pid = file_get_contents($lockFile);
return intval(trim($pid));
}
/**
*
* #return number
*/
public static function getMyPid(){
return ( self::$_pid ) ? self::$_pid : false;
}
/**
*
* #param string $lockFile
* #param string $myPid
* #throws Exception
*/
public static function validatePid($lockFile = null, $myPid = false ){
$lockFile = self::_chk_lock_file($lockFile);
if( !self::$_pid && !$myPid ){
throw new Exception('no pid supplied', self::UNKNOWN_PID ); //no stored or injected pid number
}elseif( !$myPid ){
$myPid = self::$_pid;
}
return ( $myPid == self::getPidFromLockFile( $lockFile ));
}
/**
* update the mtime of lock file
* #param string $lockFile
*/
public static function canLock( $lockFile = null){
if( self::$_pid ){
throw new Exception("Process was already locked", self::ALREADY_LOCKED ); //process was already locked - call this only before locking
}
$lockFile = self::_chk_lock_file($lockFile);
$pid = self::getPidFromLockFile( $lockFile );
if( !$pid ){
//if there is a not a pid then there is no lock file and it's ok to lock it
return true;
}
//validate the pid in the existing file
$valid = self::_validateProcess($pid);
if( !$valid ){
//if it's not valid - delete the lock file
if(unlink($lockFile)){
return true;
}else{
throw new Exception("Failed to unlock {$lockFile}", self::FAILED_TO_UNLOCK ); //no lock file exists to unlock or no permissions to delete file
}
}
//if there was a valid process running return false, we cannot lock it.
//update the lock files mTime - this is usefull for a heartbeat, a periodic keepalive script.
touch($lockFile);
return false;
}
/**
*
* #param string $lockFile
*/
public static function getLastAccess( $lockFile = null ){
$lockFile = self::_chk_lock_file($lockFile);
clearstatcache( $lockFile );
if( file_exists( $lockFile )){
return filemtime( $lockFile );
}
return false;
}
/**
*
* #param int $pid
*/
protected static function _validateProcess( $pid ){
$task = false;
$pid = intval($pid);
if(stripos(php_uname('s'), 'win') > -1){
$task = shell_exec("tasklist /fi \"PID eq {$pid}\"");
/*
'INFO: No tasks are running which match the specified criteria.
'
*/
/*
'
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
php.exe 5064 Console 1 64,516 K
'
*/
}else{
$cmd = "ps ".intval($pid);
$task = shell_exec($cmd);
/*
' PID TTY STAT TIME COMMAND
'
*/
}
//print_rr( $task );
if($task){
return ( preg_match('/php|httpd/', $task) ) ? true : false;
}
throw new Exception("pid detection failed {$pid}", self::PROC_UNKNOWN_PID); //failed to parse the pid look up results
//this has been tested on CentOs 5,6,7 and windows 7 and 10
}
/**
* destroy a lock ( safe unlock )
*/
public static function destroy($lockFile = null){
try{
$lockFile = self::_chk_lock_file($lockFile);
self::unlock( $lockFile );
}catch( Exception $e ){
//ignore errors here - this called from distruction so we dont care if it fails or succeeds
//generally a new process will be able to tell if the pid is still in use so
//this is just a cleanup process
}
}
}
/*
* register our shutdown handler - if the script dies unlock the lock
* this is superior to __destruct(), because the shutdown handler runs even in situation where PHP exhausts all memory
*/
register_shutdown_function(array('\\Lib\\Queue\\ProcLock',"destroy"));

php mutex for ram based wordpress cache in php

Im trying to implement a cache for a high traffic wp site in php. so far ive managed to store the results to a ramfs and load them directly from the htaccess. however during peak hours there are mora than one process generatin certain page and is becoming an issue
i was thinking that a mutex would help and i was wondering if there is a better way than system("mkdir cache.mutex")
From what I understand you want to make sure only a single process at a time is running a certain piece of code. A mutex or similar mechanism could be used for this. I myself use lockfiles to have a solution that works on many platforms and doesn't rely on a specific library only available on Linux etc.
For that, I have written a small Lock class. Do note that it uses some non-standard functions from my library, for instance, to get where to store temporary files etc. But you could easily change that.
<?php
class Lock
{
private $_owned = false;
private $_name = null;
private $_lockFile = null;
private $_lockFilePointer = null;
public function __construct($name)
{
$this->_name = $name;
$this->_lockFile = PluginManager::getInstance()->getCorePlugin()->getTempDir('locks') . $name . '-' . sha1($name . PluginManager::getInstance()->getCorePlugin()->getPreference('EncryptionKey')->getValue()).'.lock';
}
public function __destruct()
{
$this->release();
}
/**
* Acquires a lock
*
* Returns true on success and false on failure.
* Could be told to wait (block) and if so for a max amount of seconds or return false right away.
*
* #param bool $wait
* #param null $maxWaitTime
* #return bool
* #throws \Exception
*/
public function acquire($wait = false, $maxWaitTime = null) {
$this->_lockFilePointer = fopen($this->_lockFile, 'c');
if(!$this->_lockFilePointer) {
throw new \RuntimeException(__('Unable to create lock file', 'dliCore'));
}
if($wait && $maxWaitTime === null) {
$flags = LOCK_EX;
}
else {
$flags = LOCK_EX | LOCK_NB;
}
$startTime = time();
while(1) {
if (flock($this->_lockFilePointer, $flags)) {
$this->_owned = true;
return true;
} else {
if($maxWaitTime === null || time() - $startTime > $maxWaitTime) {
fclose($this->_lockFilePointer);
return false;
}
sleep(1);
}
}
}
/**
* Releases the lock
*/
public function release()
{
if($this->_owned) {
#flock($this->_lockFilePointer, LOCK_UN);
#fclose($this->_lockFilePointer);
#unlink($this->_lockFile);
$this->_owned = false;
}
}
}
Usage
Now you can have two process that run at the same time and execute the same script
Process 1
$lock = new Lock('runExpensiveFunction');
if($lock->acquire()) {
// Some expensive function that should only run one at a time
runExpensiveFunction();
$lock->release();
}
Process 2
$lock = new Lock('runExpensiveFunction');
// Check will be false since the lock will already be held by someone else so the function is skipped
if($lock->acquire()) {
// Some expensive function that should only run one at a time
runExpensiveFunction();
$lock->release();
}
Another alternative would be to have the second process wait for the first one to finish instead of skipping the code.
$lock = new Lock('runExpensiveFunction');
// Process will now wait for the lock to become available. A max wait time can be set if needed.
if($lock->acquire(true)) {
// Some expensive function that should only run one at a time
runExpensiveFunction();
$lock->release();
}
Ram disk
To limit the number of writes to your HDD/SSD with the lockfiles you could create a RAM disk to store them in.
On Linux you could add something like the following to /etc/fstab
tmpfs /mnt/ramdisk tmpfs nodev,nosuid,noexec,nodiratime,size=1024M 0 0
On Windows you can download something like ImDisk Toolkit and create a ramdisk with that.
I agree with #gries, a reverse proxy is going to be a really good bang-for-the-buck way to get high performance out of a high-volume Wordpress site. I've leveraged Varnish with quite a lot of success, though I suspect you can do so with nginx as well.

Lock process in symfony 1.4

I need a way to prevent multiple run on a process on symfony 1.4.
Something like: when a user is running this process, the other user who tries to run this process will get a warning message inform that the process is running.
Is there a way to implement it without using database?
kirugan's method will probably work in most cases, but it's susceptible to race conditions and can get stuck in the event of a crash.
Here's a more robust solution. It uses PHP's file locking so you know the lock is atomic, and if you forget to release the lock later or your process crashes, it gets released automatically. By default, getting a lock is non-blocking (i.e. if the lock is already held by another process, getLock() will instantly return FALSE). However, you can have the call block (i.e. wait until the lock becomes available) if you'd like. Finally, you can have different locks for different parts of your code. Just use a different name for the lock.
The only requirement is that the directory returned by getLockDir() must be server-writable. Feel free to change the location of the lock dir.
Note: I think flock() may behave differently on Windows (I use linux), so double check that if its an issue for you.
myLock.class.php
class myLock
{
/**
* Creates a lockfile and acquires an exclusive lock on it.
*
* #param string $filename The name of the lockfile.
* #param boolean $blocking Block until lock becomes available (default: don't block, just fail)
* #return mixed Returns the lockfile, or FALSE if a lock could not be acquired.
*/
public static function getLock($name, $blocking = false)
{
$filename = static::getLockDir() . '/' . $name;
if (!preg_match('/\.lo?ck$/', $filename))
{
$filename .= '.lck';
}
if (!file_exists($filename))
{
file_put_contents($filename, '');
chmod($filename, 0777); // if the file cant be opened for writing later, getting the lock will fail
}
$lockFile = fopen($filename, 'w+');
if (!flock($lockFile, $blocking ? LOCK_EX : LOCK_EX|LOCK_NB))
{
fclose($lockFile);
return false;
}
return $lockFile;
}
/**
* Free a lock.
*
* #param resource $lockFile
*/
public static function freeLock($lockFile)
{
if ($lockFile)
{
flock($lockFile, LOCK_UN);
fclose($lockFile);
}
}
public static function getLockDir()
{
return sfConfig::get('sf_root_dir') . '/data/lock';
}
}
How to use
$lockFile = myLock::getLock('lock-name');
if ($lockFile) {
// you have a lock here, do whatever you want
myLock::freeLock($lockFile);
}
else {
// you could not get the lock. show a message or throw an exception or whatever
}
Here is my functions for this:
function lock(){
$file = __DIR__ . '/file.lock';
if(file_exists($file)){
/* exit or whatever you want */
die('ALREADY LOCKED');
}
touch($file);
}
function unlock(){
$file = __DIR__ . '/parser.lock';
if(file_exists($file)){
unlink($file);
}else{
echoln("unlock function: LOCK FILE NOT FOUND");
}
}
function exitHandler(){
echoln('exitHandler function: called');
unlock();
}
Use lock function in the beginning, and set exitHandler function in register_shutdown_function(). You can save this snippets as class with the same methods or save it like helper file

PHP - Preventing collision in Cron - File lock safe?

I'm trying to find a safe way to prevent a cron job collision (ie. prevent it from running if another instance is already running).
Some options I've found recommend using a lock on a file.
Is that really a safe option? What would happen if the script dies for example? Will the lock remain?
Are there other ways of doing this?
This sample was taken at http://php.net/flock and changed a little and this is a correct way to do what you want:
$fp = fopen("/path/to/lock/file", "w+");
if (flock($fp, LOCK_EX | LOCK_NB)) { // do an exclusive lock
// do the work
flock($fp, LOCK_UN); // release the lock
} else {
echo "Couldn't get the lock!";
}
fclose($fp);
Do not use locations such as /tmp or /var/tmp as they could be cleaned up at any time by your system, thus messing with your lock as per the docs:
Programs must not assume that any files or directories in /tmp are preserved between invocations of the program.
https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s18.html
https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch05s15.html
Do use a location that is under your control.
Credits:
Michaƫl Perrin - for proposing to use w+ instead of r+
In Symfony Framework you could use the lock component symfony/lock
https://symfony.com/doc/current/console/lockable_trait.html
I've extended the concept from zerkms to create a function that can be called from the start of a cron.
Using the Cronlocker you specify a lock name, then the name of a callback function to be called if the cron is OFF. Optionally you may give an array of parameters to pass to the callback function. There's also an optional callback function if you need to do something different if the lock is ON.
In some cases I got a few exceptions and wanted to be able to trap them, and I added a function for handling fatal exceptions, which should be added. I wanted to be able to hit the file from a browser and bypass the cronlock, so that's built in.
I found as I used this a lot there were cases where I wanted to block other crons from running while this cron is running, so I added an optional array of lockblocks, which are other lock names to block.
Then there were cases where I wanted this cron to run after other crons had finished, so there's an optional array of lockwaits, which are other lock names to wait until none of which are running.
simple example:
Cronlocker::CronLock('cron1', 'RunThis');
function RunThis() {
echo('I ran!');
}
callback parameters and failure functions:
Cronlocker::CronLock('cron2', 'RunThat', ['ran'], 'ImLocked');
function RunThat($x) {
echo('I also ran! ' . $x);
}
function ImLocked($x) {
echo('I am locked :-( ' . $x);
}
blocking and waiting:
Cronlocker::CronLock('cron3', 'RunAgain', null, null, ['cron1'], ['cron2']);
function RunAgain() {
echo('I ran.<br />');
echo('I block cron1 while I am running.<br />')
echo('I wait for cron2 to finish if it is running.');
}
class:
class Cronlocker {
private static $LockFile = null;
private static $LockFileBlocks = [];
private static $LockFileWait = null;
private static function GetLockfileName($lockname) {
return "/tmp/lock-" . $lockname . ".txt";
}
/**
* Locks a PHP script from being executed more than once at a time
* #param string $lockname Use a unique lock name for each lock that needs to be applied.
* #param string $callback The name of the function to call if the lock is OFF
* #param array $callbackParams Optional array of parameters to apply to the callback function when called
* #param string $callbackFail Optional name of the function to call if the lock is ON
* #param string[] $lockblocks Optional array of locknames for other crons to also block while this cron is running
* #param string[] $lockwaits Optional array of locknames for other crons to wait until they finish running before this cron will run
* #see http://stackoverflow.com/questions/5428631/php-preventing-collision-in-cron-file-lock-safe
*/
public static function CronLock($lockname, $callback, $callbackParams = null, $callbackFail = null, $lockblocks = [], $lockwaits = []) {
// check all the crons we are waiting for to finish running
if (!empty($lockwaits)) {
$waitingOnCron = true;
while ($waitingOnCron) {
$waitingOnCron = false;
foreach ($lockwaits as $lockwait) {
self::$LockFileWait = null;
$tempfile = self::GetLockfileName($lockwait);
try {
self::$LockFileWait = fopen($tempfile, "w+");
} catch (Exception $e) {
//ignore error
}
if (flock(self::$LockFileWait, LOCK_EX | LOCK_NB)) { // do an exclusive lock
// cron we're waiting on isn't running
flock(self::$LockFileWait, LOCK_UN); // release the lock
} else {
// we're wating on a cron
$waitingOnCron = true;
}
if (is_resource(self::$LockFileWait))
fclose(self::$LockFileWait);
if ($waitingOnCron) break; // no need to check any more
}
if ($waitingOnCron) sleep(15); // wait a few seconds
}
}
// block any additional crons from starting
if (!empty($lockblocks)) {
self::$LockFileBlocks = [];
foreach ($lockblocks as $lockblock) {
$tempfile = self::GetLockfileName($lockblock);
try {
$block = fopen($tempfile, "w+");
} catch (Exception $e) {
//ignore error
}
if (flock($block, LOCK_EX | LOCK_NB)) { // do an exclusive lock
// lock made
self::$LockFileBlocks[] = $block;
} else {
// couldn't lock it, we ignore and move on
}
}
}
// set the cronlock
self::$LockFile = null;
$tempfile = self::GetLockfileName($lockname);
$return = null;
try {
if (file_exists($tempfile) && !is_writable($tempfile)) {
//assume we're hitting this from a browser and execute it regardless of the cronlock
if (empty($callbackParams))
$return = $callback();
else
$return = call_user_func_array($callback, $callbackParams);
} else {
self::$LockFile = fopen($tempfile, "w+");
}
} catch (Exception $e) {
//ignore error
}
if (!empty(self::$LockFile)) {
if (flock(self::$LockFile, LOCK_EX | LOCK_NB)) { // do an exclusive lock
// do the work
if (empty($callbackParams))
$return = $callback();
else
$return = call_user_func_array($callback, $callbackParams);
flock(self::$LockFile, LOCK_UN); // release the lock
} else {
// call the failed function
if (!empty($callbackFail)) {
if (empty($callbackParams))
$return = $callbackFail();
else
$return = call_user_func_array($callbackFail, $callbackParams);
}
}
if (is_resource(self::$LockFile))
fclose(self::$LockFile);
}
// remove any lockblocks
if (!empty($lockblocks)) {
foreach (self::$LockFileBlocks as $LockFileBlock) {
flock($LockFileBlock, LOCK_UN); // release the lock
if (is_resource($LockFileBlock))
fclose($LockFileBlock);
}
}
return $return;
}
/**
* Releases the Cron Lock locking file, useful to specify on fatal errors
*/
public static function ReleaseCronLock() {
// release the cronlock
if (!empty(self::$LockFile) && is_resource(self::$LockFile)) {
var_dump('Cronlock released after error encountered: ' . self::$LockFile);
flock(self::$LockFile, LOCK_UN);
fclose(self::$LockFile);
}
// release any lockblocks too
foreach (self::$LockFileBlocks as $LockFileBlock) {
if (!empty($LockFileBlock) && is_resource($LockFileBlock)) {
flock($LockFileBlock, LOCK_UN);
fclose($LockFileBlock);
}
}
}
}
Should also be implemented on a common page, or built into your existing fatal error handler:
function fatal_handler() {
// For cleaning up crons that fail
Cronlocker::ReleaseCronLock();
}
register_shutdown_function("fatal_handler");

best way to obtain a lock in php

I'm trying to update a variable in APC, and will be many processes trying to do that.
APC doesn't provide locking functionality, so I'm considering using other mechanisms... what I've found so far is mysql's GET_LOCK(), and php's flock(). Anything else worth considering?
Update: I've found sem_acquire, but it seems to be a blocking lock.
/*
CLASS ExclusiveLock
Description
==================================================================
This is a pseudo implementation of mutex since php does not have
any thread synchronization objects
This class uses flock() as a base to provide locking functionality.
Lock will be released in following cases
1 - user calls unlock
2 - when this lock object gets deleted
3 - when request or script ends
==================================================================
Usage:
//get the lock
$lock = new ExclusiveLock( "mylock" );
//lock
if( $lock->lock( ) == FALSE )
error("Locking failed");
//--
//Do your work here
//--
//unlock
$lock->unlock();
===================================================================
*/
class ExclusiveLock
{
protected $key = null; //user given value
protected $file = null; //resource to lock
protected $own = FALSE; //have we locked resource
function __construct( $key )
{
$this->key = $key;
//create a new resource or get exisitng with same key
$this->file = fopen("$key.lockfile", 'w+');
}
function __destruct()
{
if( $this->own == TRUE )
$this->unlock( );
}
function lock( )
{
if( !flock($this->file, LOCK_EX | LOCK_NB))
{ //failed
$key = $this->key;
error_log("ExclusiveLock::acquire_lock FAILED to acquire lock [$key]");
return FALSE;
}
ftruncate($this->file, 0); // truncate file
//write something to just help debugging
fwrite( $this->file, "Locked\n");
fflush( $this->file );
$this->own = TRUE;
return TRUE; // success
}
function unlock( )
{
$key = $this->key;
if( $this->own == TRUE )
{
if( !flock($this->file, LOCK_UN) )
{ //failed
error_log("ExclusiveLock::lock FAILED to release lock [$key]");
return FALSE;
}
ftruncate($this->file, 0); // truncate file
//write something to just help debugging
fwrite( $this->file, "Unlocked\n");
fflush( $this->file );
$this->own = FALSE;
}
else
{
error_log("ExclusiveLock::unlock called on [$key] but its not acquired by caller");
}
return TRUE; // success
}
};
You can use the apc_add function to achieve this without resorting to file systems or mysql. apc_add only succeeds when the variable is not already stored; thus, providing a mechanism of locking. TTL can be used to ensure that falied lockholders won't keep on holding the lock forever.
The reason apc_add is the correct solution is because it avoids the race condition that would otherwise exist between checking the lock and setting it to 'locked by you'. Since apc_add only sets the value if it's not already set ( "adds" it to the cache ), it ensures that the lock can't be aquired by two calls at once, regardless of their proximity in time. No solution that doesn't check and set the lock at the same time will inherently suffer from this race condition; one atomic operation is required to successfully lock without race condition.
Since APC locks will only exist in the context of that php execution, it's probably not the best solution for general locking, as it doesn't support locks between hosts. Memcache also provides an atomic add function and thus can also be used with this technique - which is one method of locking between hosts. Redis also supports atomic 'SETNX' functions and TTL, and is a very common method of locking and synchronization between hosts. Howerver, the OP requests a solution for APC in particular.
If the point of the lock is to prevent multiple processes from trying to populate an empty cache key, why wouldn't you want to have a blocking lock?
$value = apc_fetch($KEY);
if ($value === FALSE) {
shm_acquire($SEMAPHORE);
$recheck_value = apc_fetch($KEY);
if ($recheck_value !== FALSE) {
$new_value = expensive_operation();
apc_store($KEY, $new_value);
$value = $new_value;
} else {
$value = $recheck_value;
}
shm_release($SEMAPHORE);
}
If the cache is good, you just roll with it. If there's nothing in the cache, you get a lock. Once you have the lock, you'll need to double-check the cache to make sure that, while you were waiting to get the lock, the cache wasn't repopulated. If the cache was repopulated, use that value & release the lock, otherwise, you do the computation, populate the cache & then release your lock.
Actually, check to see if this will work better then Peter's suggestion.
http://us2.php.net/flock
use an exclusive lock and if your comfortable with it, put everything else that attempted to lock the file in a 2-3 second sleep. If done right your site will experience a hang regarding the locked resource but not a horde of scripts fighting to cache the samething.
If you don't mind basing your lock on the filesystem, then you could use fopen() with mode 'x'. Here is an example:
$f = fopen("lockFile.txt", 'x');
if($f) {
$me = getmypid();
$now = date('Y-m-d H:i:s');
fwrite($f, "Locked by $me at $now\n");
fclose($f);
doStuffInLock();
unlink("lockFile.txt"); // unlock
}
else {
echo "File is locked: " . file_get_contents("lockFile.txt");
exit;
}
See www.php.net/fopen
I realize this is a year old, but I just stumbled upon the question while doing some research myself on locking in PHP.
It occurs to me that a solution might be possible using APC itself. Call me crazy, but this might be a workable approach:
function acquire_lock($key, $expire=60) {
if (is_locked($key)) {
return null;
}
return apc_store($key, true, $expire);
}
function release_lock($key) {
if (!is_locked($key)) {
return null;
}
return apc_delete($key);
}
function is_locked($key) {
return apc_fetch($key);
}
// example use
if (acquire_lock("foo")) {
do_something_that_requires_a_lock();
release_lock("foo");
}
In practice I might throw another function in there to generate a key to use here, just to prevent collision with an existing APC key, e.g.:
function key_for_lock($str) {
return md5($str."locked");
}
The $expire parameter is a nice feature of APC to use, since it prevents your lock from being held forever if your script dies or something like that.
Hopefully this answer is helpful for anyone else who stumbles here a year later.
EAccelerator has methods for it; eaccelerator_lock and eaccelerator_unlock.
Can't say if this is the best way to handle the job, but at least it is convenient.
function WhileLocked($pathname, callable $function, $proj = ' ')
{
// create a semaphore for a given pathname and optional project id
$semaphore = sem_get(ftok($pathname, $proj)); // see ftok for details
sem_acquire($semaphore);
try {
// capture result
$result = call_user_func($function);
} catch (Exception $e) {
// release lock and pass on all errors
sem_release($semaphore);
throw $e;
}
// also release lock if all is good
sem_release($semaphore);
return $result;
}
Usage is as simple as this.
$result = WhileLocked(__FILE__, function () use ($that) {
$this->doSomethingNonsimultaneously($that->getFoo());
});
Third optional argument can come handy if you use this function more than once per file.
Last but not least it isn't hard to modify this function (while keeping its signature) to use any other kind of locking mechanism at a later date, e.g. if you happen to find yourself working with multiple servers.
APC is now considered unmaintained and dead. It's successor APCu offers locking via apcu_entry. But be aware, that it also prohibits the concurrent execution of any other APCu functions. Depending on your use case, this might be OK for you.
From the manual:
Note: When control enters apcu_entry() the lock for the cache is acquired exclusively, it is released when control leaves apcu_entry(): In effect, this turns the body of generator into a critical section, disallowing two processes from executing the same code paths concurrently. In addition, it prohibits the concurrent execution of any other APCu functions, since they will acquire the same lock.
APCu has apcu_entry since 5.1.0, can implement a lock mechanism with it now:
/** get a lock, will wait until the lock is available,
* make sure handle deadlock yourself :p
*
* useage : $lock = lock('THE_LOCK_KEY', uniqid(), 50);
*
* #param $lock_key : the lock you want to get it
* #param $lock_value : the unique value to specify lock owner
* #param $retry_millis : wait befor retry
* #return ['lock_key'=>$lock_key, 'lock_value'=>$lock_value]
*/
function lock($lock_key, $lock_value, $retry_millis) {
$got_lock = false;
while (!$got_lock) {
$fetched_lock_value = apcu_entry($lock_key, function ($key) use ($lock_value) {
return $lock_value;
}, 100);
$got_lock = ($fetched_lock_value == $lock_value);
if (!$got_lock) usleep($retry_millis*1000);
}
return ['lock_key'=>$lock_key, 'lock_value'=>$lock_value];
}
/** release a lock
*
* usage : unlock($lock);
*
* #param $lock : return value of function lock
*/
function unlock($lock) {
apcu_delete($lock['lock_key']);
}
What I've found, actually, is that I don't need any locking at all... given what I'm trying to create is a map of all the class => path associations for autoload, it doesn't matter if one process overwrites what the other one has found (it's highly unlikely, if coded properly), because the data will get there eventually anyway. So, the solution turned out to be "no locks".

Categories