lock.inc

  1. 7.x drupal-7.x/includes/lock.inc
  2. 6.x drupal-6.x/includes/lock.inc

A database-mediated implementation of a locking mechanism.

File

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