Merge pull request #10796 from tschuettler/CRM-20995
[civicrm-core.git] / CRM / Core / Lock.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
7e9e8871 4 | CiviCRM version 4.7 |
6a488035 5 +--------------------------------------------------------------------+
0f03f337 6 | Copyright CiviCRM LLC (c) 2004-2017 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
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. |
13 | |
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. |
18 | |
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 *
30 * @package CRM
0f03f337 31 * @copyright CiviCRM LLC (c) 2004-2017
6a488035
TO
32 * $Id$
33 *
34 */
10760fa1 35class CRM_Core_Lock implements \Civi\Core\Lock\LockInterface {
6a488035 36
8bf889ef
EM
37 /**
38 * This variable (despite it's name) roughly translates to 'lock that we actually care about'.
39 *
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'.
42 *
43 * See further comments on the aquire function.
44 *
45 * @var bool
46 */
722ce4f9
TO
47 static $jobLog = FALSE;
48
6a488035 49 // lets have a 3 second timeout for now
7da04cde 50 const TIMEOUT = 3;
6a488035
TO
51
52 protected $_hasLock = FALSE;
53
54 protected $_name;
55
8bf889ef
EM
56 protected $_id;
57
10760fa1
TO
58 /**
59 * Use MySQL's GET_LOCK(). Locks are shared across all Civi instances
60 * on the same MySQL server.
61 *
62 * @param string $name
63 * Symbolic name for the lock. Names generally look like
64 * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}").
65 *
66 * Categories: worker|data|cache|...
67 * Component: core|mailing|member|contribute|...
68 * @return \Civi\Core\Lock\LockInterface
69 */
70 public static function createGlobalLock($name) {
71 return new static($name, NULL, TRUE);
72 }
73
74 /**
75 * Use MySQL's GET_LOCK(), but apply prefixes to the lock names.
76 * Locks are unique to each instance of Civi.
77 *
78 * @param string $name
79 * Symbolic name for the lock. Names generally look like
80 * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}").
81 *
82 * Categories: worker|data|cache|...
83 * Component: core|mailing|member|contribute|...
84 * @return \Civi\Core\Lock\LockInterface
85 */
86 public static function createScopedLock($name) {
87 return new static($name);
88 }
89
90 /**
91 * Use MySQL's GET_LOCK(), but conditionally apply prefixes to the lock names
92 * (if civimail_server_wide_lock is disabled).
93 *
94 * @param string $name
95 * Symbolic name for the lock. Names generally look like
96 * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}").
97 *
98 * Categories: worker|data|cache|...
99 * Component: core|mailing|member|contribute|...
100 * @return \Civi\Core\Lock\LockInterface
101 * @deprecated
102 */
103 public static function createCivimailLock($name) {
aaffa79f 104 $serverWideLock = \Civi::settings()->get('civimail_server_wide_lock');
10760fa1
TO
105 return new static($name, NULL, $serverWideLock);
106 }
107
6a488035
TO
108 /**
109 * Initialize the constants used during lock acquire / release
110 *
6a0b768e 111 * @param string $name
10760fa1
TO
112 * Symbolic name for the lock. Names generally look like
113 * "worker.mailing.EmailProcessor" ("{category}.{component}.{AdhocName}").
114 *
115 * Categories: worker|data|cache|...
116 * Component: core|mailing|member|contribute|...
6a0b768e
TO
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.
10760fa1
TO
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
123 * jobs - CRM-91XX
6a488035 124 */
00be9182 125 public function __construct($name, $timeout = NULL, $serverWideLock = FALSE) {
e32e3c19 126 $config = CRM_Core_Config::singleton();
6a488035
TO
127 $dsnArray = DB::parseDSN($config->dsn);
128 $database = $dsnArray['database'];
129 $domainID = CRM_Core_Config::domainID();
130 if ($serverWideLock) {
131 $this->_name = $name;
132 }
133 else {
134 $this->_name = $database . '.' . $domainID . '.' . $name;
135 }
8bf889ef
EM
136 // MySQL 5.7 doesn't like long lock names so creating a lock id
137 $this->_id = sha1($this->_name);
f2da77e6 138 if (defined('CIVICRM_LOCK_DEBUG')) {
8bf889ef 139 CRM_Core_Error::debug_log_message('trying to construct lock for ' . $this->_name . '(' . $this->_id . ')');
f2da77e6 140 }
6a488035 141 $this->_timeout = $timeout !== NULL ? $timeout : self::TIMEOUT;
6a488035
TO
142 }
143
00be9182 144 public function __destruct() {
6a488035
TO
145 $this->release();
146 }
147
a0ee3941 148 /**
ad37ac8e 149 * Acquire lock.
150 *
8bf889ef
EM
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.
154 *
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.
158 *
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
162 * have a lock.
163 *
164 * @todo bypass hackyHandleBrokenCode for mysql version 5.7.5+
165 *
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.
168 *
169 * All this means CiviMail locks are first class citizens & any other process gets a 'best effort lock'.
170 *
171 * @todo document naming convention for CiviMail locks as this is key to ensuring they work properly.
172 *
ad37ac8e 173 * @param int $timeout
174 *
a0ee3941 175 * @return bool
ad37ac8e 176 * @throws \CRM_Core_Exception
a0ee3941 177 */
10760fa1 178 public function acquire($timeout = NULL) {
6a488035 179 if (!$this->_hasLock) {
c1374ea1
TO
180 if (self::$jobLog && CRM_Core_DAO::singleValueQuery("SELECT IS_USED_LOCK( '" . self::$jobLog . "')")) {
181 return $this->hackyHandleBrokenCode(self::$jobLog);
182 }
183
6a488035 184 $query = "SELECT GET_LOCK( %1, %2 )";
e32e3c19 185 $params = array(
8bf889ef 186 1 => array($this->_id, 'String'),
10760fa1 187 2 => array($timeout ? $timeout : $this->_timeout, 'Integer'),
6a488035
TO
188 );
189 $res = CRM_Core_DAO::singleValueQuery($query, $params);
190 if ($res) {
722ce4f9 191 if (defined('CIVICRM_LOCK_DEBUG')) {
8bf889ef 192 CRM_Core_Error::debug_log_message('acquire lock for ' . $this->_name . '(' . $this->_id . ')');
722ce4f9 193 }
6a488035 194 $this->_hasLock = TRUE;
c1374ea1 195 if (stristr($this->_name, 'data.mailing.job.')) {
8bf889ef 196 self::$jobLog = $this->_id;
c1374ea1 197 }
6a488035 198 }
722ce4f9
TO
199 else {
200 if (defined('CIVICRM_LOCK_DEBUG')) {
8bf889ef 201 CRM_Core_Error::debug_log_message('failed to acquire lock for ' . $this->_name . '(' . $this->_id . ')');
722ce4f9
TO
202 }
203 }
6a488035
TO
204 }
205 return $this->_hasLock;
206 }
207
a0ee3941
EM
208 /**
209 * @return null|string
210 */
00be9182 211 public function release() {
6a488035 212 if ($this->_hasLock) {
10760fa1 213 if (defined('CIVICRM_LOCK_DEBUG')) {
8bf889ef 214 CRM_Core_Error::debug_log_message('release lock for ' . $this->_name . '(' . $this->_id . ')');
10760fa1 215 }
6a488035
TO
216 $this->_hasLock = FALSE;
217
8bf889ef 218 if (self::$jobLog == $this->_id) {
722ce4f9
TO
219 self::$jobLog = FALSE;
220 }
221
6a488035 222 $query = "SELECT RELEASE_LOCK( %1 )";
8bf889ef 223 $params = array(1 => array($this->_id, 'String'));
6a488035
TO
224 return CRM_Core_DAO::singleValueQuery($query, $params);
225 }
226 }
227
a0ee3941
EM
228 /**
229 * @return null|string
230 */
00be9182 231 public function isFree() {
6a488035 232 $query = "SELECT IS_FREE_LOCK( %1 )";
8bf889ef 233 $params = array(1 => array($this->_id, 'String'));
6a488035
TO
234 return CRM_Core_DAO::singleValueQuery($query, $params);
235 }
236
a0ee3941
EM
237 /**
238 * @return bool
239 */
00be9182 240 public function isAcquired() {
6a488035
TO
241 return $this->_hasLock;
242 }
f877d534
E
243
244 /**
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'
252 *
253 * @param string $jobLog
254 * @throws CRM_Core_Exception
28a04ea9 255 * @return bool
f877d534 256 */
00be9182 257 public function hackyHandleBrokenCode($jobLog) {
e32e3c19 258 if (stristr($this->_name, 'job')) {
8bf889ef
EM
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');
f877d534 261 }
f2da77e6 262 if (defined('CIVICRM_LOCK_DEBUG')) {
8bf889ef 263 CRM_Core_Error::debug_log_message('(CRM-12856) faking lock for ' . $this->_name . '(' . $this->_id . ')');
f2da77e6 264 }
f877d534
E
265 $this->_hasLock = TRUE;
266 return TRUE;
267 }
96025800 268
6a488035 269}