Using mysql based locking in CiviMail ...

Published
2007-09-14 17:21
Written by
A cron job executes periodically to process all the scheduled / running jobs for CiviMail. The admin can set an optional limit on the number of mails processed in the cron job. This allows two different invocations of the cron job to step on each other, a bad thing which could result in users getting multiple copies of the same mail. To avoid this from happening the cron script used flock. This had two main disadvantages:
  • The cronjob could run on only one machine due to the file system lock
  • The lock was very granular and applied to entire script (civimail.cronjob.php). This is not optimal
In v1.9, we fixed the problem by introducing a database based locking scheme using MySQL. Specifically we used the functions: get_lock, release_lock and is_free_lock from the mysql miscellaneous functions library. This allows us to maintain server based locking and hence can run multiple instances of the cron script at the same time from the same or different machines. We also moved the locking to the 'job' level, thus allowing two jobs to be processed in parallel if needed. The outline for this code is:
  foreach job that is either scheduled or running do:
    lockName = "civimail.job.{$domainID}.{$jobID}";
    acquire an exclusive DB lock for lockName
    if lock acquired:
       process job
       release lock
    end if
  end foreach
Our php lock library code is reproduced below:
class CRM_Core_Lock {
    // lets have a 1 second timeout window
    const TIMEOUT = 1;
    protected $_hasLock = false;
    protected $_name;
    function __construct( $name, $timeout = null ) {
        $this->_name    = $name;
        $this->_timeout = $timeout ? $timeout : self::TIMEOUT;
        $this->acquire( );
    }
    function __destruct( ) {
        $this->release( );
    }
    function acquire( ) {
        if ( ! $this->_hasLock ) {
            $query  = "SELECT GET_LOCK( %1, %2 )";
            $params = array( 1 => array( $this->_name   , 'String'  ),
                             2 => array( $this->_timeout, 'Integer' ) );
            $res = CRM_Core_DAO::singleValueQuery( $query, $params );
            if ( $res ) {
                $this->_hasLock = true;
            }
        }
        return $this->_hasLock;
    }
    function release( ) {
        if ( $this->_hasLock ) {
            $this->_hasLock = false;
            $query = "SELECT RELEASE_LOCK( %1 )";
            $params = array( 1 => array( $this->_name, 'String' ) );
            return CRM_Core_DAO::singleValueQuery( $query, $params );
        }
    }
    function isFree( ) {
        $query = "SELECT IS_FREE_LOCK( %1 )";
        $params = array( 1 => array( $this->_name, 'String' ) );
        return CRM_Core_DAO::singleValueQuery( $query, $params );
    }
    function isAcquired( ) {
        return $this->_hasLock;
    }
}
Filed under

Comments

Anonymous (not verified)
2007-10-17 - 11:30

Thanks for the interesting post.

Have you run into any notable problems/limitations with the new scheme?