CRM-16387 - LockManager, LockInterface
authorTim Otten <totten@civicrm.org>
Thu, 14 May 2015 00:29:28 +0000 (17:29 -0700)
committerTim Otten <totten@civicrm.org>
Mon, 15 Jun 2015 17:34:04 +0000 (10:34 -0700)
CRM/Core/Lock.php
Civi/Core/Container.php
Civi/Core/Lock/LockInterface.php [new file with mode: 0644]
Civi/Core/Lock/LockManager.php [new file with mode: 0644]
Civi/Core/Lock/NullLock.php [new file with mode: 0644]

index fb252d00f2bf68e7e29066dd188841d1c65775bc..f0be5f18fffe50f3eb0935ba4efa6a7ac963310b 100644 (file)
@@ -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) {
index 71679e9aff0a263be86391ef8991bbeebe4fc0eb..3df0ebfb1bf61bdbb0547c64e685d37d73dec8c1 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace Civi\Core;
 
+use Civi\Core\Lock\LockManager;
 use Doctrine\Common\Annotations\AnnotationReader;
 use Doctrine\Common\Annotations\AnnotationRegistry;
 use Doctrine\Common\Annotations\FileCacheReader;
@@ -67,6 +68,12 @@ class Container {
     //      }
     //    }
 
+    $container->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 (file)
index 0000000..dadd4d8
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.6                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2015                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+ */
+namespace Civi\Core\Lock;
+
+interface LockInterface {
+
+  /**
+   * @param int|NULL $timeout
+   *   The number of seconds to wait to get the lock.
+   *   For a default value, use NULL.
+   * @return bool
+   */
+  public function acquire($timeout = NULL);
+
+  /**
+   * @return bool|null|string
+   *   Trueish/falsish.
+   */
+  public function release();
+
+  /**
+   * @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 bool
+   */
+  public function isAcquired();
+}
diff --git a/Civi/Core/Lock/LockManager.php b/Civi/Core/Lock/LockManager.php
new file mode 100644 (file)
index 0000000..673c975
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.6                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2015                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+ */
+namespace Civi\Core\Lock;
+
+use Civi\Core\Resolver;
+
+/**
+ * Class LockManager
+ * @package Civi\Core\Lock
+ *
+ * The lock-manager allows one to define the lock policy -- i.e. given a
+ * specific lock, how does one acquire the lock?
+ */
+class LockManager {
+
+  private $rules = array();
+
+  /**
+   * @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 LockInterface
+   * @throws \CRM_Core_Exception
+   */
+  public function create($name) {
+    $factory = $this->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 (file)
index 0000000..8a2d0e4
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 4.6                                                |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2015                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+ */
+namespace Civi\Core\Lock;
+
+/**
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC (c) 2004-2015
+ * $Id$
+ *
+ */
+class NullLock implements LockInterface {
+
+  private $hasLock = FALSE;
+
+  /**
+   * @param string $name
+   * @return static
+   */
+  public static function create($name) {
+    return new static();
+  }
+
+  /**
+   * @param int|NULL $timeout
+   *   The number of seconds to wait to get the lock.
+   *   For a default value, use NULL.
+   * @return bool
+   */
+  public function acquire($timeout = NULL) {
+    $this->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;
+  }
+
+}