| : | Javascript | : | PHP Index | : | MySQL | : |
| : | Source | : | : | Explanation | : | : | Example | : | : | Todo | : | : | Feedback | : |
This mutex (mutual exclusion) class can help to avoid the simultaneous use of a common resource (concurrency problem)
Added: 2008-08-07 07:12:41
Last update: 2009-02-18 07:42:15
<?
/******
* You may use and/or modify this script as long as you:
* 1. Keep my name & webpage mentioned
* 2. Don't use it for commercial purposes
*
* If you want to use this script without complying to the rules above, please contact me first at: marty@excudo.net
*
* Author: Martijn Korse
* Website: http://devshed.excudo.net
*
* Date: 2008-08-07 07:12:41
***/
/**
* Mutex Class
*
* This mutex class works based on the principle that every running proces will be able to access
* one common storage device (such as, for example, a database)
* The exclusivity is enforced by creating a record in this storage device that says:
* 'process Z is running for at least X-seconds'. When this process is activated from another location
* it will not get a mutex-lock if this record isn't yet expired. When process Z knows it has finished
* doing what it was supposed to do, it should release the lock immediately, so a new lock can be
* acquired as soon as possible.
*
* To keep the class flexibel it works with a storage adapter and so it requires at least one
* such adapter that can act as the storage engine for the mutex-records.
* This adapter class is only in charge of storing, retrieving and modifying the data in the storage
* device.
* As an example, this is what a mysql table should look like:
* CREATE TABLE `Mutex` (
* `MutexID` int(10) unsigned NOT NULL auto_increment,
* `Application` varchar(255) NOT NULL,
* `AppLock` int(10) unsigned NOT NULL,
* `MutexStart` datetime NOT NULL,
* `MutexEnd` datetime default NULL,
* `MutexTTL` mediumint(8) unsigned NOT NULL default '180',
* `Log` text NOT NULL,
* PRIMARY KEY (`MutexID`),
* UNIQUE KEY `ApplicationLock` (`AppLock`,`Application`)
* ) ENGINE=MyISAM
*
* MutexID
* will represent the lock-id
* Application
* will be the name of the application (or process) that wants the mutex-lock
* AppLock
* is a way to ensure exclusivity. When a record is created, it will get the value 0. So,
* when inserting a new record with the same name, the unique key will interfere as long as this
* value is 0. To release the lock, this value is then updated to the value of the MutexID field.
* This way, it will stay unique and a new record can be created
* MutexStart
* Indicates when the mutex was created
* MutexEnd
* Indicates when the mutex lock was released
* MutexTTL
* MutexTimeToLive: When the difference between the current timestamp and MutexStart is bigger than
* this value, the lock should also be released. So, this is the maximum time a mutex is allowed to live
* after that, we must assume the application that required the lock is no longer running
* Log
* This field can be abused to log certain things
*
* It is very important that whatever adapter you create, it will assign unique integers to every mutex
* record it creates!
*/
class Mutex
{
/**
* The name of the application of process that wants to use the mutex mechanism
*
* NOTE: This name must be unique for that application/process within the system you are using
* If you give two different applications the same name they will lock eachother!
*
* @var string
*/
protected $appName;
/**
* The id of the lock.
* This will get its value once a lock has been set. It is the auto_increment primary key
* from the Mutex table
*
* @var integer
*/
protected $lockId;
/**
* This variable indicates (later on) if we are already doing a sanity-check
*
* @see checkSanity()
*
* @var boolean
*/
private $checkingSanity;
/**
* @see Mutex_Storage_Adapter_Interface
* @var Mutex_Storage_Adapter_Interface
*/
protected $adapter;
/**
* Default Time To Live (in seconds)
* This is the default maximum time a mutex is allowed to live.
* After this, the mutex will be released.
*
* When calling getLock() a custom TimeToLive can be set. If it's not set
* this value will be used
*
* @see getLock()
*/
const DEFAULT_TTL = 180; // in seconds
/**
* Constructor
*
* Sets all the defaults and assigns an adapter which will be in charge of exchanging
* information with whatever source is used in the adapter for storage
*
* @param string $appName
* @param Mutex_Storage_Adapter_Interface $adapter
*/
public function __construct($appName, Mutex_Storage_Adapter_Interface $adapter)
{
$this->appName = $appName;
$this->lockId = False;
$this->checkingSanity = False;
$this->adapter = $adapter;
}
/**
* This function will return a numerical value when a lock has been succesfully acquired.
* That numerical value can then later be used to release the lock again.
* If no lock could be acquired, False will be returned. This means that a lock has already been
* given to an application/proces using the same $appName
*
* @return int|boolean
* @throws Exception (when sanity check fails, or the storage adapater throws an exception)
*/
public function getLock($ttl = null)
{
if (is_null($ttl))
{
$ttl = self::DEFAULT_TTL;
}
try {
$lockId = $this->adapter->getLock($ttl, $this->getAppName());
}
catch(Exception $e) {
// an error occured.
// although we've caught the exception, we return an exception as well
// this structure may be handy later when we add different type of exceptions
throw new Exception($e->getMessage(), $e->getCode());
}
if (False === $lockId)
{
$sanityCheck = $this->checkSanity($ttl);
if (False === $sanityCheck)
{
throw new Exception('Sanity check failed. Something must be wrong');
}
elseif (True === $sanityCheck)
{
// passed the sanity check. there must be another lock preventing us
// from having one
return False;
}
else
{
// else, it must contain a new mutexId
$this->lockId = $sanityCheck;
}
}
else
{
$this->lockId = $lockId;
}
return $this->lockId;
}
/**
*
* @param int $mutexId the id of the mutex record that should be unlocked
* @return boolean True, if the lock was succesfully released, or false
* when it wasn't
* @throws Exception when mutexId was not passed and not set internally
*/
public function releaseLock($mutexId = null)
{
if (is_null($mutexId))
$mutexId = $this->lockId;
if (is_numeric($mutexId))
{
if ($this->adapter->releaseLock($mutexId))
{
$this->lockId = True;
return True;
}
else
{
return False;
}
}
else
{
throw new Exception('mutexId could\'t be determined');
}
}
/**
* Retrieves an array with information about the mutex from the adapter
* This array must have the following properties:
* 'TL' Time Living: the difference betw
* @return <type>
* @throws Exception when the adapter returns an invalid array
*/
protected function getRunningMutex()
{
$result = $this->adapter->getRunningMutex($this->getAppName());
if ($result !== False)
{
if (!is_array($result) || !isset($result['TL']) || !isset($result['TTL']) || !isset($result['MutexID']))
{
throw new Exception('The returned result from the adapter is an invalid array');
}
}
return $result;
}
/**
* Performs a sanity check.
* This class will do that in the moment a new lock couldnt be acquired.
*
* It will look up if there is an active mutex for the current application/proces
* and if so, return either of two variable types:
* Bool False: meaning: no, we are not sane.
* Bool True: meaning, we are sane, but there is already an active mutex lock
* Integer: meaning, there was an expired mutex lock that was released and this is the
* value of the new lock i got for you.
*
* @param int $ttl The Time To Live in case we can release an old lock and acquire a new one
* @return boolean|int see above for explanation
*/
private function checkSanity($ttl)
{
if (False === $this->checkingSanity)
{
// indicate we are checking the sanity (in case getLock() further on
// triggers it again
$this->checkingSanity = True;
try {
$curMutex = $this->getRunningMutex();
if (False === $curMutex)
{
return $this->getLock();
}
else
{
if ($curMutex['TL'] > $curMutex['TTL'])
{
$this->releaseLock($curMutex['MutexID']);
return $this->getLock($ttl);
}
else
{
return True;
}
}
}
catch (Exception $e) {
// catching this because we might want to distinguish types of exceptions
// in the future. for now, we simply throw the passed Exception
throw new Exception($e->getMessage(), $e->getCode());
}
}
else
{
// apparently we've already tried to check the sanity and are entering
// a loophole if we don't break it
$this->checkingSanity = False;
return False;
}
}
/**
* Writes a string to the log-field in the database of the current mutex-record
* Because its public, any application that instantiates an object of this class
* may write to it
*
* @param string $string holds whatever we want to log
*
* @return boolean returns True if the log-action succeeded
* returns False if we somehow couldnt write to the log. Most likely
* this will be because there is no active mutex.
*/
public function log($string)
{
if (False !== $this->getLockId())
{
return $this->adapter->log($this->getLockId(), $string);
}
else
{
return False;
}
}
/**
* Getter method for the protected variable
*
* @return boolean|int Boolean False if there is no mutexId set as the lockId yet
* Otherwise an integer, which corresponds to the unique id of
* the mutex record that acts as lock
*/
public function getLockId()
{
return $this->lockId;
}
/**
* Getter method for the protected variable
* @return string
*/
public function getAppName()
{
return $this->appName;
}
}
/**
* Mutex_Storage_Adapter_Interface
*/
interface Mutex_Storage_Adapter_Interface
{
/**
* Has to write $string to the log, with the given $mutexId
*
* @return boolean (true, if logging succeeded, otherwise false
*/
public function log($mutexId, $string);
/**
* Looks up if there is an active mutex for the given application name
* @param string $appName
* @return boolean|int boolean false when there is no active mutex
* integer when the mutex has been found. the value of this
* integer is the mutex-id
* @throws Exception when something went wrong with the engine
*/
public function getRunningMutex($appName);
/**
* This releases a specific mutex. It 'unlocks' the mutex with the given id
*
* @param int $mutexId
* @return boolean false when the lock couldnt be released
* true when the lock was successfully released
*/
public function releaseLock($mutexId);
/**
* Tries to create a lock and returns the mutexId if the lock was successfully
* acquired.
*
* @param int $ttl Time To Live in seconds. After this, the lock will be
* automatically released the next time this function is
* called with the same $appName
* @param string $appName The name of the application/process for which we
* are setting the lock
*
* @return boolean|int boolean False when the lock could not be created. This means
* that there is already a lock for $appName which hasn't outlived
* it's time yet. (start-time + ttl > current time)
* @throws Exception When an error occured with the engine
*/
public function getLock($ttl, $appName);
/**
* Returns what type of storage engine the adapter is.
* For example 'mysql' or 'filesystem' ....
* It's just an indication and you're free to return whatever you like.
*
* @return string
*/
public function getEngineType();
}
/**
* Implementation of the Interface.
* This one uses mysql as storage device
*
* @see Mutex_Storage_Adapter_Interface
*/
class Mutex_Storage_Adapter_Mysql implements Mutex_Storage_Adapter_Interface
{
/**
* This value is returned by getEngineType()
*
* @see getEngineType()
*/
const ENGINE = 'mysql';
/**
* This value is used in the reportError() method
*
* @see reportError()
*/
const EMAIL_ADDRESS = 'your@email.address';
/**
* This value is used in the reportError() method
*
* @see reportError()
*/
const EMAIL_SUBJECT = 'databse-error with mutex class';
/**
* Indication that the report function should send the errors by email
* Assign this to the REPORT_BY constant if that is what you want
*
* @see REPORT_BY
*/
const REPORT_BY_EMAIL = 'by email';
/**
* Indication that the report function should output the errors to the screen
* Assign this to the REPORT_BY constant if that is what you want
*
* @see REPORT_BY
*/
const REPORT_TO_SCREEN = 'to the screen';
/**
* Assign one of the REPORT_TO_ constants to this one, to determine how errors should be
* reported to you. Or, assign any other value (for example 'null') to it, if you don't
* want any error reporting at all.
*/
const REPORT_BY = self::REPORT_TO_SCREEN;
/**
* Constructor
*
* It depends on your situation what the constructor does. If there is already a database
* connection, you won't need to create one here. If not, this is the place to do it. In
* that case you may want to uncomment the require-once line; but if you do: make sure this
* file exists and that it connects to your mysql-database.
*/
public function __construct()
{
// connect to the database if necessary
# require_once "connect.php";
}
/**
* @see Mutex_Storage_Adapter_Interface::getEngineType()
*/
public function getEngineType()
{
return self::ENGINE;
}
/**
* @see Mutex_Storage_Adapter_Interface::getLock()
*/
public function getLock($ttl, $appName)
{
$query = "
INSERT INTO Mutex (
Application,
AppLock,
MutexStart,
MutexTTL
) VALUES (
'".mysql_real_escape_string($appName)."',
0,
NOW(),
".((int) $ttl)."
)";
if (mysql_query($query))
{
return mysql_insert_id();
}
else
{
if (mysql_errno() != 1062)
{
$this->reportError($query."\n".mysql_error(), __LINE__);
throw new Exception('database error');
}
else
{
return False;
}
}
}
/**
* @see Mutex_Storage_Adapter_Interface::releaseLock()
*/
public function releaseLock($mutexId)
{
$query = "
UPDATE Mutex SET
AppLock=MutexID,
MutexEnd=NOW()
WHERE MutexID=".$mutexId;
if (mysql_query($query))
{
return True;
}
else
{
$this->reportError($query."\n".mysql_error(), __LINE__);
return False;
}
}
/**
* @see Mutex_Storage_Adapter_Interface::getRunningMutex()
*/
public function getRunningMutex($appName)
{
$query = "
SELECT
MutexID, MutexTTL AS TTL,
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(MutexStart) AS TL
FROM Mutex
WHERE Application='".mysql_real_escape_string($appName)."' AND AppLock=0";
if ($result = mysql_query($query))
{
if (mysql_num_rows($result) == 0)
return False;
else
return mysql_fetch_assoc($result);
}
else
{
$this->reportError($query."\n".mysql_error(), __LINE__);
throw new Exception('database error', 1000);
}
}
/**
* @see Mutex_Storage_Adapter_Interface::log()
*/
public function log($mutexId, $string)
{
$query = "
UPDATE Mutex SET
Log=CONCAT(Log, '\n', '".mysql_real_escape_string($string)."')
WHERE MutexID=".$mutexId;
if (mysql_query($query))
{
return True;
}
else
{
self::reportError($query."\n".mysql_error(), __LINE__);
return False;
}
}
/**
* This function reports any errors specific for this adapter.
* It reports this:
* 1. to the screen
* or
* 2. by email
* depending on self::REPORT_BY
*
* @param string $string description of the error
* @param int $line the line on which it occured
*/
private function reportError($string, $line)
{
switch (self::REPORT_BY)
{
case self::REPORT_BY_EMAIL :
mail(self::EMAIL_ADDRESS, self::EMAIL_SUBJECT, "Error: ".$string."\non line: ".__LINE__." in file: ".__FILE__);
break;
case self::REPORT_TO_SCREEN :
echo "Error: ".$string."\non line: ".__LINE__." in file: ".__FILE__;
break;
default :
; // we don't do anything
}
}
}?>