memcache-lock-code.inc 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. <?php
  2. /**
  3. * @file
  4. * A memcache based implementation of a locking mechanism.
  5. * See includes/lock.inc for documenation
  6. *
  7. * ATTENTION: Don't include this file directly - use the memcache-lock.inc to
  8. * have a failover solution.
  9. */
  10. /**
  11. * Initialize the locking system.
  12. */
  13. function lock_initialize() {
  14. global $locks;
  15. $locks = array();
  16. }
  17. /**
  18. * Acquire (or renew) a lock, but do not block if it fails.
  19. *
  20. * @param string $name
  21. * The name of the lock.
  22. * @param int $timeout
  23. * A number of seconds (int) before the lock expires (minimum of 1).
  24. *
  25. * @return bool
  26. * TRUE if the lock was acquired, FALSE if it failed.
  27. */
  28. function lock_acquire($name, $timeout = 30) {
  29. global $locks;
  30. // Special case variable_init, as on memcache errors we can get stuck in an
  31. // infinite loop.
  32. static $variable_init = 0;
  33. if ($name == 'variable_init') {
  34. if ($variable_init > 25) {
  35. register_shutdown_function('watchdog', 'memcache', 'Broke out of loop trying to grab lock for variable_init.');
  36. return TRUE;
  37. }
  38. $variable_init++;
  39. }
  40. // Ensure that the timeout is at least 1 sec. This is a limitation
  41. // imposed by memcached.
  42. $timeout = (int) max($timeout, 1);
  43. if (dmemcache_add($name, _lock_id(), $timeout, 'semaphore')) {
  44. $locks[$name] = _lock_id();
  45. }
  46. elseif (($result = dmemcache_get($name, 'semaphore')) && isset($locks[$name]) && $locks[$name] == $result) {
  47. // Only renew the lock if we already set it and it has not expired.
  48. dmemcache_set($name, _lock_id(), $timeout, 'semaphore');
  49. }
  50. else {
  51. // Failed to acquire the lock. Unset the key from the $locks array even if
  52. // not set, PHP 5+ allows this without error or warning.
  53. unset($locks[$name]);
  54. }
  55. return isset($locks[$name]);
  56. }
  57. /**
  58. * Check if lock acquired by a different process may be available.
  59. *
  60. * If an existing lock has expired, it is removed.
  61. *
  62. * @param string $name
  63. * The name of the lock.
  64. *
  65. * @return bool
  66. * TRUE if there is no lock or it was removed, FALSE otherwise.
  67. */
  68. function lock_may_be_available($name) {
  69. return !dmemcache_get($name, 'semaphore');
  70. }
  71. /**
  72. * Wait for a lock to be available.
  73. *
  74. * This function may be called in a request that fails to acquire a desired
  75. * lock. This will block further execution until the lock is available or the
  76. * specified delay in seconds is reached. This should not be used with locks
  77. * that are acquired very frequently, since the lock is likely to be acquired
  78. * again by a different request while waiting.
  79. *
  80. * @param string $name
  81. * The name of the lock.
  82. * @param int $delay
  83. * The maximum number of seconds to wait, as an integer.
  84. *
  85. * @return bool
  86. * TRUE if the lock holds, FALSE if it is available.
  87. */
  88. function lock_wait($name, $delay = 30) {
  89. /*
  90. * Pause the process for short periods between calling
  91. * lock_may_be_available(). This prevents hitting the database with constant
  92. * database queries while waiting, which could lead to performance issues.
  93. * However, if the wait period is too long, there is the potential for a
  94. * large number of processes to be blocked waiting for a lock, especially
  95. * if the item being rebuilt is commonly requested. To address both of these
  96. * concerns, begin waiting for 25ms, then add 25ms to the wait period each
  97. * time until it reaches 500ms. After this point polling will continue every
  98. * 500ms until $delay is reached.
  99. */
  100. // $delay is passed in seconds, but we will be using usleep(), which takes
  101. // microseconds as a parameter. Multiply it by 1 million so that all
  102. // further numbers are equivalent.
  103. $delay = (int) $delay * 1000000;
  104. // Begin sleeping at 25ms.
  105. $sleep = 25000;
  106. while ($delay > 0) {
  107. // This function should only be called by a request that failed to get a
  108. // lock, so we sleep first to give the parallel request a chance to finish
  109. // and release the lock.
  110. usleep($sleep);
  111. // After each sleep, increase the value of $sleep until it reaches
  112. // 500ms, to reduce the potential for a lock stampede.
  113. $delay = $delay - $sleep;
  114. $sleep = min(500000, $sleep + 25000, $delay);
  115. if (!dmemcache_get($name, 'semaphore')) {
  116. // No longer need to wait.
  117. return FALSE;
  118. }
  119. }
  120. // The caller must still wait longer to get the lock.
  121. return TRUE;
  122. }
  123. /**
  124. * Release a lock previously acquired by lock_acquire().
  125. *
  126. * This will release the named lock if it is still held by the current request.
  127. *
  128. * @param string $name
  129. * The name of the lock.
  130. */
  131. function lock_release($name) {
  132. global $locks;
  133. if (isset($locks[$name]) && (dmemcache_get($name, 'semaphore') == $locks[$name])) {
  134. dmemcache_delete($name, 'semaphore');
  135. // We unset unconditionally since caller assumes lock is released anyway.
  136. unset($locks[$name]);
  137. }
  138. }
  139. /**
  140. * Generate a unique identifier for locks generated during this request.
  141. */
  142. function _lock_id() {
  143. static $lock_id;
  144. if (!isset($lock_id)) {
  145. $lock_id = uniqid(mt_rand(), TRUE);
  146. // We only register a shutdown function if a lock is used.
  147. register_shutdown_function('lock_release_all', $lock_id);
  148. }
  149. return $lock_id;
  150. }
  151. /**
  152. * Release all locks acquired by this request.
  153. */
  154. function lock_release_all($lock_id = NULL) {
  155. global $locks;
  156. foreach ($locks as $name => $id) {
  157. $value = dmemcache_get($name, 'semaphore');
  158. if ($value == $id) {
  159. dmemcache_delete($name, 'semaphore');
  160. }
  161. }
  162. }