diff --git a/CRM/Core/Lock.php b/CRM/Core/Lock.php index 9c28de7fd244..30b31b4aa47e 100644 --- a/CRM/Core/Lock.php +++ b/CRM/Core/Lock.php @@ -3,7 +3,7 @@ +--------------------------------------------------------------------+ | CiviCRM version 4.6 | +--------------------------------------------------------------------+ - | Copyright CiviCRM LLC (c) 2004-2015 | + | Copyright CiviCRM LLC (c) 2004-2016 | +--------------------------------------------------------------------+ | This file is a part of CiviCRM. | | | @@ -24,25 +24,30 @@ | see the CiviCRM license FAQ at http://civicrm.org/licensing | +--------------------------------------------------------------------+ */ - /** * * @package CRM - * @copyright CiviCRM LLC (c) 2004-2015 + * @copyright CiviCRM LLC (c) 2004-2016 * $Id$ * */ class CRM_Core_Lock implements \Civi\Core\Lock\LockInterface { - + /** + * This variable (despite it's name) roughly translates to 'lock that we actually care about'. + * + * Prior to version 5.7.5 mysql only supports a single named lock. This variable is + * part of the skullduggery involved in 'say it's no so Frank'. + * + * See further comments on the aquire function. + * + * @var bool + */ static $jobLog = FALSE; - // lets have a 3 second timeout for now const TIMEOUT = 3; - protected $_hasLock = FALSE; - protected $_name; - + protected $_id; /** * Use MySQL's GET_LOCK(). Locks are shared across all Civi instances * on the same MySQL server. @@ -58,7 +63,6 @@ class CRM_Core_Lock implements \Civi\Core\Lock\LockInterface { public static function createGlobalLock($name) { return new static($name, NULL, TRUE); } - /** * Use MySQL's GET_LOCK(), but apply prefixes to the lock names. * Locks are unique to each instance of Civi. @@ -74,7 +78,6 @@ public static function createGlobalLock($name) { public static function createScopedLock($name) { return new static($name); } - /** * Use MySQL's GET_LOCK(), but conditionally apply prefixes to the lock names * (if civimail_server_wide_lock is disabled). @@ -95,7 +98,6 @@ public static function createCivimailLock($name) { ); return new static($name, NULL, $serverWideLock); } - /** * Initialize the constants used during lock acquire / release * @@ -124,85 +126,105 @@ public function __construct($name, $timeout = NULL, $serverWideLock = FALSE) { else { $this->_name = $database . '.' . $domainID . '.' . $name; } + // MySQL 5.7 doesn't like long lock names so creating a lock id + $this->_id = sha1($this->_name); if (defined('CIVICRM_LOCK_DEBUG')) { - CRM_Core_Error::debug_log_message('trying to construct lock for ' . $this->_name); + CRM_Core_Error::debug_log_message('trying to construct lock for ' . $this->_name . '(' . $this->_id . ')'); } $this->_timeout = $timeout !== NULL ? $timeout : self::TIMEOUT; } - public function __destruct() { $this->release(); } - /** + * Acquire lock. + * + * The advantage of mysql locks is that they can be used across processes. However, only one + * can be used at once within a process. An attempt to use a second one within a process + * prior to mysql 5.7.5 results in the first being released. + * + * The process here is + * 1) first attempt to grab a lock for a mailing job - self::jobLog will be populated with the + * lock id & a mysql lock will be created for the ID. + * + * If a second function in the same process attempts to grab the lock it will enter the hackyHandleBrokenCode routine + * which says 'I won't break a mailing lock for you but if you are not a civimail send process I'll let you + * pretend you have a lock already and you can go ahead with whatever you were doing under the delusion you + * have a lock. + * + * @todo bypass hackyHandleBrokenCode for mysql version 5.7.5+ + * + * If a second function in a separate process attempts to grab the lock already in use it should be rejected, + * but it appears it IS allowed to grab a different lock & unlike in the same process the first lock won't be released. + * + * All this means CiviMail locks are first class citizens & any other process gets a 'best effort lock'. + * + * @todo document naming convention for CiviMail locks as this is key to ensuring they work properly. + * + * @param int $timeout + * * @return bool + * @throws \CRM_Core_Exception */ public function acquire($timeout = NULL) { if (!$this->_hasLock) { if (self::$jobLog && CRM_Core_DAO::singleValueQuery("SELECT IS_USED_LOCK( '" . self::$jobLog . "')")) { return $this->hackyHandleBrokenCode(self::$jobLog); } - $query = "SELECT GET_LOCK( %1, %2 )"; $params = array( - 1 => array($this->_name, 'String'), + 1 => array($this->_id, 'String'), 2 => array($timeout ? $timeout : $this->_timeout, 'Integer'), ); $res = CRM_Core_DAO::singleValueQuery($query, $params); if ($res) { if (defined('CIVICRM_LOCK_DEBUG')) { - CRM_Core_Error::debug_log_message('acquire lock for ' . $this->_name); + CRM_Core_Error::debug_log_message('acquire lock for ' . $this->_name . '(' . $this->_id . ')'); } $this->_hasLock = TRUE; if (stristr($this->_name, 'data.mailing.job.')) { - self::$jobLog = $this->_name; + self::$jobLog = $this->_id; } } else { if (defined('CIVICRM_LOCK_DEBUG')) { - CRM_Core_Error::debug_log_message('failed to acquire lock for ' . $this->_name); + CRM_Core_Error::debug_log_message('failed to acquire lock for ' . $this->_name . '(' . $this->_id . ')'); } } } return $this->_hasLock; } - /** * @return null|string */ public function release() { if ($this->_hasLock) { if (defined('CIVICRM_LOCK_DEBUG')) { - CRM_Core_Error::debug_log_message('release lock for ' . $this->_name); + CRM_Core_Error::debug_log_message('release lock for ' . $this->_name . '(' . $this->_id . ')'); } $this->_hasLock = FALSE; - - if (self::$jobLog == $this->_name) { + if (self::$jobLog == $this->_id) { self::$jobLog = FALSE; } - $query = "SELECT RELEASE_LOCK( %1 )"; - $params = array(1 => array($this->_name, 'String')); + $params = array(1 => array($this->_id, 'String')); return CRM_Core_DAO::singleValueQuery($query, $params); } } - /** * @return null|string */ public function isFree() { $query = "SELECT IS_FREE_LOCK( %1 )"; - $params = array(1 => array($this->_name, 'String')); + $params = array(1 => array($this->_id, 'String')); return CRM_Core_DAO::singleValueQuery($query, $params); } - /** * @return bool */ public function isAcquired() { return $this->_hasLock; } - /** * CRM-12856 locks were originally set up for jobs, but the concept was extended to caching & groups without * understanding that would undermine the job locks (because grabbing a lock implicitly releases existing ones) @@ -218,11 +240,11 @@ public function isAcquired() { */ public function hackyHandleBrokenCode($jobLog) { if (stristr($this->_name, 'job')) { - CRM_Core_Error::debug_log_message('lock acquisition for ' . $this->_name . ' attempted when ' . $jobLog . ' is not released'); - throw new CRM_Core_Exception('lock acquisition for ' . $this->_name . ' attempted when ' . $jobLog . ' is not released'); + CRM_Core_Error::debug_log_message('lock acquisition for ' . $this->_name . '(' . $this->_id . ')' . ' attempted when ' . $jobLog . ' is not released'); + throw new CRM_Core_Exception('lock acquisition for ' . $this->_name . '(' . $this->_id . ')' . ' attempted when ' . $jobLog . ' is not released'); } if (defined('CIVICRM_LOCK_DEBUG')) { - CRM_Core_Error::debug_log_message('(CRM-12856) faking lock for ' . $this->_name); + CRM_Core_Error::debug_log_message('(CRM-12856) faking lock for ' . $this->_name . '(' . $this->_id . ')'); } $this->_hasLock = TRUE; return TRUE;