PageRenderTime 43ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/lock.inc

https://github.com/Crell/drupal-oo-hooks
Pascal | 252 lines | 218 code | 4 blank | 30 comment | 2 complexity | fe2d84b01652aace7e16c4a410aa11e8 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. // $Id$
  3. /**
  4. * @file
  5. * A database-mediated implementation of a locking mechanism.
  6. */
  7. /**
  8. * @defgroup lock Locking mechanisms
  9. * @{
  10. * Functions to coordinate long-running operations across requests.
  11. *
  12. * In most environments, multiple Drupal page requests (a.k.a. threads or
  13. * processes) will execute in parallel. This leads to potential conflicts or
  14. * race conditions when two requests execute the same code at the same time. A
  15. * common example of this is a rebuild like menu_rebuild() where we invoke many
  16. * hook implementations to get and process data from all active modules, and
  17. * then delete the current data in the database to insert the new afterwards.
  18. *
  19. * This is a cooperative, advisory lock system. Any long-running operation
  20. * that could potentially be attempted in parallel by multiple requests should
  21. * try to acquire a lock before proceeding. By obtaining a lock, one request
  22. * notifies any other requests that a specific operation is in progress which
  23. * must not be executed in parallel.
  24. *
  25. * To use this API, pick a unique name for the lock. A sensible choice is the
  26. * name of the function performing the operation. A very simple example use of
  27. * this API:
  28. * @code
  29. * function mymodule_long_operation() {
  30. * if (lock_acquire('mymodule_long_operation')) {
  31. * // Do the long operation here.
  32. * // ...
  33. * lock_release('mymodule_long_operation');
  34. * }
  35. * }
  36. * @endcode
  37. *
  38. * If a function acquires a lock it should always release it when the
  39. * operation is complete by calling lock_release(), as in the example.
  40. *
  41. * A function that has acquired a lock may attempt to renew a lock (extend the
  42. * duration of the lock) by calling lock_acquire() again during the operation.
  43. * Failure to renew a lock is indicative that another request has acquired
  44. * the lock, and that the current operation may need to be aborted.
  45. *
  46. * If a function fails to acquire a lock it may either immediately return, or
  47. * it may call lock_wait() if the rest of the current page request requires
  48. * that the operation in question be complete. After lock_wait() returns,
  49. * the function may again attempt to acquire the lock, or may simply allow the
  50. * page request to proceed on the assumption that a parallel request completed
  51. * the operation.
  52. *
  53. * lock_acquire() and lock_wait() will automatically break (delete) a lock
  54. * whose duration has exceeded the timeout specified when it was acquired.
  55. *
  56. * Alternative implementations of this API (such as APC) may be substituted
  57. * by setting the 'lock_inc' variable to an alternate include filepath. Since
  58. * this is an API intended to support alternative implementations, code using
  59. * this API should never rely upon specific implementation details (for example
  60. * no code should look for or directly modify a lock in the {semaphore} table).
  61. */
  62. /**
  63. * Initialize the locking system.
  64. */
  65. function lock_initialize() {
  66. global $locks;
  67. $locks = array();
  68. }
  69. /**
  70. * Helper function to get this request's unique id.
  71. */
  72. function _lock_id() {
  73. $lock_id = &drupal_static(__FUNCTION__);
  74. if (!isset($lock_id)) {
  75. // Assign a unique id.
  76. $lock_id = uniqid(mt_rand(), TRUE);
  77. // We only register a shutdown function if a lock is used.
  78. drupal_register_shutdown_function('lock_release_all', $lock_id);
  79. }
  80. return $lock_id;
  81. }
  82. /**
  83. * Acquire (or renew) a lock, but do not block if it fails.
  84. *
  85. * @param $name
  86. * The name of the lock.
  87. * @param $timeout
  88. * A number of seconds (float) before the lock expires (minimum of 0.001).
  89. *
  90. * @return
  91. * TRUE if the lock was acquired, FALSE if it failed.
  92. */
  93. function lock_acquire($name, $timeout = 30.0) {
  94. global $locks;
  95. // Insure that the timeout is at least 1 ms.
  96. $timeout = max($timeout, 0.001);
  97. $expire = microtime(TRUE) + $timeout;
  98. if (isset($locks[$name])) {
  99. // Try to extend the expiration of a lock we already acquired.
  100. $success = (bool) db_update('semaphore')
  101. ->fields(array('expire' => $expire))
  102. ->condition('name', $name)
  103. ->condition('value', _lock_id())
  104. ->execute();
  105. if (!$success) {
  106. // The lock was broken.
  107. unset($locks[$name]);
  108. }
  109. return $success;
  110. }
  111. else {
  112. // Optimistically try to acquire the lock, then retry once if it fails.
  113. // The first time through the loop cannot be a retry.
  114. $retry = FALSE;
  115. // We always want to do this code at least once.
  116. do {
  117. try {
  118. db_insert('semaphore')
  119. ->fields(array(
  120. 'name' => $name,
  121. 'value' => _lock_id(),
  122. 'expire' => $expire,
  123. ))
  124. ->execute();
  125. // We track all acquired locks in the global variable.
  126. $locks[$name] = TRUE;
  127. // We never need to try again.
  128. $retry = FALSE;
  129. }
  130. catch (PDOException $e) {
  131. // Suppress the error. If this is our first pass through the loop,
  132. // then $retry is FALSE. In this case, the insert must have failed
  133. // meaning some other request acquired the lock but did not release it.
  134. // We decide whether to retry by checking lock_may_be_available()
  135. // Since this will break the lock in case it is expired.
  136. $retry = $retry ? FALSE : lock_may_be_available($name);
  137. }
  138. // We only retry in case the first attempt failed, but we then broke
  139. // an expired lock.
  140. } while ($retry);
  141. }
  142. return isset($locks[$name]);
  143. }
  144. /**
  145. * Check if lock acquired by a different process may be available.
  146. *
  147. * If an existing lock has expired, it is removed.
  148. *
  149. * @param $name
  150. * The name of the lock.
  151. *
  152. * @return
  153. * TRUE if there is no lock or it was removed, FALSE otherwise.
  154. */
  155. function lock_may_be_available($name) {
  156. $lock = db_query('SELECT expire, value FROM {semaphore} WHERE name = :name', array(':name' => $name))->fetchAssoc();
  157. if (!$lock) {
  158. return TRUE;
  159. }
  160. $expire = (float) $lock['expire'];
  161. $now = microtime(TRUE);
  162. if ($now > $expire) {
  163. // We check two conditions to prevent a race condition where another
  164. // request acquired the lock and set a new expire time. We add a small
  165. // number to $expire to avoid errors with float to string conversion.
  166. return (bool) db_delete('semaphore')
  167. ->condition('name', $name)
  168. ->condition('value', $lock['value'])
  169. ->condition('expire', 0.0001 + $expire, '<=')
  170. ->execute();
  171. }
  172. return FALSE;
  173. }
  174. /**
  175. * Wait for a lock to be available.
  176. *
  177. * This function may be called in a request that fails to acquire a desired
  178. * lock. This will block further execution until the lock is available or the
  179. * specified delay in seconds is reached. This should not be used with locks
  180. * that are acquired very frequently, since the lock is likely to be acquired
  181. * again by a different request during the sleep().
  182. *
  183. * @param $name
  184. * The name of the lock.
  185. * @param $delay
  186. * The maximum number of seconds to wait, as an integer.
  187. *
  188. * @return
  189. * TRUE if the lock holds, FALSE if it is available.
  190. */
  191. function lock_wait($name, $delay = 30) {
  192. $delay = (int) $delay;
  193. while ($delay--) {
  194. // This function should only be called by a request that failed to get a
  195. // lock, so we sleep first to give the parallel request a chance to finish
  196. // and release the lock.
  197. sleep(1);
  198. if (lock_may_be_available($name)) {
  199. // No longer need to wait.
  200. return FALSE;
  201. }
  202. }
  203. // The caller must still wait longer to get the lock.
  204. return TRUE;
  205. }
  206. /**
  207. * Release a lock previously acquired by lock_acquire().
  208. *
  209. * This will release the named lock if it is still held by the current request.
  210. *
  211. * @param $name
  212. * The name of the lock.
  213. */
  214. function lock_release($name) {
  215. global $locks;
  216. unset($locks[$name]);
  217. db_delete('semaphore')
  218. ->condition('name', $name)
  219. ->condition('value', _lock_id())
  220. ->execute();
  221. }
  222. /**
  223. * Release all previously acquired locks.
  224. */
  225. function lock_release_all($lock_id = NULL) {
  226. global $locks;
  227. $locks = array();
  228. if (empty($lock_id)) {
  229. $lock_id = _lock_id();
  230. }
  231. db_delete('semaphore')
  232. ->condition('value', $lock_id)
  233. ->execute();
  234. }
  235. /**
  236. * @} End of "defgroup lock".
  237. */