From 10760fa17bbc0a69a1434895f8a68385f7386440 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 13 May 2015 17:29:28 -0700 Subject: [PATCH] CRM-16387 - LockManager, LockInterface --- CRM/Core/Lock.php | 82 +++++++++++++++++---- Civi/Core/Container.php | 26 +++++++ Civi/Core/Lock/LockInterface.php | 58 +++++++++++++++ Civi/Core/Lock/LockManager.php | 121 +++++++++++++++++++++++++++++++ Civi/Core/Lock/NullLock.php | 86 ++++++++++++++++++++++ 5 files changed, 358 insertions(+), 15 deletions(-) create mode 100644 Civi/Core/Lock/LockInterface.php create mode 100644 Civi/Core/Lock/LockManager.php create mode 100644 Civi/Core/Lock/NullLock.php diff --git a/CRM/Core/Lock.php b/CRM/Core/Lock.php index fb252d00f2..f0be5f18ff 100644 --- a/CRM/Core/Lock.php +++ b/CRM/Core/Lock.php @@ -32,7 +32,7 @@ * $Id$ * */ -class CRM_Core_Lock { +class CRM_Core_Lock implements \Civi\Core\Lock\LockInterface { static $jobLog = FALSE; @@ -43,21 +43,75 @@ class CRM_Core_Lock { protected $_name; + /** + * Use MySQL's GET_LOCK(). Locks are shared across all Civi instances + * on the same MySQL server. + * + * @param string $name + * Symbolic name for the lock. Names generally look like + * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}"). + * + * Categories: worker|data|cache|... + * Component: core|mailing|member|contribute|... + * @return \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. + * + * @param string $name + * Symbolic name for the lock. Names generally look like + * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}"). + * + * Categories: worker|data|cache|... + * Component: core|mailing|member|contribute|... + * @return \Civi\Core\Lock\LockInterface + */ + 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). + * + * @param string $name + * Symbolic name for the lock. Names generally look like + * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}"). + * + * Categories: worker|data|cache|... + * Component: core|mailing|member|contribute|... + * @return \Civi\Core\Lock\LockInterface + * @deprecated + */ + public static function createCivimailLock($name) { + $serverWideLock = \CRM_Core_BAO_Setting::getItem( + \CRM_Core_BAO_Setting::MAILING_PREFERENCES_NAME, + 'civimail_server_wide_lock' + ); + return new static($name, NULL, $serverWideLock); + } + /** * Initialize the constants used during lock acquire / release * * @param string $name - * Name of the lock. Please prefix with component / functionality. - * e.g. civimail.cronjob.JOB_ID + * Symbolic name for the lock. Names generally look like + * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}"). + * + * Categories: worker|data|cache|... + * Component: core|mailing|member|contribute|... * @param int $timeout * The number of seconds to wait to get the lock. 1 if not set. * @param bool $serverWideLock * Should this lock be applicable across your entire mysql server. - * this is useful if you have multiple sites running on the same - * mysql server and you want to limit the number of parallel cron - * jobs - CRM-91XX - * - * @return \CRM_Core_Lock the lock object + * this is useful if you have multiple sites running on the same + * mysql server and you want to limit the number of parallel cron + * jobs - CRM-91XX */ public function __construct($name, $timeout = NULL, $serverWideLock = FALSE) { $config = CRM_Core_Config::singleton(); @@ -80,8 +134,6 @@ class CRM_Core_Lock { self::$jobLog = $this->_name; } $this->_timeout = $timeout !== NULL ? $timeout : self::TIMEOUT; - - $this->acquire(); } public function __destruct() { @@ -91,12 +143,12 @@ class CRM_Core_Lock { /** * @return bool */ - public function acquire() { + public function acquire($timeout = NULL) { if (!$this->_hasLock) { $query = "SELECT GET_LOCK( %1, %2 )"; $params = array( 1 => array($this->_name, 'String'), - 2 => array($this->_timeout, 'Integer'), + 2 => array($timeout ? $timeout : $this->_timeout, 'Integer'), ); $res = CRM_Core_DAO::singleValueQuery($query, $params); if ($res) { @@ -118,10 +170,10 @@ class CRM_Core_Lock { * @return null|string */ public function release() { - if (defined('CIVICRM_LOCK_DEBUG')) { - CRM_Core_Error::debug_log_message('release lock for ' . $this->_name . ' (' . ($this->_hasLock ? 'hasLock' : '!hasLock') . ')'); - } if ($this->_hasLock) { + if (defined('CIVICRM_LOCK_DEBUG')) { + CRM_Core_Error::debug_log_message('release lock for ' . $this->_name); + } $this->_hasLock = FALSE; if (self::$jobLog == $this->_name) { diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index 71679e9aff..3df0ebfb1b 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -1,6 +1,7 @@ setDefinition('lockManager', new Definition( + '\Civi\Core\Lock\LockManager', + array() + )) + ->setFactoryService(self::SELF)->setFactoryMethod('createLockManager'); + $container->setDefinition('angular', new Definition( '\Civi\Angular\Manager', array() @@ -133,6 +140,25 @@ class Container { return $dispatcher; } + /** + * @return LockManager + */ + public function createLockManager() { + // Ideally, downstream implementers could override any definitions in + // the container. For now, we'll make-do with some define()s. + $lm = new LockManager(); + $lm + ->register('/^cache\./', defined('CIVICRM_CACHE_LOCK') ? CIVICRM_CACHE_LOCK : array('CRM_Core_Lock', 'createScopedLock')) + ->register('/^data\./', defined('CIVICRM_DATA_LOCK') ? CIVICRM_DATA_LOCK : array('CRM_Core_Lock', 'createScopedLock')) + ->register('/^worker\.mailing\.send\./', defined('CIVICRM_WORK_LOCK') ? CIVICRM_WORK_LOCK : array('CRM_Core_Lock', 'createCivimailLock')) + ->register('/^worker\./', defined('CIVICRM_WORK_LOCK') ? CIVICRM_WORK_LOCK : array('CRM_Core_Lock', 'createScopedLock')); + + // Registrations may use complex resolver expressions, but (as a micro-optimization) + // the default factory is specified as an array. + + return $lm; + } + /** * @param \Symfony\Component\EventDispatcher\EventDispatcher $dispatcher * @param $magicFunctionProvider diff --git a/Civi/Core/Lock/LockInterface.php b/Civi/Core/Lock/LockInterface.php new file mode 100644 index 0000000000..dadd4d88cd --- /dev/null +++ b/Civi/Core/Lock/LockInterface.php @@ -0,0 +1,58 @@ +getFactory($name); + if ($factory) { + /** @var LockInterface $lock */ + $lock = call_user_func_array($factory, array($name)); + return $lock; + } + else { + throw new \CRM_Core_Exception("Lock \"$name\" does not match any rules. Use register() to add more rules."); + } + } + + /** + * Create and attempt to acquire a lock. + * + * Note: Be sure to check $lock->isAcquired() to determine whether + * acquisition was successful. + * + * @param string $name + * Symbolic name for the lock. Names generally look like + * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}"). + * + * Categories: worker|data|cache|... + * Component: core|mailing|member|contribute|... + * @param int|NULL $timeout + * The number of seconds to wait to get the lock. + * For a default value, use NULL. + * @return LockInterface + * @throws \CRM_Core_Exception + */ + public function acquire($name, $timeout = NULL) { + $lock = $this->create($name); + $lock->acquire($timeout); + return $lock; + } + + /** + * @param string $name + * Symbolic name for the lock. + * @return callable|NULL + */ + public function getFactory($name) { + foreach ($this->rules as $rule) { + if (preg_match($rule['pattern'], $name)) { + return Resolver::singleton()->get($rule['factory']); + } + } + return NULL; + } + + /** + * Register the lock-factory to use for specific lock-names. + * + * @param string $pattern + * A regex to match against the lock name. + * @param string|array $factory + * A callback. The callback should accept a $name parameter. + * Callbacks will be located using the resolver. + * @return $this + * @see Resolver + */ + public function register($pattern, $factory) { + $this->rules[] = array( + 'pattern' => $pattern, + 'factory' => $factory, + ); + return $this; + } + +} diff --git a/Civi/Core/Lock/NullLock.php b/Civi/Core/Lock/NullLock.php new file mode 100644 index 0000000000..8a2d0e46fa --- /dev/null +++ b/Civi/Core/Lock/NullLock.php @@ -0,0 +1,86 @@ +hasLock = TRUE; + return TRUE; + } + + /** + * @return bool|null|string + * Trueish/falsish. + */ + public function release() { + $this->hasLock = FALSE; + return TRUE; + } + + /** + * @return bool|null|string + * Trueish/falsish. + * @deprecated + * Not supported by some locking strategies. If you need to poll, better + * to use acquire(0). + */ + public function isFree() { + return !$this->hasLock; + } + + /** + * @return bool + */ + public function isAcquired() { + return $this->hasLock; + } + +} -- 2.25.1