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