lock.inc 11 KB

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