| 1 |
<?php
|
| 2 |
// $Id$
|
| 3 |
|
| 4 |
/**
|
| 5 |
* @file
|
| 6 |
* A database-mediated implementation of a locking mechanism.
|
| 7 |
*/
|
| 8 |
|
| 9 |
/**
|
| 10 |
* @defgroup lock 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 obtainng 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 |
* A function that has acquired a lock may attempt to renew a lock (extend the
|
| 57 |
* duration of the lock) by calling lock_acquire() again during the operation.
|
| 58 |
* Failure to renew a lock is indicative that another request has acquired
|
| 59 |
* the lock, and that the current operation may need to be aborted.
|
| 60 |
*
|
| 61 |
* Alternative implementations of this API (such as APC) may be substituted
|
| 62 |
* by setting the 'lock_inc' variable to an alternate include filepath. Since
|
| 63 |
* this is an API intended to support alternative implementations, code using
|
| 64 |
* this API should never rely upon specific implementation details (for example
|
| 65 |
* no code should look for or directly modify a lock in the {semaphore} table).
|
| 66 |
*/
|
| 67 |
|
| 68 |
/**
|
| 69 |
* Initialize the locking system.
|
| 70 |
*/
|
| 71 |
function lock_initialize() {
|
| 72 |
global $locks;
|
| 73 |
|
| 74 |
$locks = array();
|
| 75 |
}
|
| 76 |
|
| 77 |
/**
|
| 78 |
* Helper function to get this request's unique id.
|
| 79 |
*/
|
| 80 |
function _lock_id() {
|
| 81 |
$lock_id = &drupal_static(__FUNCTION__);
|
| 82 |
|
| 83 |
if (!isset($lock_id)) {
|
| 84 |
// Assign a unique id.
|
| 85 |
$lock_id = uniqid(mt_rand(), TRUE);
|
| 86 |
// We only register a shutdown function if a lock is used.
|
| 87 |
register_shutdown_function('lock_release_all', $lock_id);
|
| 88 |
}
|
| 89 |
return $lock_id;
|
| 90 |
}
|
| 91 |
|
| 92 |
/**
|
| 93 |
* Acquire (or renew) a lock, but do not block if it fails.
|
| 94 |
*
|
| 95 |
* @param $name
|
| 96 |
* The name of the lock.
|
| 97 |
* @param $timeout
|
| 98 |
* A number of seconds (float) before the lock expires (minimum of 0.001).
|
| 99 |
* @return
|
| 100 |
* TRUE if the lock was acquired, FALSE if it failed.
|
| 101 |
*/
|
| 102 |
function lock_acquire($name, $timeout = 30.0) {
|
| 103 |
global $locks;
|
| 104 |
|
| 105 |
// Insure that the timeout is at least 1 ms.
|
| 106 |
$timeout = max($timeout, 0.001);
|
| 107 |
$expire = microtime(TRUE) + $timeout;
|
| 108 |
if (isset($locks[$name])) {
|
| 109 |
// Try to extend the expiration of a lock we already acquired.
|
| 110 |
$success = (bool) db_update('semaphore')
|
| 111 |
->fields(array('expire' => $expire))
|
| 112 |
->condition('name', $name)
|
| 113 |
->condition('value', _lock_id())
|
| 114 |
->execute();
|
| 115 |
if (!$success) {
|
| 116 |
// The lock was broken.
|
| 117 |
unset($locks[$name]);
|
| 118 |
}
|
| 119 |
return $success;
|
| 120 |
}
|
| 121 |
else {
|
| 122 |
// Optimistically try to acquire the lock, then retry once if it fails.
|
| 123 |
// The first time through the loop cannot be a retry.
|
| 124 |
$retry = FALSE;
|
| 125 |
// We always want to do this code at least once.
|
| 126 |
do {
|
| 127 |
try {
|
| 128 |
db_insert('semaphore')
|
| 129 |
->fields(array(
|
| 130 |
'name' => $name,
|
| 131 |
'value' => _lock_id(),
|
| 132 |
'expire' => $expire,
|
| 133 |
))
|
| 134 |
->execute();
|
| 135 |
// We track all acquired locks in the global variable.
|
| 136 |
$locks[$name] = TRUE;
|
| 137 |
// We never need to try again.
|
| 138 |
$retry = FALSE;
|
| 139 |
}
|
| 140 |
catch (PDOException $e) {
|
| 141 |
// Suppress the error. If this is our first pass through the loop,
|
| 142 |
// then $retry is FALSE. In this case, the insert must have failed
|
| 143 |
// meaning some other request acquired the lock but did not release it.
|
| 144 |
// We decide whether to retry by checking lock_may_be_available()
|
| 145 |
// Since this will break the lock in case it is expired.
|
| 146 |
$retry = $retry ? FALSE : lock_may_be_available($name);
|
| 147 |
}
|
| 148 |
// We only retry in case the first attempt failed, but we then broke
|
| 149 |
// an expired lock.
|
| 150 |
} while ($retry);
|
| 151 |
}
|
| 152 |
return isset($locks[$name]);
|
| 153 |
}
|
| 154 |
|
| 155 |
/**
|
| 156 |
* Check if lock acquired by a different process may be available.
|
| 157 |
*
|
| 158 |
* If an existing lock has expired, it is removed.
|
| 159 |
*
|
| 160 |
* @param $name
|
| 161 |
* The name of the lock.
|
| 162 |
* @return
|
| 163 |
* TRUE if there is no lock or it was removed, FALSE otherwise.
|
| 164 |
*/
|
| 165 |
function lock_may_be_available($name) {
|
| 166 |
$lock = db_query('SELECT expire, value FROM {semaphore} WHERE name = :name', array(':name' => $name))->fetchAssoc();
|
| 167 |
if (!$lock) {
|
| 168 |
return TRUE;
|
| 169 |
}
|
| 170 |
$expire = (float) $lock['expire'];
|
| 171 |
$now = microtime(TRUE);
|
| 172 |
if ($now > $expire) {
|
| 173 |
// We check two conditions to prevent a race condition where another
|
| 174 |
// request acquired the lock and set a new expire time. We add a small
|
| 175 |
// number to $expire to avoid errors with float to string conversion.
|
| 176 |
return (bool) db_delete('semaphore')
|
| 177 |
->condition('name', $name)
|
| 178 |
->condition('value', $lock['value'])
|
| 179 |
->condition('expire', 0.0001 + $expire, '<=')
|
| 180 |
->execute();
|
| 181 |
}
|
| 182 |
return FALSE;
|
| 183 |
}
|
| 184 |
|
| 185 |
/**
|
| 186 |
* Wait for a lock to be available.
|
| 187 |
*
|
| 188 |
* This function may be called in a request that fails to acquire a desired
|
| 189 |
* lock. This will block further execution until the lock is available or the
|
| 190 |
* specified delay in seconds is reached. This should not be used with locks
|
| 191 |
* that are acquired very frequently, since the lock is likely to be acquired
|
| 192 |
* again by a different request during the sleep().
|
| 193 |
*
|
| 194 |
* @param $name
|
| 195 |
* The name of the lock.
|
| 196 |
* @param $delay
|
| 197 |
* The maximum number of seconds to wait, as an integer.
|
| 198 |
* @return
|
| 199 |
* TRUE if the lock holds, FALSE if it is available.
|
| 200 |
*/
|
| 201 |
function lock_wait($name, $delay = 30) {
|
| 202 |
$delay = (int) $delay;
|
| 203 |
while ($delay--) {
|
| 204 |
// This function should only be called by a request that failed to get a
|
| 205 |
// lock, so we sleep first to give the parallel request a chance to finish
|
| 206 |
// and release the lock.
|
| 207 |
sleep(1);
|
| 208 |
if (lock_may_be_available($name)) {
|
| 209 |
// No longer need to wait.
|
| 210 |
return FALSE;
|
| 211 |
}
|
| 212 |
}
|
| 213 |
// The caller must still wait longer to get the lock.
|
| 214 |
return TRUE;
|
| 215 |
}
|
| 216 |
|
| 217 |
/**
|
| 218 |
* Release a lock previously acquired by lock_acquire().
|
| 219 |
*
|
| 220 |
* This will release the named lock if it is still held by the current request.
|
| 221 |
*
|
| 222 |
* @param $name
|
| 223 |
* The name of the lock.
|
| 224 |
*/
|
| 225 |
function lock_release($name) {
|
| 226 |
global $locks;
|
| 227 |
|
| 228 |
unset($locks[$name]);
|
| 229 |
db_delete('semaphore')
|
| 230 |
->condition('name', $name)
|
| 231 |
->condition('value', _lock_id())
|
| 232 |
->execute();
|
| 233 |
}
|
| 234 |
|
| 235 |
/**
|
| 236 |
* Release all previously acquired locks.
|
| 237 |
*/
|
| 238 |
function lock_release_all($lock_id = NULL) {
|
| 239 |
global $locks;
|
| 240 |
|
| 241 |
$locks = array();
|
| 242 |
if (empty($lock_id)) {
|
| 243 |
$lock_id = _lock_id();
|
| 244 |
}
|
| 245 |
db_delete('semaphore')
|
| 246 |
->condition('value', $lock_id)
|
| 247 |
->execute();
|
| 248 |
}
|
| 249 |
|
| 250 |
/**
|
| 251 |
* @} End of "defgroup lock".
|
| 252 |
*/
|