This extension can be used in case a certain part of your application should only run ONCE at a time. For example you may have a cronjob console command that is executed every minute regardless of how long the action in the cronjob takes. See Mutex article on Wikipedia.
Resources ¶
Documentation ¶
Requirements ¶
- Yii 1.0 or above
Installation ¶
- Extract the release file under
protected/extensions
- Define component in config
'components' => array(
...
'mutex' => array(
'class' => 'application.extensions.EMutex',
),
...
),
Usage ¶
See the following code example:
// Check if we have a lock already. If not set one which
// expires automatically after 10 minutes.
if (Yii::app()->mutex->lock('some-unique-id', 600))
{
// Do some time-expensive stuff here...
// sleep(10) as example
// and after that release the lock...
Yii::app()->mutex->unlock();
}
else
{
// The lock does already exist so we exit
echo "Already working on it...";
exit;
}
The $timeout
variable is there to ensure that the lock isn't infinite in case of things like a server crash. Means if you don't define a timeout and unlock() isn't called for some reason, the lock will stay forever.
All locks are represented in a single file. You can change the $mutexFile
variable to change the path of this file (defaults to the Yii runtime path + /mutex.bin
).
You can also define an $id
when unlocking. That means in one cronjob you could setup a lock and in another one you could release that lock. For local created locks (current request), only unlock() works to make sure nested locks will get released in order.
Some more usage examples:
// Waiting for a lock to get released (spin lock)
// Make sure to call sleep() inside of the loop, because everytime
// you call lock(), the $mutexFile gets read (physical file-access).
while (!Yii::app()->mutex->lock('id'))
{
sleep(1);
}
...
Yii::app()->mutex->unlock();
// Nested locks
if (Yii::app()->mutex->lock('id1'))
{
while (!Yii::app()->mutex->lock('id2'))
{
sleep(1);
}
...
if (Yii::app()->mutex->lock('id3'))
{
...
Yii::app()->mutex->unlock(); // releases "id3"
}
...
Yii::app()->mutex->unlock(); // releases "id2"
Yii::app()->mutex->unlock(); // releases "id1"
}
You may use __CLASS__
as $id
for example in a console command ;-)
Change Log ¶
April 12, 2010 ¶
- Rewritten. Now using one file to store all the locks. See forum discussion and updated documentation for more info.
April 3, 2010 ¶
- The application name is now considered for the unqiue lock file names. So it's possible to use one lock file directory for different applications without conflicts.
March 29, 2010 ¶
- Initial release.
other way to do the same
If your app uses MySQL database, better use MySQL named locks for that. They will unlock automatically if your thread or even the entire server happens to die prematurely, so there's no need to set a timeout. Just make sure your lock's name is unique throughout the entire MySQL server (which is not very difficult to achieve).
Good one
Simple yet useful tool for locking various tasks not to be performed twice.
when exception happens from the locked code segment , what 's your advise?
// Check if we have a lock already. If not set one which // expires automatically after 10 minutes. if (Yii::app()->mutex->lock('some-unique-id')) { // Do some time-expensive stuff here... // sleep(10) as example throw new Exception('for test !'); trigger_error('for test too!'); // and after that release the lock... Yii::app()->mutex->unlock(); } else { // The lock does already exist so we exit echo "Already working on it..."; exit; }
you see if there happens some error or trigger exception , the unlock will never called !
for the exception reason ,use try/catch to settle:
if (Yii::app()->mutex->lock('someId')) { //always use try/catch blok try{ throw new Exception('for test !'); // trigger_error('for test too!'); }catch( Exception $e){ Yii::log('exception happens in method'.__METHOD__); Yii::app()->mutex->unlock(); throw $e; //we just rethrow the exception instance } echo 'hi this is in locked code segment'; // and after that release the lock... Yii::app()->mutex->unlock(); } else { // The lock does already exist so we exit echo "Already working on it..."; exit; }
the php has no finally statement: try/catch/finally ; if so it's better place to unlock in finally clause;
for the trigger_error('for test too!'); you need do some effort to settle it , convert the old school php error to Exception firstly !
Improvement
Thank you for this extension!
And why not to add shell command, which flushes/removes all of the locks?
Implement Lock check
I've implemented a small function to check if a specific (or any) lock exists:
/* * Verify wheter a lock exists or not. * * @param mixed $id The optional lock id to verify * @return boolean If the locks exists */ public function lockExists($id = null) { if($id === null) if(count($this->_locks) == 0) return false; else return true; else if(in_array($id, $this->_locks)) return true; else return false; } }
Feel free to use or even add to the extension.
I added a quick-and-dirty "relock" method that restarts the timeout
//refreshes timeout. assumes the calling process already has the lock.
public function relock($id, $timeout = 0) {
$lockFileHandle = $this->_getLockFileHandle($id); if (flock($lockFileHandle, LOCK_EX)) { $data = @unserialize(@file_get_contents($this->mutexFile)); if (empty($data) || !isset($data[$id]) || (($data[$id][0] > 0) && ($data[$id][0] + $data[$id][1]) <= microtime(true))) { $result = false; } else { $data[$id] = array($timeout, microtime(true)); //array_push($this->_locks, $id); $result = (bool)file_put_contents($this->mutexFile, serialize($data)); } } fclose($lockFileHandle); @unlink($this->_getLockFile($id)); return isset($result) ? $result : false; }
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.