[TASK] Improve Locker
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Locking / Locker.php
1 <?php
2 namespace TYPO3\CMS\Core\Locking;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2008-2013 Michael Stucki (michael@typo3.org)
8 * (c) 2014 Markus Klein <klein.t3@mfc-linz.at>
9 * All rights reserved
10 *
11 * This script is part of the TYPO3 project. The TYPO3 project is
12 * free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
16 *
17 * The GNU General Public License can be found at
18 * http://www.gnu.org/copyleft/gpl.html.
19 * A copy is found in the text file GPL.txt and important notices to the license
20 * from the author is found in LICENSE.txt distributed with these scripts.
21 *
22 *
23 * This script is distributed in the hope that it will be useful,
24 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 * GNU General Public License for more details.
27 *
28 * This copyright notice MUST APPEAR in all copies of the script!
29 ***************************************************************/
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31
32 /**
33 * TYPO3 locking class
34 * This class provides an abstract layer to various locking features for TYPO3
35 *
36 * It is intended to blocks requests until some data has been generated.
37 * This is especially useful if two clients are requesting the same website short after each other. While the request of client 1 triggers building and caching of the website, client 2 will be waiting at this lock.
38 *
39 * @author Michael Stucki <michael@typo3.org>
40 * @author Markus Klein <klein.t3@mfc-linz.at>
41 */
42 class Locker {
43
44 const LOCKING_METHOD_SIMPLE = 'simple';
45 const LOCKING_METHOD_FLOCK = 'flock';
46 const LOCKING_METHOD_SEMAPHORE = 'semaphore';
47 const LOCKING_METHOD_DISABLED = 'disable';
48
49 const FILE_LOCK_FOLDER = 'typo3temp/locks/';
50
51 /**
52 * @var string Locking method: One of the constants above
53 */
54 protected $method = '';
55
56 /**
57 * @var mixed Identifier used for this lock
58 */
59 protected $id;
60
61 /**
62 * @var mixed Resource used for this lock (can be a file or a semaphore resource)
63 */
64 protected $resource;
65
66 /**
67 * @var resource File pointer if using flock method
68 */
69 protected $filePointer;
70
71 /**
72 * @var boolean True if lock is acquired
73 */
74 protected $isAcquired = FALSE;
75
76 /**
77 * @var integer Number of times a locked resource is tried to be acquired. Only used in manual locks method "simple".
78 */
79 protected $loops = 150;
80
81 /**
82 * @var integer Milliseconds after lock acquire is retried. $loops * $step results in the maximum delay of a lock. Only used in manual lock method "simple".
83 */
84 protected $step = 200;
85
86 /**
87 * @var string Logging facility
88 */
89 protected $syslogFacility = 'cms';
90
91 /**
92 * @var boolean True if locking should be logged
93 */
94 protected $isLoggingEnabled = TRUE;
95
96 /**
97 * Constructor:
98 * initializes locking, check input parameters and set variables accordingly.
99 *
100 * Parameters $loops and $step only apply to the locking method LOCKING_METHOD_SIMPLE.
101 *
102 * @param string $id ID to identify this lock in the system
103 * @param string $method Define which locking method to use. Use one of the LOCKING_METHOD_* constants. Defaults to LOCKING_METHOD_SIMPLE. Use '' to use setting from Install Tool.
104 * @param int $loops Number of times a locked resource is tried to be acquired.
105 * @param int $step Milliseconds after lock acquire is retried. $loops * $step results in the maximum delay of a lock.
106 * @throws \RuntimeException
107 * @throws \InvalidArgumentException
108 */
109 public function __construct($id, $method = self::LOCKING_METHOD_SIMPLE, $loops = 0, $step = 0) {
110 // Force ID to be string
111 $id = (string)$id;
112 if ((int)$loops) {
113 $this->loops = (int)$loops;
114 }
115 if ((int)$step) {
116 $this->step = (int)$step;
117 }
118 if ($method === '' && isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['lockingMode'])) {
119 $method = (string)$GLOBALS['TYPO3_CONF_VARS']['SYS']['lockingMode'];
120 }
121
122 switch ($method) {
123 case self::LOCKING_METHOD_SIMPLE:
124 // intended fall through
125 case self::LOCKING_METHOD_FLOCK:
126 $path = PATH_site . self::FILE_LOCK_FOLDER;
127 if (!is_dir($path)) {
128 GeneralUtility::mkdir($path);
129 }
130 $this->id = md5($id);
131 $this->resource = $path . $this->id;
132 break;
133 case self::LOCKING_METHOD_SEMAPHORE:
134 $this->id = abs(crc32($id));
135 break;
136 case self::LOCKING_METHOD_DISABLED:
137 break;
138 default:
139 throw new \InvalidArgumentException('No such locking method "' . $method . '"', 1294586097);
140 }
141 $this->method = $method;
142 }
143
144 /**
145 * Destructor:
146 * Releases lock automatically when instance is destroyed and release resources
147 */
148 public function __destruct() {
149 $this->release();
150 switch ($this->method) {
151 case self::LOCKING_METHOD_FLOCK:
152 if (
153 GeneralUtility::isAllowedAbsPath($this->resource)
154 && GeneralUtility::isFirstPartOfStr($this->resource, PATH_site . self::FILE_LOCK_FOLDER)
155 ) {
156 @unlink($this->resource);
157 }
158 break;
159 case self::LOCKING_METHOD_SEMAPHORE:
160 @sem_remove($this->resource);
161 break;
162 default:
163 // do nothing
164 }
165 }
166
167 /**
168 * Tries to allocate the semaphore
169 *
170 * @return void
171 * @throws \RuntimeException
172 */
173 protected function getSemaphore() {
174 $this->resource = sem_get($this->id, 1);
175 if ($this->resource === FALSE) {
176 throw new \RuntimeException('Unable to get semaphore with id ' . $this->id, 1313828196);
177 }
178 }
179
180 /**
181 * Acquire a lock and return when successful.
182 *
183 * It is important to know that the lock will be acquired in any case, even if the request was blocked first.
184 * Therefore, the lock needs to be released in every situation.
185 *
186 * @return boolean Returns TRUE if lock could be acquired without waiting, FALSE otherwise.
187 * @throws \RuntimeException
188 * @deprecated since 6.2 - will be removed two versions later; use new API instead
189 */
190 public function acquire() {
191 // TODO refactor locking in TSFE to use the new API, then this call can be logged
192 // GeneralUtility::logDeprecatedFunction();
193
194 // Default is TRUE, which means continue without caring for other clients.
195 // In the case of TYPO3s cache management, this has no negative effect except some resource overhead.
196 $noWait = FALSE;
197 $isAcquired = FALSE;
198 switch ($this->method) {
199 case self::LOCKING_METHOD_SIMPLE:
200 if (file_exists($this->resource)) {
201 $this->sysLog('Waiting for a different process to release the lock');
202 $maxExecutionTime = (int)ini_get('max_execution_time');
203 $maxAge = time() - ($maxExecutionTime ?: 120);
204 if (@filectime($this->resource) < $maxAge) {
205 @unlink($this->resource);
206 $this->sysLog('Unlinking stale lockfile');
207 }
208 }
209 for ($i = 0; $i < $this->loops; $i++) {
210 $filePointer = @fopen($this->resource, 'x');
211 if ($filePointer !== FALSE) {
212 fclose($filePointer);
213 GeneralUtility::fixPermissions($this->resource);
214 $this->sysLog('Lock acquired');
215 $noWait = $i === 0;
216 $isAcquired = TRUE;
217 break;
218 }
219 usleep($this->step * 1000);
220 }
221 if (!$isAcquired) {
222 throw new \RuntimeException('Lock file could not be created', 1294586098);
223 }
224 break;
225 case self::LOCKING_METHOD_FLOCK:
226 $this->filePointer = fopen($this->resource, 'c');
227 if ($this->filePointer === FALSE) {
228 throw new \RuntimeException('Lock file could not be opened', 1294586099);
229 }
230 // Lock without blocking
231 if (flock($this->filePointer, LOCK_EX | LOCK_NB)) {
232 $noWait = TRUE;
233 } elseif (flock($this->filePointer, LOCK_EX)) {
234 // Lock with blocking (waiting for similar locks to become released)
235 $noWait = FALSE;
236 } else {
237 throw new \RuntimeException('Could not lock file "' . $this->resource . '"', 1294586100);
238 }
239 $isAcquired = TRUE;
240 break;
241 case self::LOCKING_METHOD_SEMAPHORE:
242 $this->getSemaphore();
243 while (!$isAcquired) {
244 if (@sem_acquire($this->resource)) {
245 // Unfortunately it is not possible to find out if the request has blocked,
246 // as sem_acquire will block until we get the resource.
247 // So we do not set $noWait here at all
248 $isAcquired = TRUE;
249 }
250 }
251 break;
252 case self::LOCKING_METHOD_DISABLED:
253 break;
254 default:
255 // will never be reached
256 }
257 $this->isAcquired = $isAcquired;
258 return $noWait;
259 }
260
261 /**
262 * Try to acquire an exclusive lock
263 *
264 * @throws \RuntimeException
265 * @return bool Returns TRUE if the lock was acquired successfully
266 */
267 public function acquireExclusiveLock() {
268 if ($this->isAcquired) {
269 return TRUE;
270 }
271 $this->isAcquired = FALSE;
272 switch ($this->method) {
273 case self::LOCKING_METHOD_SIMPLE:
274 if (file_exists($this->resource)) {
275 $this->sysLog('Waiting for a different process to release the lock');
276 $maxExecutionTime = (int)ini_get('max_execution_time');
277 $maxAge = time() - ($maxExecutionTime ?: 120);
278 if (@filectime($this->resource) < $maxAge) {
279 @unlink($this->resource);
280 $this->sysLog('Unlinking stale lockfile');
281 }
282 }
283 for ($i = 0; $i < $this->loops; $i++) {
284 $filePointer = @fopen($this->resource, 'x');
285 if ($filePointer !== FALSE) {
286 fclose($filePointer);
287 GeneralUtility::fixPermissions($this->resource);
288 $this->sysLog('Lock acquired');
289 $this->isAcquired = TRUE;
290 break;
291 }
292 usleep($this->step * 1000);
293 }
294 break;
295 case self::LOCKING_METHOD_FLOCK:
296 $this->filePointer = fopen($this->resource, 'c');
297 if ($this->filePointer === FALSE) {
298 throw new \RuntimeException('Lock file could not be opened', 1294586099);
299 }
300 if (flock($this->filePointer, LOCK_EX)) {
301 $this->isAcquired = TRUE;
302 }
303 break;
304 case self::LOCKING_METHOD_SEMAPHORE:
305 $this->getSemaphore();
306 if (@sem_acquire($this->resource)) {
307 $this->isAcquired = TRUE;
308 }
309 break;
310 case self::LOCKING_METHOD_DISABLED:
311 break;
312 default:
313 // will never be reached
314 }
315 return $this->isAcquired;
316 }
317
318 /**
319 * Try to acquire a shared lock
320 *
321 * (Only works for the flock() locking method currently)
322 *
323 * @return bool Returns TRUE if the lock was acquired successfully
324 * @throws \RuntimeException
325 */
326 public function acquireSharedLock() {
327 if ($this->isAcquired) {
328 return TRUE;
329 }
330 $isAcquired = FALSE;
331 if ($this->method === self::LOCKING_METHOD_FLOCK) {
332 $this->filePointer = fopen($this->resource, 'c');
333 if ($this->filePointer === FALSE) {
334 throw new \RuntimeException('Lock file could not be opened', 1294586099);
335 }
336 if (flock($this->filePointer, LOCK_SH)) {
337 $isAcquired = TRUE;
338 }
339 }
340 return $isAcquired;
341 }
342
343 /**
344 * Release the lock
345 *
346 * @return boolean Returns TRUE on success or FALSE on failure
347 */
348 public function release() {
349 if (!$this->isAcquired) {
350 return TRUE;
351 }
352 $success = TRUE;
353 switch ($this->method) {
354 case self::LOCKING_METHOD_SIMPLE:
355 if (
356 GeneralUtility::isAllowedAbsPath($this->resource)
357 && GeneralUtility::isFirstPartOfStr($this->resource, PATH_site . self::FILE_LOCK_FOLDER)
358 ) {
359 if (@unlink($this->resource) === FALSE) {
360 $success = FALSE;
361 }
362 }
363 break;
364 case self::LOCKING_METHOD_FLOCK:
365 if (is_resource($this->filePointer)) {
366 if (flock($this->filePointer, LOCK_UN) === FALSE) {
367 $success = FALSE;
368 }
369 fclose($this->filePointer);
370 }
371 break;
372 case self::LOCKING_METHOD_SEMAPHORE:
373 if (!@sem_release($this->resource)) {
374 $success = FALSE;
375 }
376 break;
377 case self::LOCKING_METHOD_DISABLED:
378 break;
379 default:
380 // will never be reached
381 }
382 $this->isAcquired = FALSE;
383 return $success;
384 }
385
386 /**
387 * Return the locking method which is currently used
388 *
389 * @return string Locking method
390 */
391 public function getMethod() {
392 return $this->method;
393 }
394
395 /**
396 * Return the ID which is currently used
397 *
398 * @return string Locking ID
399 */
400 public function getId() {
401 return $this->id;
402 }
403
404 /**
405 * Return the resource which is currently used.
406 * Depending on the locking method this can be a filename or a semaphore resource.
407 *
408 * @return mixed Locking resource (filename as string or semaphore as resource)
409 */
410 public function getResource() {
411 return $this->resource;
412 }
413
414 /**
415 * Return the local status of a lock
416 *
417 * @return bool Returns TRUE if lock is acquired by this process, FALSE otherwise
418 */
419 public function getLockStatus() {
420 return $this->isAcquired;
421 }
422
423 /**
424 * Return the global status of the lock
425 *
426 * @return bool Returns TRUE if the lock is locked by either this or another process, FALSE otherwise
427 */
428 public function isLocked() {
429 $result = FALSE;
430 switch ($this->method) {
431 case self::LOCKING_METHOD_SIMPLE:
432 if (file_exists($this->resource)) {
433 $maxExecutionTime = (int)ini_get('max_execution_time');
434 $maxAge = time() - ($maxExecutionTime ?: 120);
435 if (@filectime($this->resource) < $maxAge) {
436 @unlink($this->resource);
437 $this->sysLog('Unlinking stale lockfile');
438 } else {
439 $result = TRUE;
440 }
441 }
442 break;
443 case self::LOCKING_METHOD_FLOCK:
444 // we can't detect this reliably here, since the third parameter of flock() does not work on windows
445 break;
446 case self::LOCKING_METHOD_SEMAPHORE:
447 // no way to detect this at all, no PHP API for that
448 break;
449 case self::LOCKING_METHOD_DISABLED:
450 break;
451 default:
452 // will never be reached
453 }
454 return $result;
455 }
456
457 /**
458 * Sets the facility (extension name) for the syslog entry.
459 *
460 * @param string $syslogFacility
461 */
462 public function setSyslogFacility($syslogFacility) {
463 $this->syslogFacility = $syslogFacility;
464 }
465
466 /**
467 * Enable/ disable logging
468 *
469 * @param boolean $isLoggingEnabled
470 */
471 public function setEnableLogging($isLoggingEnabled) {
472 $this->isLoggingEnabled = $isLoggingEnabled;
473 }
474
475 /**
476 * Adds a common log entry for this locking API using \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog().
477 * Example: 25-02-08 17:58 - cms: Locking [simple::0aeafd2a67a6bb8b9543fb9ea25ecbe2]: Acquired
478 *
479 * @param string $message The message to be logged
480 * @param integer $severity Severity - 0 is info (default), 1 is notice, 2 is warning, 3 is error, 4 is fatal error
481 * @return void
482 */
483 public function sysLog($message, $severity = 0) {
484 if ($this->isLoggingEnabled) {
485 GeneralUtility::sysLog('Locking [' . $this->method . '::' . $this->id . ']: ' . trim($message), $this->syslogFacility, $severity);
486 }
487 }
488
489 }