/[drupal]/drupal/includes/lock.inc
ViewVC logotype

Contents of /drupal/includes/lock.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.1 - (show annotations) (download) (as text)
Mon Aug 17 20:32:29 2009 UTC (3 months, 1 week ago) by dries
Branch: MAIN
CVS Tags: DRUPAL-7-0-UNSTABLE-9, DRUPAL-7-0-UNSTABLE-10, HEAD
File MIME type: text/x-php
- Patch #251792 by pwolanin, Damien Tournoud, slantview, c960657: added a locking framework for long running operations.
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 */

  ViewVC Help
Powered by ViewVC 1.1.2