3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2017 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2017
35 class CRM_Core_Lock
implements \Civi\Core\Lock\LockInterface
{
38 * This variable (despite it's name) roughly translates to 'lock that we actually care about'.
40 * Prior to version 5.7.5 mysql only supports a single named lock. This variable is
41 * part of the skullduggery involved in 'say it's no so Frank'.
43 * See further comments on the aquire function.
47 static $jobLog = FALSE;
49 // lets have a 3 second timeout for now
52 protected $_hasLock = FALSE;
59 * Use MySQL's GET_LOCK(). Locks are shared across all Civi instances
60 * on the same MySQL server.
63 * Symbolic name for the lock. Names generally look like
64 * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}").
66 * Categories: worker|data|cache|...
67 * Component: core|mailing|member|contribute|...
68 * @return \Civi\Core\Lock\LockInterface
70 public static function createGlobalLock($name) {
71 return new static($name, NULL, TRUE);
75 * Use MySQL's GET_LOCK(), but apply prefixes to the lock names.
76 * Locks are unique to each instance of Civi.
79 * Symbolic name for the lock. Names generally look like
80 * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}").
82 * Categories: worker|data|cache|...
83 * Component: core|mailing|member|contribute|...
84 * @return \Civi\Core\Lock\LockInterface
86 public static function createScopedLock($name) {
87 return new static($name);
91 * Use MySQL's GET_LOCK(), but conditionally apply prefixes to the lock names
92 * (if civimail_server_wide_lock is disabled).
95 * Symbolic name for the lock. Names generally look like
96 * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}").
98 * Categories: worker|data|cache|...
99 * Component: core|mailing|member|contribute|...
100 * @return \Civi\Core\Lock\LockInterface
103 public static function createCivimailLock($name) {
104 $serverWideLock = \Civi
::settings()->get('civimail_server_wide_lock');
105 return new static($name, NULL, $serverWideLock);
109 * Initialize the constants used during lock acquire / release
111 * @param string $name
112 * Symbolic name for the lock. Names generally look like
113 * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}").
115 * Categories: worker|data|cache|...
116 * Component: core|mailing|member|contribute|...
117 * @param int $timeout
118 * The number of seconds to wait to get the lock. 1 if not set.
119 * @param bool $serverWideLock
120 * Should this lock be applicable across your entire mysql server.
121 * this is useful if you have multiple sites running on the same
122 * mysql server and you want to limit the number of parallel cron
125 public function __construct($name, $timeout = NULL, $serverWideLock = FALSE) {
126 $config = CRM_Core_Config
::singleton();
127 $dsnArray = DB
::parseDSN($config->dsn
);
128 $database = $dsnArray['database'];
129 $domainID = CRM_Core_Config
::domainID();
130 if ($serverWideLock) {
131 $this->_name
= $name;
134 $this->_name
= $database . '.' . $domainID . '.' . $name;
136 // MySQL 5.7 doesn't like long lock names so creating a lock id
137 $this->_id
= sha1($this->_name
);
138 if (defined('CIVICRM_LOCK_DEBUG')) {
139 CRM_Core_Error
::debug_log_message('trying to construct lock for ' . $this->_name
. '(' . $this->_id
. ')');
141 $this->_timeout
= $timeout !== NULL ?
$timeout : self
::TIMEOUT
;
144 public function __destruct() {
151 * The advantage of mysql locks is that they can be used across processes. However, only one
152 * can be used at once within a process. An attempt to use a second one within a process
153 * prior to mysql 5.7.5 results in the first being released.
155 * The process here is
156 * 1) first attempt to grab a lock for a mailing job - self::jobLog will be populated with the
157 * lock id & a mysql lock will be created for the ID.
159 * If a second function in the same process attempts to grab the lock it will enter the hackyHandleBrokenCode routine
160 * which says 'I won't break a mailing lock for you but if you are not a civimail send process I'll let you
161 * pretend you have a lock already and you can go ahead with whatever you were doing under the delusion you
164 * @todo bypass hackyHandleBrokenCode for mysql version 5.7.5+
166 * If a second function in a separate process attempts to grab the lock already in use it should be rejected,
167 * but it appears it IS allowed to grab a different lock & unlike in the same process the first lock won't be released.
169 * All this means CiviMail locks are first class citizens & any other process gets a 'best effort lock'.
171 * @todo document naming convention for CiviMail locks as this is key to ensuring they work properly.
173 * @param int $timeout
176 * @throws \CRM_Core_Exception
178 public function acquire($timeout = NULL) {
179 if (!$this->_hasLock
) {
180 if (self
::$jobLog && CRM_Core_DAO
::singleValueQuery("SELECT IS_USED_LOCK( '" . self
::$jobLog . "')")) {
181 return $this->hackyHandleBrokenCode(self
::$jobLog);
184 $query = "SELECT GET_LOCK( %1, %2 )";
186 1 => array($this->_id
, 'String'),
187 2 => array($timeout ?
$timeout : $this->_timeout
, 'Integer'),
189 $res = CRM_Core_DAO
::singleValueQuery($query, $params);
191 if (defined('CIVICRM_LOCK_DEBUG')) {
192 CRM_Core_Error
::debug_log_message('acquire lock for ' . $this->_name
. '(' . $this->_id
. ')');
194 $this->_hasLock
= TRUE;
195 if (stristr($this->_name
, 'data.mailing.job.')) {
196 self
::$jobLog = $this->_id
;
200 if (defined('CIVICRM_LOCK_DEBUG')) {
201 CRM_Core_Error
::debug_log_message('failed to acquire lock for ' . $this->_name
. '(' . $this->_id
. ')');
205 return $this->_hasLock
;
209 * @return null|string
211 public function release() {
212 if ($this->_hasLock
) {
213 if (defined('CIVICRM_LOCK_DEBUG')) {
214 CRM_Core_Error
::debug_log_message('release lock for ' . $this->_name
. '(' . $this->_id
. ')');
216 $this->_hasLock
= FALSE;
218 if (self
::$jobLog == $this->_id
) {
219 self
::$jobLog = FALSE;
222 $query = "SELECT RELEASE_LOCK( %1 )";
223 $params = array(1 => array($this->_id
, 'String'));
224 return CRM_Core_DAO
::singleValueQuery($query, $params);
229 * @return null|string
231 public function isFree() {
232 $query = "SELECT IS_FREE_LOCK( %1 )";
233 $params = array(1 => array($this->_id
, 'String'));
234 return CRM_Core_DAO
::singleValueQuery($query, $params);
240 public function isAcquired() {
241 return $this->_hasLock
;
245 * CRM-12856 locks were originally set up for jobs, but the concept was extended to caching & groups without
246 * understanding that would undermine the job locks (because grabbing a lock implicitly releases existing ones)
247 * this is all a big hack to mitigate the impact of that - but should not be seen as a fix. Not sure correct fix
248 * but maybe locks should be used more selectively? Or else we need to handle is some cool way that Tim is yet to write :-)
249 * if we are running in the context of the cron log then we would rather die (or at least let our process die)
250 * than release that lock - so if the attempt is being made by setCache or something relatively trivial
251 * we'll just return TRUE, but if it's another job then we will crash as that seems 'safer'
253 * @param string $jobLog
254 * @throws CRM_Core_Exception
257 public function hackyHandleBrokenCode($jobLog) {
258 if (stristr($this->_name
, 'job')) {
259 CRM_Core_Error
::debug_log_message('lock acquisition for ' . $this->_name
. '(' . $this->_id
. ')' . ' attempted when ' . $jobLog . ' is not released');
260 throw new CRM_Core_Exception('lock acquisition for ' . $this->_name
. '(' . $this->_id
. ')' . ' attempted when ' . $jobLog . ' is not released');
262 if (defined('CIVICRM_LOCK_DEBUG')) {
263 CRM_Core_Error
::debug_log_message('(CRM-12856) faking lock for ' . $this->_name
. '(' . $this->_id
. ')');
265 $this->_hasLock
= TRUE;