Merge pull request #13235 from civicrm/5.8
[civicrm-core.git] / CRM / Core / Lock.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2018 |
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 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2018
32 * $Id$
33 *
34 */
35 class CRM_Core_Lock implements \Civi\Core\Lock\LockInterface {
36
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 */
47 static $jobLog = FALSE;
48
49 // lets have a 3 second timeout for now
50 const TIMEOUT = 3;
51
52 protected $_hasLock = FALSE;
53
54 protected $_name;
55
56 protected $_id;
57
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) {
104 $serverWideLock = \Civi::settings()->get('civimail_server_wide_lock');
105 return new static($name, NULL, $serverWideLock);
106 }
107
108 /**
109 * Initialize the constants used during lock acquire / release
110 *
111 * @param string $name
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|...
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
123 * jobs - CRM-91XX
124 */
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;
132 }
133 else {
134 $this->_name = $database . '.' . $domainID . '.' . $name;
135 }
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 . ')');
140 }
141 $this->_timeout = $timeout !== NULL ? $timeout : self::TIMEOUT;
142 }
143
144 public function __destruct() {
145 $this->release();
146 }
147
148 /**
149 * Acquire lock.
150 *
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 *
173 * @param int $timeout
174 *
175 * @return bool
176 * @throws \CRM_Core_Exception
177 */
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);
182 }
183
184 $query = "SELECT GET_LOCK( %1, %2 )";
185 $params = array(
186 1 => array($this->_id, 'String'),
187 2 => array($timeout ? $timeout : $this->_timeout, 'Integer'),
188 );
189 $res = CRM_Core_DAO::singleValueQuery($query, $params);
190 if ($res) {
191 if (defined('CIVICRM_LOCK_DEBUG')) {
192 CRM_Core_Error::debug_log_message('acquire lock for ' . $this->_name . '(' . $this->_id . ')');
193 }
194 $this->_hasLock = TRUE;
195 if (stristr($this->_name, 'data.mailing.job.')) {
196 self::$jobLog = $this->_id;
197 }
198 }
199 else {
200 if (defined('CIVICRM_LOCK_DEBUG')) {
201 CRM_Core_Error::debug_log_message('failed to acquire lock for ' . $this->_name . '(' . $this->_id . ')');
202 }
203 }
204 }
205 return $this->_hasLock;
206 }
207
208 /**
209 * @return null|string
210 */
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 . ')');
215 }
216 $this->_hasLock = FALSE;
217
218 if (self::$jobLog == $this->_id) {
219 self::$jobLog = FALSE;
220 }
221
222 $query = "SELECT RELEASE_LOCK( %1 )";
223 $params = array(1 => array($this->_id, 'String'));
224 return CRM_Core_DAO::singleValueQuery($query, $params);
225 }
226 }
227
228 /**
229 * @return null|string
230 */
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);
235 }
236
237 /**
238 * @return bool
239 */
240 public function isAcquired() {
241 return $this->_hasLock;
242 }
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
255 * @return bool
256 */
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');
261 }
262 if (defined('CIVICRM_LOCK_DEBUG')) {
263 CRM_Core_Error::debug_log_message('(CRM-12856) faking lock for ' . $this->_name . '(' . $this->_id . ')');
264 }
265 $this->_hasLock = TRUE;
266 return TRUE;
267 }
268
269 }