info about link
: Javascript : PHP Index : MySQL :
mutex class
: Source : : Explanation : : Example : : Todo : : Feedback :

mutex.class.php

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

download

<?
/******
* 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($appNameMutex_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_ADDRESSself::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
      
}
   }
}
?>

download

=[Disclaimer]=     © 2005-2018 Excudo.net