Index: branches/FileBackend/phase3/includes/filerepo/backend/LockManagerGroup.php |
— | — | @@ -1,68 +0,0 @@ |
2 | | -<?php |
3 | | -/** |
4 | | - * Class to handle file lock manager registration |
5 | | - * |
6 | | - * @ingroup FileBackend |
7 | | - */ |
8 | | -class LockManagerGroup { |
9 | | - protected static $instance = null; |
10 | | - |
11 | | - /** @var Array of (name => ('class' =>, 'config' =>, 'instance' =>)) */ |
12 | | - protected $managers = array(); |
13 | | - |
14 | | - protected function __construct() {} |
15 | | - protected function __clone() {} |
16 | | - |
17 | | - public static function singleton() { |
18 | | - if ( self::$instance == null ) { |
19 | | - self::$instance = new self(); |
20 | | - } |
21 | | - return self::$instance; |
22 | | - } |
23 | | - |
24 | | - /** |
25 | | - * Register an array of file lock manager configurations |
26 | | - * |
27 | | - * @param $configs Array |
28 | | - * @return void |
29 | | - * @throws MWException |
30 | | - */ |
31 | | - public function register( array $configs ) { |
32 | | - foreach ( $configs as $config ) { |
33 | | - if ( !isset( $config['name'] ) ) { |
34 | | - throw new MWException( "Cannot register a lock manager with no name." ); |
35 | | - } |
36 | | - $name = $config['name']; |
37 | | - if ( !isset( $config['class'] ) ) { |
38 | | - throw new MWException( "Cannot register lock manager `{$name}` with no class." ); |
39 | | - } |
40 | | - $class = $config['class']; |
41 | | - unset( $config['class'] ); // lock manager won't need this |
42 | | - $this->managers[$name] = array( |
43 | | - 'class' => $class, |
44 | | - 'config' => $config, |
45 | | - 'instance' => null |
46 | | - ); |
47 | | - } |
48 | | - } |
49 | | - |
50 | | - /** |
51 | | - * Get the lock manager object with a given name |
52 | | - * |
53 | | - * @param $name string |
54 | | - * @return LockManager |
55 | | - * @throws MWException |
56 | | - */ |
57 | | - public function get( $name ) { |
58 | | - if ( !isset( $this->managers[$name] ) ) { |
59 | | - throw new MWException( "No lock manager defined with the name `$name`." ); |
60 | | - } |
61 | | - // Lazy-load the actual backend instance |
62 | | - if ( !isset( $this->managers[$name]['instance'] ) ) { |
63 | | - $class = $this->managers[$name]['class']; |
64 | | - $config = $this->managers[$name]['config']; |
65 | | - $this->managers[$name]['instance'] = new $class( $config ); |
66 | | - } |
67 | | - return $this->managers[$name]['instance']; |
68 | | - } |
69 | | -} |
Index: branches/FileBackend/phase3/includes/filerepo/backend/LockManager.php |
— | — | @@ -1,810 +0,0 @@ |
2 | | -<?php |
3 | | -/** |
4 | | - * Class for handling resource locking. |
5 | | - * Locks on resource keys can either be shared or exclusive. |
6 | | - * |
7 | | - * Implementations must keep track of what is locked by this proccess |
8 | | - * in-memory and support nested locking calls (using reference counting). |
9 | | - * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op. |
10 | | - * Locks should either be non-blocking or have low wait timeouts. |
11 | | - * |
12 | | - * Subclasses should avoid throwing exceptions at all costs. |
13 | | - * |
14 | | - * @ingroup FileBackend |
15 | | - */ |
16 | | -abstract class LockManager { |
17 | | - /* Lock types; stronger locks have higher values */ |
18 | | - const LOCK_SH = 1; // shared lock (for reads) |
19 | | - const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) |
20 | | - const LOCK_EX = 3; // exclusive lock (for writes) |
21 | | - |
22 | | - /** @var Array Mapping of lock types to the type actually used */ |
23 | | - protected $lockTypeMap = array( |
24 | | - self::LOCK_SH => self::LOCK_SH, |
25 | | - self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH |
26 | | - self::LOCK_EX => self::LOCK_EX |
27 | | - ); |
28 | | - |
29 | | - /** |
30 | | - * Construct a new instance from configuration |
31 | | - * |
32 | | - * @param $config Array |
33 | | - */ |
34 | | - public function __construct( array $config ) {} |
35 | | - |
36 | | - /** |
37 | | - * Lock the resources at the given abstract paths |
38 | | - * |
39 | | - * @param $paths Array List of resource names |
40 | | - * @param $type integer LockManager::LOCK_* constant |
41 | | - * @return Status |
42 | | - */ |
43 | | - final public function lock( array $paths, $type = self::LOCK_EX ) { |
44 | | - $keys = array_unique( array_map( 'sha1', $paths ) ); |
45 | | - return $this->doLock( $keys, $this->lockTypeMap[$type] ); |
46 | | - } |
47 | | - |
48 | | - /** |
49 | | - * Unlock the resources at the given abstract paths |
50 | | - * |
51 | | - * @param $paths Array List of storage paths |
52 | | - * @param $type integer LockManager::LOCK_* constant |
53 | | - * @return Status |
54 | | - */ |
55 | | - final public function unlock( array $paths, $type = self::LOCK_EX ) { |
56 | | - $keys = array_unique( array_map( 'sha1', $paths ) ); |
57 | | - return $this->doUnlock( $keys, $this->lockTypeMap[$type] ); |
58 | | - } |
59 | | - |
60 | | - /** |
61 | | - * Lock resources with the given keys and lock type |
62 | | - * |
63 | | - * @param $key Array List of keys to lock (40 char hex hashes) |
64 | | - * @param $type integer LockManager::LOCK_* constant |
65 | | - * @return string |
66 | | - */ |
67 | | - abstract protected function doLock( array $keys, $type ); |
68 | | - |
69 | | - /** |
70 | | - * Unlock resources with the given keys and lock type |
71 | | - * |
72 | | - * @param $key Array List of keys to unlock (40 char hex hashes) |
73 | | - * @param $type integer LockManager::LOCK_* constant |
74 | | - * @return string |
75 | | - */ |
76 | | - abstract protected function doUnlock( array $keys, $type ); |
77 | | -} |
78 | | - |
79 | | -/** |
80 | | - * LockManager helper class to handle scoped locks, which |
81 | | - * release when an object is destroyed or goes out of scope. |
82 | | - */ |
83 | | -class ScopedLock { |
84 | | - /** @var LockManager */ |
85 | | - protected $manager; |
86 | | - /** @var Status */ |
87 | | - protected $status; |
88 | | - /** @var Array List of resource paths*/ |
89 | | - protected $paths; |
90 | | - |
91 | | - protected $type; // integer lock type |
92 | | - |
93 | | - /** |
94 | | - * @param $manager LockManager |
95 | | - * @param $paths Array List of storage paths |
96 | | - * @param $type integer LockManager::LOCK_* constant |
97 | | - * @param $status Status |
98 | | - */ |
99 | | - protected function __construct( |
100 | | - LockManager $manager, array $paths, $type, Status $status |
101 | | - ) { |
102 | | - $this->manager = $manager; |
103 | | - $this->paths = $paths; |
104 | | - $this->status = $status; |
105 | | - $this->type = $type; |
106 | | - } |
107 | | - |
108 | | - protected function __clone() {} |
109 | | - |
110 | | - /** |
111 | | - * Get a ScopedLock object representing a lock on resource paths. |
112 | | - * Any locks are released once this object goes out of scope. |
113 | | - * The status object is updated with any errors or warnings. |
114 | | - * |
115 | | - * @param $manager LockManager |
116 | | - * @param $paths Array List of storage paths |
117 | | - * @param $type integer LockManager::LOCK_* constant |
118 | | - * @param $status Status |
119 | | - * @return ScopedLock|null Returns null on failure |
120 | | - */ |
121 | | - public static function factory( |
122 | | - LockManager $manager, array $paths, $type, Status $status |
123 | | - ) { |
124 | | - $lockStatus = $manager->lock( $paths, $type ); |
125 | | - $status->merge( $lockStatus ); |
126 | | - if ( $lockStatus->isOK() ) { |
127 | | - return new self( $manager, $paths, $type, $status ); |
128 | | - } |
129 | | - return null; |
130 | | - } |
131 | | - |
132 | | - function __destruct() { |
133 | | - $wasOk = $this->status->isOK(); |
134 | | - $this->status->merge( $this->manager->unlock( $this->paths, $this->type ) ); |
135 | | - if ( $wasOk ) { |
136 | | - // Make sure status is OK, despite any unlockFiles() fatals |
137 | | - $this->status->setResult( true, $this->status->value ); |
138 | | - } |
139 | | - } |
140 | | -} |
141 | | - |
142 | | -/** |
143 | | - * Simple version of LockManager based on using FS lock files. |
144 | | - * All locks are non-blocking, which avoids deadlocks. |
145 | | - * |
146 | | - * This should work fine for small sites running off one server. |
147 | | - * Do not use this with 'lockDir' set to an NFS mount unless the |
148 | | - * NFS client is at least version 2.6.12. Otherwise, the BSD flock() |
149 | | - * locks will be ignored; see http://nfs.sourceforge.net/#section_d. |
150 | | - */ |
151 | | -class FSLockManager extends LockManager { |
152 | | - /** @var Array Mapping of lock types to the type actually used */ |
153 | | - protected $lockTypeMap = array( |
154 | | - self::LOCK_SH => self::LOCK_SH, |
155 | | - self::LOCK_UW => self::LOCK_SH, |
156 | | - self::LOCK_EX => self::LOCK_EX |
157 | | - ); |
158 | | - |
159 | | - protected $lockDir; // global dir for all servers |
160 | | - |
161 | | - /** @var Array Map of (locked key => lock type => count) */ |
162 | | - protected $locksHeld = array(); |
163 | | - /** @var Array Map of (locked key => lock type => lock file handle) */ |
164 | | - protected $handles = array(); |
165 | | - |
166 | | - function __construct( array $config ) { |
167 | | - $this->lockDir = $config['lockDirectory']; |
168 | | - } |
169 | | - |
170 | | - protected function doLock( array $keys, $type ) { |
171 | | - $status = Status::newGood(); |
172 | | - |
173 | | - $lockedKeys = array(); // files locked in this attempt |
174 | | - foreach ( $keys as $key ) { |
175 | | - $subStatus = $this->doSingleLock( $key, $type ); |
176 | | - $status->merge( $subStatus ); |
177 | | - if ( $status->isOK() ) { |
178 | | - // Don't append to $lockedKeys if $key is already locked. |
179 | | - // We do NOT want to unlock the key if we have to rollback. |
180 | | - if ( $subStatus->isGood() ) { // no warnings/fatals? |
181 | | - $lockedKeys[] = $key; |
182 | | - } |
183 | | - } else { |
184 | | - // Abort and unlock everything |
185 | | - $status->merge( $this->doUnlock( $lockedKeys, $type ) ); |
186 | | - return $status; |
187 | | - } |
188 | | - } |
189 | | - |
190 | | - return $status; |
191 | | - } |
192 | | - |
193 | | - protected function doUnlock( array $keys, $type ) { |
194 | | - $status = Status::newGood(); |
195 | | - |
196 | | - foreach ( $keys as $key ) { |
197 | | - $status->merge( $this->doSingleUnlock( $key, $type ) ); |
198 | | - } |
199 | | - |
200 | | - return $status; |
201 | | - } |
202 | | - |
203 | | - /** |
204 | | - * Lock a single resource key |
205 | | - * |
206 | | - * @param $key string |
207 | | - * @param $type integer |
208 | | - * @return Status |
209 | | - */ |
210 | | - protected function doSingleLock( $key, $type ) { |
211 | | - $status = Status::newGood(); |
212 | | - |
213 | | - if ( isset( $this->locksHeld[$key][$type] ) ) { |
214 | | - ++$this->locksHeld[$key][$type]; |
215 | | - } elseif ( isset( $this->locksHeld[$key][self::LOCK_EX] ) ) { |
216 | | - $this->locksHeld[$key][$type] = 1; |
217 | | - } else { |
218 | | - wfSuppressWarnings(); |
219 | | - $handle = fopen( $this->getLockPath( $key ), 'a+' ); |
220 | | - wfRestoreWarnings(); |
221 | | - if ( !$handle ) { // lock dir missing? |
222 | | - wfMkdirParents( $this->lockDir ); |
223 | | - wfSuppressWarnings(); |
224 | | - $handle = fopen( $this->getLockPath( $key ), 'a+' ); // try again |
225 | | - wfRestoreWarnings(); |
226 | | - } |
227 | | - if ( $handle ) { |
228 | | - // Either a shared or exclusive lock |
229 | | - $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX; |
230 | | - if ( flock( $handle, $lock | LOCK_NB ) ) { |
231 | | - // Record this lock as active |
232 | | - $this->locksHeld[$key][$type] = 1; |
233 | | - $this->handles[$key][$type] = $handle; |
234 | | - } else { |
235 | | - fclose( $handle ); |
236 | | - $status->fatal( 'lockmanager-fail-acquirelock', $key ); |
237 | | - } |
238 | | - } else { |
239 | | - $status->fatal( 'lockmanager-fail-openlock', $key ); |
240 | | - } |
241 | | - } |
242 | | - |
243 | | - return $status; |
244 | | - } |
245 | | - |
246 | | - /** |
247 | | - * Unlock a single resource key |
248 | | - * |
249 | | - * @param $key string |
250 | | - * @param $type integer |
251 | | - * @return Status |
252 | | - */ |
253 | | - protected function doSingleUnlock( $key, $type ) { |
254 | | - $status = Status::newGood(); |
255 | | - |
256 | | - if ( !isset( $this->locksHeld[$key] ) ) { |
257 | | - $status->warning( 'lockmanager-notlocked', $key ); |
258 | | - } elseif ( !isset( $this->locksHeld[$key][$type] ) ) { |
259 | | - $status->warning( 'lockmanager-notlocked', $key ); |
260 | | - } else { |
261 | | - $handlesToClose = array(); |
262 | | - --$this->locksHeld[$key][$type]; |
263 | | - if ( $this->locksHeld[$key][$type] <= 0 ) { |
264 | | - unset( $this->locksHeld[$key][$type] ); |
265 | | - // If a LOCK_SH comes in while we have a LOCK_EX, we don't |
266 | | - // actually add a handler, so check for handler existence. |
267 | | - if ( isset( $this->handles[$key][$type] ) ) { |
268 | | - // Mark this handle to be unlocked and closed |
269 | | - $handlesToClose[] = $this->handles[$key][$type]; |
270 | | - unset( $this->handles[$key][$type] ); |
271 | | - } |
272 | | - } |
273 | | - // Unlock handles to release locks and delete |
274 | | - // any lock files that end up with no locks on them... |
275 | | - if ( wfIsWindows() ) { |
276 | | - // Windows: for any process, including this one, |
277 | | - // calling unlink() on a locked file will fail |
278 | | - $status->merge( $this->closeLockHandles( $key, $handlesToClose ) ); |
279 | | - $status->merge( $this->pruneKeyLockFiles( $key ) ); |
280 | | - } else { |
281 | | - // Unix: unlink() can be used on files currently open by this |
282 | | - // process and we must do so in order to avoid race conditions |
283 | | - $status->merge( $this->pruneKeyLockFiles( $key ) ); |
284 | | - $status->merge( $this->closeLockHandles( $key, $handlesToClose ) ); |
285 | | - } |
286 | | - } |
287 | | - |
288 | | - return $status; |
289 | | - } |
290 | | - |
291 | | - private function closeLockHandles( $key, array $handlesToClose ) { |
292 | | - $status = Status::newGood(); |
293 | | - foreach ( $handlesToClose as $handle ) { |
294 | | - wfSuppressWarnings(); |
295 | | - if ( !flock( $handle, LOCK_UN ) ) { |
296 | | - $status->fatal( 'lockmanager-fail-releaselock', $key ); |
297 | | - } |
298 | | - if ( !fclose( $handle ) ) { |
299 | | - $status->warning( 'lockmanager-fail-closelock', $key ); |
300 | | - } |
301 | | - wfRestoreWarnings(); |
302 | | - } |
303 | | - return $status; |
304 | | - } |
305 | | - |
306 | | - private function pruneKeyLockFiles( $key ) { |
307 | | - $status = Status::newGood(); |
308 | | - if ( !count( $this->locksHeld[$key] ) ) { |
309 | | - wfSuppressWarnings(); |
310 | | - # No locks are held for the lock file anymore |
311 | | - if ( !unlink( $this->getLockPath( $key ) ) ) { |
312 | | - $status->warning( 'lockmanager-fail-deletelock', $key ); |
313 | | - } |
314 | | - wfRestoreWarnings(); |
315 | | - unset( $this->locksHeld[$key] ); |
316 | | - unset( $this->handles[$key] ); |
317 | | - } |
318 | | - return $status; |
319 | | - } |
320 | | - |
321 | | - /** |
322 | | - * Get the path to the lock file for a key |
323 | | - * @param $key string |
324 | | - * @return string |
325 | | - */ |
326 | | - protected function getLockPath( $key ) { |
327 | | - return "{$this->lockDir}/{$key}.lock"; |
328 | | - } |
329 | | - |
330 | | - function __destruct() { |
331 | | - // Make sure remaining locks get cleared for sanity |
332 | | - foreach ( $this->locksHeld as $key => $locks ) { |
333 | | - $this->doSingleUnlock( $key, 0 ); |
334 | | - } |
335 | | - } |
336 | | -} |
337 | | - |
338 | | -/** |
339 | | - * Version of LockManager based on using DB table locks. |
340 | | - * This is meant for multi-wiki systems that may share files. |
341 | | - * All locks are blocking, so it might be useful to set a small |
342 | | - * lock-wait timeout via server config to curtail deadlocks. |
343 | | - * |
344 | | - * All lock requests for a resource, identified by a hash string, will map |
345 | | - * to one bucket. Each bucket maps to one or several peer DBs, each on their |
346 | | - * own server, all having the filelocks.sql tables (with row-level locking). |
347 | | - * A majority of peer DBs must agree for a lock to be acquired. |
348 | | - * |
349 | | - * Caching is used to avoid hitting servers that are down. |
350 | | - */ |
351 | | -class DBLockManager extends LockManager { |
352 | | - /** @var Array Map of DB names to server config */ |
353 | | - protected $dbServers; // (DB name => server config array) |
354 | | - /** @var Array Map of bucket indexes to peer DB lists */ |
355 | | - protected $dbsByBucket; // (bucket index => (ldb1, ldb2, ...)) |
356 | | - /** @var BagOStuff */ |
357 | | - protected $statusCache; |
358 | | - |
359 | | - protected $lockExpiry; // integer number of seconds |
360 | | - protected $safeDelay; // integer number of seconds |
361 | | - |
362 | | - protected $session = 0; // random integer |
363 | | - /** @var Array Map of (locked key => lock type => count) */ |
364 | | - protected $locksHeld = array(); |
365 | | - /** @var Array Map Database connections (DB name => Database) */ |
366 | | - protected $conns = array(); |
367 | | - |
368 | | - /** |
369 | | - * Construct a new instance from configuration. |
370 | | - * $config paramaters include: |
371 | | - * 'dbServers' : Associative array of DB names to server configuration. |
372 | | - * Configuration is an associative array that includes: |
373 | | - * 'host' - DB server name |
374 | | - * 'dbname' - DB name |
375 | | - * 'type' - DB type (mysql,postgres,...) |
376 | | - * 'user' - DB user |
377 | | - * 'password' - DB user password |
378 | | - * 'tablePrefix' - DB table prefix |
379 | | - * 'flags' - DB flags (see DatabaseBase) |
380 | | - * 'dbsByBucket' : Array of 1-16 consecutive integer keys, starting from 0, |
381 | | - * each having an odd-numbered list of DB names (peers) as values. |
382 | | - * 'lockExpiry' : Lock timeout (seconds) for dropped connections. [optional] |
383 | | - * This tells the DB server how long to wait before assuming |
384 | | - * connection failure and releasing all the locks for a session. |
385 | | - * |
386 | | - * @param Array $config |
387 | | - */ |
388 | | - public function __construct( array $config ) { |
389 | | - $this->dbServers = $config['dbServers']; |
390 | | - // Sanitize dbsByBucket config to prevent PHP errors |
391 | | - $this->dbsByBucket = array_filter( $config['dbsByBucket'], 'is_array' ); |
392 | | - $this->dbsByBucket = array_values( $this->dbsByBucket ); // consecutive |
393 | | - |
394 | | - if ( isset( $config['lockExpiry'] ) ) { |
395 | | - $this->lockExpiry = $config['lockExpiry']; |
396 | | - } else { |
397 | | - $met = ini_get( 'max_execution_time' ); |
398 | | - $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0 |
399 | | - } |
400 | | - $this->safeDelay = ( $this->lockExpiry <= 0 ) |
401 | | - ? 60 // pick a safe-ish number to match DB timeout default |
402 | | - : $this->lockExpiry; // cover worst case |
403 | | - |
404 | | - foreach ( $this->dbsByBucket as $bucket ) { |
405 | | - if ( count( $bucket ) > 1 ) { |
406 | | - // Tracks peers that couldn't be queried recently to avoid lengthy |
407 | | - // connection timeouts. This is useless if each bucket has one peer. |
408 | | - $this->statusCache = wfGetMainCache(); |
409 | | - break; |
410 | | - } |
411 | | - } |
412 | | - |
413 | | - $this->session = mt_rand( 0, 2147483647 ); |
414 | | - } |
415 | | - |
416 | | - protected function doLock( array $keys, $type ) { |
417 | | - $status = Status::newGood(); |
418 | | - |
419 | | - $keysToLock = array(); |
420 | | - // Get locks that need to be acquired (buckets => locks)... |
421 | | - foreach ( $keys as $key ) { |
422 | | - if ( isset( $this->locksHeld[$key][$type] ) ) { |
423 | | - ++$this->locksHeld[$key][$type]; |
424 | | - } elseif ( isset( $this->locksHeld[$key][self::LOCK_EX] ) ) { |
425 | | - $this->locksHeld[$key][$type] = 1; |
426 | | - } else { |
427 | | - $bucket = $this->getBucketFromKey( $key ); |
428 | | - $keysToLock[$bucket][] = $key; |
429 | | - } |
430 | | - } |
431 | | - |
432 | | - $lockedKeys = array(); // files locked in this attempt |
433 | | - // Attempt to acquire these locks... |
434 | | - foreach ( $keysToLock as $bucket => $keys ) { |
435 | | - // Try to acquire the locks for this bucket |
436 | | - $res = $this->doLockingQueryAll( $bucket, $keys, $type ); |
437 | | - if ( $res === 'cantacquire' ) { |
438 | | - // Resources already locked by another process. |
439 | | - // Abort and unlock everything we just locked. |
440 | | - $status->fatal( 'lockmanager-fail-acquirelocks', implode( ', ', $keys ) ); |
441 | | - $status->merge( $this->doUnlock( $lockedKeys, $type ) ); |
442 | | - return $status; |
443 | | - } elseif ( $res !== true ) { |
444 | | - // Couldn't contact any DBs for this bucket. |
445 | | - // Abort and unlock everything we just locked. |
446 | | - $status->fatal( 'lockmanager-fail-db-bucket', $bucket ); |
447 | | - $status->merge( $this->doUnlock( $lockedKeys, $type ) ); |
448 | | - return $status; |
449 | | - } |
450 | | - // Record these locks as active |
451 | | - foreach ( $keys as $key ) { |
452 | | - $this->locksHeld[$key][$type] = 1; // locked |
453 | | - } |
454 | | - // Keep track of what locks were made in this attempt |
455 | | - $lockedKeys = array_merge( $lockedKeys, $keys ); |
456 | | - } |
457 | | - |
458 | | - return $status; |
459 | | - } |
460 | | - |
461 | | - protected function doUnlock( array $keys, $type ) { |
462 | | - $status = Status::newGood(); |
463 | | - |
464 | | - foreach ( $keys as $key ) { |
465 | | - if ( !isset( $this->locksHeld[$key] ) ) { |
466 | | - $status->warning( 'lockmanager-notlocked', $key ); |
467 | | - } elseif ( !isset( $this->locksHeld[$key][$type] ) ) { |
468 | | - $status->warning( 'lockmanager-notlocked', $key ); |
469 | | - } else { |
470 | | - --$this->locksHeld[$key][$type]; |
471 | | - if ( $this->locksHeld[$key][$type] <= 0 ) { |
472 | | - unset( $this->locksHeld[$key][$type] ); |
473 | | - } |
474 | | - if ( !count( $this->locksHeld[$key] ) ) { |
475 | | - unset( $this->locksHeld[$key] ); // no SH or EX locks left for key |
476 | | - } |
477 | | - } |
478 | | - } |
479 | | - |
480 | | - // Reference count the locks held and COMMIT when zero |
481 | | - if ( !count( $this->locksHeld ) ) { |
482 | | - $status->merge( $this->finishLockTransactions() ); |
483 | | - } |
484 | | - |
485 | | - return $status; |
486 | | - } |
487 | | - |
488 | | - /** |
489 | | - * Get a connection to a lock DB and acquire locks on $keys. |
490 | | - * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118. |
491 | | - * |
492 | | - * @param $lockDb string |
493 | | - * @param $keys Array |
494 | | - * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH |
495 | | - * @return bool Resources able to be locked |
496 | | - * @throws DBError |
497 | | - */ |
498 | | - protected function doLockingQuery( $lockDb, array $keys, $type ) { |
499 | | - if ( $type == self::LOCK_EX ) { // writer locks |
500 | | - $db = $this->getConnection( $lockDb ); |
501 | | - if ( !$db ) { |
502 | | - return false; // bad config |
503 | | - } |
504 | | - $data = array(); |
505 | | - foreach ( $keys as $key ) { |
506 | | - $data[] = array( 'fle_key' => $key ); |
507 | | - } |
508 | | - # Wait on any existing writers and block new ones if we get in |
509 | | - $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); |
510 | | - } |
511 | | - return true; |
512 | | - } |
513 | | - |
514 | | - /** |
515 | | - * Attempt to acquire locks with the peers for a bucket. |
516 | | - * This should avoid throwing any exceptions. |
517 | | - * |
518 | | - * @param $bucket integer |
519 | | - * @param $keys Array List of resource keys to lock |
520 | | - * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH |
521 | | - * @return bool|string One of (true, 'cantacquire', 'dberrors') |
522 | | - */ |
523 | | - protected function doLockingQueryAll( $bucket, array $keys, $type ) { |
524 | | - $yesVotes = 0; // locks made on trustable DBs |
525 | | - $votesLeft = count( $this->dbsByBucket[$bucket] ); // remaining DBs |
526 | | - $quorum = floor( $votesLeft/2 + 1 ); // simple majority |
527 | | - // Get votes for each DB, in order, until we have enough... |
528 | | - foreach ( $this->dbsByBucket[$bucket] as $index => $lockDb ) { |
529 | | - // Check that DB is not *known* to be down |
530 | | - if ( $this->cacheCheckFailures( $lockDb ) ) { |
531 | | - try { |
532 | | - // Attempt to acquire the lock on this DB |
533 | | - if ( !$this->doLockingQuery( $lockDb, $keys, $type ) ) { |
534 | | - return 'cantacquire'; // vetoed; resource locked |
535 | | - } |
536 | | - // Check that DB has no signs of lock loss |
537 | | - if ( $this->checkUptime( $lockDb ) ) { |
538 | | - ++$yesVotes; // success for this peer |
539 | | - if ( $yesVotes >= $quorum ) { |
540 | | - return true; // lock obtained |
541 | | - } |
542 | | - } |
543 | | - } catch ( DBConnectionError $e ) { |
544 | | - $this->cacheRecordFailure( $lockDb ); |
545 | | - } catch ( DBError $e ) { |
546 | | - if ( $this->lastErrorIndicatesLocked( $lockDb ) ) { |
547 | | - return 'cantacquire'; // vetoed; resource locked |
548 | | - } |
549 | | - } |
550 | | - } |
551 | | - $votesLeft--; |
552 | | - $votesNeeded = $quorum - $yesVotes; |
553 | | - if ( $votesNeeded > $votesLeft ) { |
554 | | - // In "trust cache" mode we don't have to meet the quorum |
555 | | - break; // short-circuit |
556 | | - } |
557 | | - } |
558 | | - // At this point, we must not have meet the quorum |
559 | | - return 'dberrors'; // not enough votes to ensure correctness |
560 | | - } |
561 | | - |
562 | | - /** |
563 | | - * Get (or reuse) a connection to a lock DB |
564 | | - * |
565 | | - * @param $lockDb string |
566 | | - * @return Database |
567 | | - * @throws DBError |
568 | | - */ |
569 | | - protected function getConnection( $lockDb ) { |
570 | | - if ( !isset( $this->conns[$lockDb] ) ) { |
571 | | - $config = $this->dbServers[$lockDb]; |
572 | | - $this->conns[$lockDb] = DatabaseBase::factory( $config['type'], $config ); |
573 | | - if ( !$this->conns[$lockDb] ) { |
574 | | - return null; // config error? |
575 | | - } |
576 | | - # If the connection drops, try to avoid letting the DB rollback |
577 | | - # and release the locks before the file operations are finished. |
578 | | - # This won't handle the case of DB server restarts however. |
579 | | - $options = array(); |
580 | | - if ( $this->lockExpiry > 0 ) { |
581 | | - $options['connTimeout'] = $this->lockExpiry; |
582 | | - } |
583 | | - $this->conns[$lockDb]->setSessionOptions( $options ); |
584 | | - $this->initConnection( $lockDb, $this->conns[$lockDb] ); |
585 | | - } |
586 | | - if ( !$this->conns[$lockDb]->trxLevel() ) { |
587 | | - $this->conns[$lockDb]->begin(); // start transaction |
588 | | - } |
589 | | - return $this->conns[$lockDb]; |
590 | | - } |
591 | | - |
592 | | - /** |
593 | | - * Do additional initialization for new lock DB connection |
594 | | - * |
595 | | - * @param $lockDb string |
596 | | - * @param $db DatabaseBase |
597 | | - * @return void |
598 | | - * @throws DBError |
599 | | - */ |
600 | | - protected function initConnection( $lockDb, DatabaseBase $db ) {} |
601 | | - |
602 | | - /** |
603 | | - * Commit all changes to lock-active databases. |
604 | | - * This should avoid throwing any exceptions. |
605 | | - * |
606 | | - * @return Status |
607 | | - */ |
608 | | - protected function finishLockTransactions() { |
609 | | - $status = Status::newGood(); |
610 | | - foreach ( $this->conns as $lockDb => $db ) { |
611 | | - if ( $db->trxLevel() ) { // in transaction |
612 | | - try { |
613 | | - $db->rollback(); // finish transaction and kill any rows |
614 | | - } catch ( DBError $e ) { |
615 | | - $status->fatal( 'lockmanager-fail-db-release', $lockDb ); |
616 | | - } |
617 | | - } |
618 | | - } |
619 | | - return $status; |
620 | | - } |
621 | | - |
622 | | - /** |
623 | | - * Check if the last DB error for $lockDb indicates |
624 | | - * that a requested resource was locked by another process. |
625 | | - * This should avoid throwing any exceptions. |
626 | | - * |
627 | | - * @param $lockDb string |
628 | | - * @return bool |
629 | | - */ |
630 | | - protected function lastErrorIndicatesLocked( $lockDb ) { |
631 | | - if ( isset( $this->conns[$lockDb] ) ) { // sanity |
632 | | - $db = $this->conns[$lockDb]; |
633 | | - return ( $db->wasDeadlock() || $db->wasLockTimeout() ); |
634 | | - } |
635 | | - return false; |
636 | | - } |
637 | | - |
638 | | - /** |
639 | | - * Checks if the DB server did not recently restart. |
640 | | - * This curtails the problem of locks falling off when DB servers restart. |
641 | | - * |
642 | | - * @param $lockDb string |
643 | | - * @return bool |
644 | | - * @throws DBError |
645 | | - */ |
646 | | - protected function checkUptime( $lockDb ) { |
647 | | - if ( isset( $this->conns[$lockDb] ) ) { // sanity |
648 | | - if ( $this->safeDelay > 0 ) { |
649 | | - $db = $this->conns[$lockDb]; |
650 | | - return ( $db->getServerUptime() > $this->safeDelay ); |
651 | | - } |
652 | | - return true; |
653 | | - } |
654 | | - return false; |
655 | | - } |
656 | | - |
657 | | - /** |
658 | | - * Checks if the DB has not recently had connection/query errors. |
659 | | - * When in "trust cache" mode, this curtails the problem of peers occasionally |
660 | | - * missing locks. Otherwise, it just avoids wasting time on connection attempts. |
661 | | - * |
662 | | - * @param $lockDb string |
663 | | - * @return bool |
664 | | - */ |
665 | | - protected function cacheCheckFailures( $lockDb ) { |
666 | | - if ( $this->statusCache && $this->safeDelay > 0 ) { |
667 | | - $key = $this->getMissKey( $lockDb ); |
668 | | - $misses = $this->statusCache->get( $key ); |
669 | | - return !$misses; |
670 | | - } |
671 | | - return true; |
672 | | - } |
673 | | - |
674 | | - /** |
675 | | - * Log a lock request failure to the cache. |
676 | | - * |
677 | | - * Worst case scenario is that a resource lock was only |
678 | | - * on one peer and then that peer is restarted or goes down. |
679 | | - * Clients trying to get locks need to know if a DB server is down. |
680 | | - * |
681 | | - * @param $lockDb string |
682 | | - * @return bool Success |
683 | | - */ |
684 | | - protected function cacheRecordFailure( $lockDb ) { |
685 | | - if ( $this->statusCache && $this->safeDelay > 0 ) { |
686 | | - $key = $this->getMissKey( $lockDb ); |
687 | | - $misses = $this->statusCache->get( $key ); |
688 | | - if ( $misses ) { |
689 | | - return $this->statusCache->incr( $key ); |
690 | | - } else { |
691 | | - return $this->statusCache->add( $key, 1, $this->safeDelay ); |
692 | | - } |
693 | | - } |
694 | | - return true; |
695 | | - } |
696 | | - |
697 | | - /** |
698 | | - * Get a cache key for recent query misses for a DB |
699 | | - * |
700 | | - * @param $lockDb string |
701 | | - * @return string |
702 | | - */ |
703 | | - protected function getMissKey( $lockDb ) { |
704 | | - return 'lockmanager:querymisses:' . str_replace( ' ', '_', $lockDb ); |
705 | | - } |
706 | | - |
707 | | - /** |
708 | | - * Get the bucket for lock key. |
709 | | - * This should avoid throwing any exceptions. |
710 | | - * |
711 | | - * @param $key string (40 char hex key) |
712 | | - * @return integer |
713 | | - */ |
714 | | - protected function getBucketFromKey( $key ) { |
715 | | - $prefix = substr( $key, 0, 2 ); // first 2 hex chars (8 bits) |
716 | | - return intval( base_convert( $prefix, 16, 10 ) ) % count( $this->dbsByBucket ); |
717 | | - } |
718 | | - |
719 | | - /** |
720 | | - * Make sure remaining locks get cleared for sanity |
721 | | - */ |
722 | | - function __destruct() { |
723 | | - foreach ( $this->conns as $lockDb => $db ) { |
724 | | - if ( $db->trxLevel() ) { // in transaction |
725 | | - try { |
726 | | - $db->rollback(); // finish transaction and kill any rows |
727 | | - } catch ( DBError $e ) { |
728 | | - // oh well |
729 | | - } |
730 | | - } |
731 | | - $db->close(); |
732 | | - } |
733 | | - } |
734 | | -} |
735 | | - |
736 | | -/** |
737 | | - * MySQL version of DBLockManager that supports shared locks. |
738 | | - * All locks are non-blocking, which avoids deadlocks. |
739 | | - */ |
740 | | -class MySqlLockManager extends DBLockManager { |
741 | | - /** @var Array Mapping of lock types to the type actually used */ |
742 | | - protected $lockTypeMap = array( |
743 | | - self::LOCK_SH => self::LOCK_SH, |
744 | | - self::LOCK_UW => self::LOCK_SH, |
745 | | - self::LOCK_EX => self::LOCK_EX |
746 | | - ); |
747 | | - |
748 | | - protected function initConnection( $lockDb, DatabaseBase $db ) { |
749 | | - # Let this transaction see lock rows from other transactions |
750 | | - $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" ); |
751 | | - } |
752 | | - |
753 | | - protected function doLockingQuery( $lockDb, array $keys, $type ) { |
754 | | - $db = $this->getConnection( $lockDb ); |
755 | | - if ( !$db ) { |
756 | | - return false; |
757 | | - } |
758 | | - $data = array(); |
759 | | - foreach ( $keys as $key ) { |
760 | | - $data[] = array( 'fls_key' => $key, 'fls_session' => $this->session ); |
761 | | - } |
762 | | - # Block new writers... |
763 | | - $db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) ); |
764 | | - # Actually do the locking queries... |
765 | | - if ( $type == self::LOCK_SH ) { // reader locks |
766 | | - # Bail if there are any existing writers... |
767 | | - $blocked = $db->selectField( 'filelocks_exclusive', '1', |
768 | | - array( 'fle_key' => $keys ), |
769 | | - __METHOD__ |
770 | | - ); |
771 | | - # Prospective writers that haven't yet updated filelocks_exclusive |
772 | | - # will recheck filelocks_shared after doing so and bail due to our entry. |
773 | | - } else { // writer locks |
774 | | - $encSession = $db->addQuotes( $this->session ); |
775 | | - # Bail if there are any existing writers... |
776 | | - # The may detect readers, but the safe check for them is below. |
777 | | - # Note: if two writers come at the same time, both bail :) |
778 | | - $blocked = $db->selectField( 'filelocks_shared', '1', |
779 | | - array( 'fls_key' => $keys, "fls_session != $encSession" ), |
780 | | - __METHOD__ |
781 | | - ); |
782 | | - if ( !$blocked ) { |
783 | | - $data = array(); |
784 | | - foreach ( $keys as $key ) { |
785 | | - $data[] = array( 'fle_key' => $key ); |
786 | | - } |
787 | | - # Block new readers/writers... |
788 | | - $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); |
789 | | - # Bail if there are any existing readers... |
790 | | - $blocked = $db->selectField( 'filelocks_shared', '1', |
791 | | - array( 'fls_key' => $keys, "fls_session != $encSession" ), |
792 | | - __METHOD__ |
793 | | - ); |
794 | | - } |
795 | | - } |
796 | | - return !$blocked; |
797 | | - } |
798 | | -} |
799 | | - |
800 | | -/** |
801 | | - * Simple version of LockManager that does nothing |
802 | | - */ |
803 | | -class NullLockManager extends LockManager { |
804 | | - protected function doLock( array $keys, $type ) { |
805 | | - return Status::newGood(); |
806 | | - } |
807 | | - |
808 | | - protected function doUnlock( array $keys, $type ) { |
809 | | - return Status::newGood(); |
810 | | - } |
811 | | -} |
Index: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/DBLockManager.php |
— | — | @@ -0,0 +1,463 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * Version of LockManager based on using DB table locks. |
| 6 | + * This is meant for multi-wiki systems that may share files. |
| 7 | + * All locks are blocking, so it might be useful to set a small |
| 8 | + * lock-wait timeout via server config to curtail deadlocks. |
| 9 | + * |
| 10 | + * All lock requests for a resource, identified by a hash string, will map |
| 11 | + * to one bucket. Each bucket maps to one or several peer DBs, each on their |
| 12 | + * own server, all having the filelocks.sql tables (with row-level locking). |
| 13 | + * A majority of peer DBs must agree for a lock to be acquired. |
| 14 | + * |
| 15 | + * Caching is used to avoid hitting servers that are down. |
| 16 | + */ |
| 17 | +class DBLockManager extends LockManager { |
| 18 | + /** @var Array Map of DB names to server config */ |
| 19 | + protected $dbServers; // (DB name => server config array) |
| 20 | + /** @var Array Map of bucket indexes to peer DB lists */ |
| 21 | + protected $dbsByBucket; // (bucket index => (ldb1, ldb2, ...)) |
| 22 | + /** @var BagOStuff */ |
| 23 | + protected $statusCache; |
| 24 | + |
| 25 | + protected $lockExpiry; // integer number of seconds |
| 26 | + protected $safeDelay; // integer number of seconds |
| 27 | + |
| 28 | + protected $session = 0; // random integer |
| 29 | + /** @var Array Map of (locked key => lock type => count) */ |
| 30 | + protected $locksHeld = array(); |
| 31 | + /** @var Array Map Database connections (DB name => Database) */ |
| 32 | + protected $conns = array(); |
| 33 | + |
| 34 | + /** |
| 35 | + * Construct a new instance from configuration. |
| 36 | + * $config paramaters include: |
| 37 | + * 'dbServers' : Associative array of DB names to server configuration. |
| 38 | + * Configuration is an associative array that includes: |
| 39 | + * 'host' - DB server name |
| 40 | + * 'dbname' - DB name |
| 41 | + * 'type' - DB type (mysql,postgres,...) |
| 42 | + * 'user' - DB user |
| 43 | + * 'password' - DB user password |
| 44 | + * 'tablePrefix' - DB table prefix |
| 45 | + * 'flags' - DB flags (see DatabaseBase) |
| 46 | + * 'dbsByBucket' : Array of 1-16 consecutive integer keys, starting from 0, |
| 47 | + * each having an odd-numbered list of DB names (peers) as values. |
| 48 | + * 'lockExpiry' : Lock timeout (seconds) for dropped connections. [optional] |
| 49 | + * This tells the DB server how long to wait before assuming |
| 50 | + * connection failure and releasing all the locks for a session. |
| 51 | + * |
| 52 | + * @param Array $config |
| 53 | + */ |
| 54 | + public function __construct( array $config ) { |
| 55 | + $this->dbServers = $config['dbServers']; |
| 56 | + // Sanitize dbsByBucket config to prevent PHP errors |
| 57 | + $this->dbsByBucket = array_filter( $config['dbsByBucket'], 'is_array' ); |
| 58 | + $this->dbsByBucket = array_values( $this->dbsByBucket ); // consecutive |
| 59 | + |
| 60 | + if ( isset( $config['lockExpiry'] ) ) { |
| 61 | + $this->lockExpiry = $config['lockExpiry']; |
| 62 | + } else { |
| 63 | + $met = ini_get( 'max_execution_time' ); |
| 64 | + $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0 |
| 65 | + } |
| 66 | + $this->safeDelay = ( $this->lockExpiry <= 0 ) |
| 67 | + ? 60 // pick a safe-ish number to match DB timeout default |
| 68 | + : $this->lockExpiry; // cover worst case |
| 69 | + |
| 70 | + foreach ( $this->dbsByBucket as $bucket ) { |
| 71 | + if ( count( $bucket ) > 1 ) { |
| 72 | + // Tracks peers that couldn't be queried recently to avoid lengthy |
| 73 | + // connection timeouts. This is useless if each bucket has one peer. |
| 74 | + $this->statusCache = wfGetMainCache(); |
| 75 | + break; |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + $this->session = mt_rand( 0, 2147483647 ); |
| 80 | + } |
| 81 | + |
| 82 | + protected function doLock( array $keys, $type ) { |
| 83 | + $status = Status::newGood(); |
| 84 | + |
| 85 | + $keysToLock = array(); |
| 86 | + // Get locks that need to be acquired (buckets => locks)... |
| 87 | + foreach ( $keys as $key ) { |
| 88 | + if ( isset( $this->locksHeld[$key][$type] ) ) { |
| 89 | + ++$this->locksHeld[$key][$type]; |
| 90 | + } elseif ( isset( $this->locksHeld[$key][self::LOCK_EX] ) ) { |
| 91 | + $this->locksHeld[$key][$type] = 1; |
| 92 | + } else { |
| 93 | + $bucket = $this->getBucketFromKey( $key ); |
| 94 | + $keysToLock[$bucket][] = $key; |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + $lockedKeys = array(); // files locked in this attempt |
| 99 | + // Attempt to acquire these locks... |
| 100 | + foreach ( $keysToLock as $bucket => $keys ) { |
| 101 | + // Try to acquire the locks for this bucket |
| 102 | + $res = $this->doLockingQueryAll( $bucket, $keys, $type ); |
| 103 | + if ( $res === 'cantacquire' ) { |
| 104 | + // Resources already locked by another process. |
| 105 | + // Abort and unlock everything we just locked. |
| 106 | + $status->fatal( 'lockmanager-fail-acquirelocks', implode( ', ', $keys ) ); |
| 107 | + $status->merge( $this->doUnlock( $lockedKeys, $type ) ); |
| 108 | + return $status; |
| 109 | + } elseif ( $res !== true ) { |
| 110 | + // Couldn't contact any DBs for this bucket. |
| 111 | + // Abort and unlock everything we just locked. |
| 112 | + $status->fatal( 'lockmanager-fail-db-bucket', $bucket ); |
| 113 | + $status->merge( $this->doUnlock( $lockedKeys, $type ) ); |
| 114 | + return $status; |
| 115 | + } |
| 116 | + // Record these locks as active |
| 117 | + foreach ( $keys as $key ) { |
| 118 | + $this->locksHeld[$key][$type] = 1; // locked |
| 119 | + } |
| 120 | + // Keep track of what locks were made in this attempt |
| 121 | + $lockedKeys = array_merge( $lockedKeys, $keys ); |
| 122 | + } |
| 123 | + |
| 124 | + return $status; |
| 125 | + } |
| 126 | + |
| 127 | + protected function doUnlock( array $keys, $type ) { |
| 128 | + $status = Status::newGood(); |
| 129 | + |
| 130 | + foreach ( $keys as $key ) { |
| 131 | + if ( !isset( $this->locksHeld[$key] ) ) { |
| 132 | + $status->warning( 'lockmanager-notlocked', $key ); |
| 133 | + } elseif ( !isset( $this->locksHeld[$key][$type] ) ) { |
| 134 | + $status->warning( 'lockmanager-notlocked', $key ); |
| 135 | + } else { |
| 136 | + --$this->locksHeld[$key][$type]; |
| 137 | + if ( $this->locksHeld[$key][$type] <= 0 ) { |
| 138 | + unset( $this->locksHeld[$key][$type] ); |
| 139 | + } |
| 140 | + if ( !count( $this->locksHeld[$key] ) ) { |
| 141 | + unset( $this->locksHeld[$key] ); // no SH or EX locks left for key |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + // Reference count the locks held and COMMIT when zero |
| 147 | + if ( !count( $this->locksHeld ) ) { |
| 148 | + $status->merge( $this->finishLockTransactions() ); |
| 149 | + } |
| 150 | + |
| 151 | + return $status; |
| 152 | + } |
| 153 | + |
| 154 | + /** |
| 155 | + * Get a connection to a lock DB and acquire locks on $keys. |
| 156 | + * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118. |
| 157 | + * |
| 158 | + * @param $lockDb string |
| 159 | + * @param $keys Array |
| 160 | + * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH |
| 161 | + * @return bool Resources able to be locked |
| 162 | + * @throws DBError |
| 163 | + */ |
| 164 | + protected function doLockingQuery( $lockDb, array $keys, $type ) { |
| 165 | + if ( $type == self::LOCK_EX ) { // writer locks |
| 166 | + $db = $this->getConnection( $lockDb ); |
| 167 | + if ( !$db ) { |
| 168 | + return false; // bad config |
| 169 | + } |
| 170 | + $data = array(); |
| 171 | + foreach ( $keys as $key ) { |
| 172 | + $data[] = array( 'fle_key' => $key ); |
| 173 | + } |
| 174 | + # Wait on any existing writers and block new ones if we get in |
| 175 | + $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); |
| 176 | + } |
| 177 | + return true; |
| 178 | + } |
| 179 | + |
| 180 | + /** |
| 181 | + * Attempt to acquire locks with the peers for a bucket. |
| 182 | + * This should avoid throwing any exceptions. |
| 183 | + * |
| 184 | + * @param $bucket integer |
| 185 | + * @param $keys Array List of resource keys to lock |
| 186 | + * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH |
| 187 | + * @return bool|string One of (true, 'cantacquire', 'dberrors') |
| 188 | + */ |
| 189 | + protected function doLockingQueryAll( $bucket, array $keys, $type ) { |
| 190 | + $yesVotes = 0; // locks made on trustable DBs |
| 191 | + $votesLeft = count( $this->dbsByBucket[$bucket] ); // remaining DBs |
| 192 | + $quorum = floor( $votesLeft/2 + 1 ); // simple majority |
| 193 | + // Get votes for each DB, in order, until we have enough... |
| 194 | + foreach ( $this->dbsByBucket[$bucket] as $index => $lockDb ) { |
| 195 | + // Check that DB is not *known* to be down |
| 196 | + if ( $this->cacheCheckFailures( $lockDb ) ) { |
| 197 | + try { |
| 198 | + // Attempt to acquire the lock on this DB |
| 199 | + if ( !$this->doLockingQuery( $lockDb, $keys, $type ) ) { |
| 200 | + return 'cantacquire'; // vetoed; resource locked |
| 201 | + } |
| 202 | + // Check that DB has no signs of lock loss |
| 203 | + if ( $this->checkUptime( $lockDb ) ) { |
| 204 | + ++$yesVotes; // success for this peer |
| 205 | + if ( $yesVotes >= $quorum ) { |
| 206 | + return true; // lock obtained |
| 207 | + } |
| 208 | + } |
| 209 | + } catch ( DBConnectionError $e ) { |
| 210 | + $this->cacheRecordFailure( $lockDb ); |
| 211 | + } catch ( DBError $e ) { |
| 212 | + if ( $this->lastErrorIndicatesLocked( $lockDb ) ) { |
| 213 | + return 'cantacquire'; // vetoed; resource locked |
| 214 | + } |
| 215 | + } |
| 216 | + } |
| 217 | + $votesLeft--; |
| 218 | + $votesNeeded = $quorum - $yesVotes; |
| 219 | + if ( $votesNeeded > $votesLeft ) { |
| 220 | + // In "trust cache" mode we don't have to meet the quorum |
| 221 | + break; // short-circuit |
| 222 | + } |
| 223 | + } |
| 224 | + // At this point, we must not have meet the quorum |
| 225 | + return 'dberrors'; // not enough votes to ensure correctness |
| 226 | + } |
| 227 | + |
| 228 | + /** |
| 229 | + * Get (or reuse) a connection to a lock DB |
| 230 | + * |
| 231 | + * @param $lockDb string |
| 232 | + * @return Database |
| 233 | + * @throws DBError |
| 234 | + */ |
| 235 | + protected function getConnection( $lockDb ) { |
| 236 | + if ( !isset( $this->conns[$lockDb] ) ) { |
| 237 | + $config = $this->dbServers[$lockDb]; |
| 238 | + $this->conns[$lockDb] = DatabaseBase::factory( $config['type'], $config ); |
| 239 | + if ( !$this->conns[$lockDb] ) { |
| 240 | + return null; // config error? |
| 241 | + } |
| 242 | + # If the connection drops, try to avoid letting the DB rollback |
| 243 | + # and release the locks before the file operations are finished. |
| 244 | + # This won't handle the case of DB server restarts however. |
| 245 | + $options = array(); |
| 246 | + if ( $this->lockExpiry > 0 ) { |
| 247 | + $options['connTimeout'] = $this->lockExpiry; |
| 248 | + } |
| 249 | + $this->conns[$lockDb]->setSessionOptions( $options ); |
| 250 | + $this->initConnection( $lockDb, $this->conns[$lockDb] ); |
| 251 | + } |
| 252 | + if ( !$this->conns[$lockDb]->trxLevel() ) { |
| 253 | + $this->conns[$lockDb]->begin(); // start transaction |
| 254 | + } |
| 255 | + return $this->conns[$lockDb]; |
| 256 | + } |
| 257 | + |
| 258 | + /** |
| 259 | + * Do additional initialization for new lock DB connection |
| 260 | + * |
| 261 | + * @param $lockDb string |
| 262 | + * @param $db DatabaseBase |
| 263 | + * @return void |
| 264 | + * @throws DBError |
| 265 | + */ |
| 266 | + protected function initConnection( $lockDb, DatabaseBase $db ) {} |
| 267 | + |
| 268 | + /** |
| 269 | + * Commit all changes to lock-active databases. |
| 270 | + * This should avoid throwing any exceptions. |
| 271 | + * |
| 272 | + * @return Status |
| 273 | + */ |
| 274 | + protected function finishLockTransactions() { |
| 275 | + $status = Status::newGood(); |
| 276 | + foreach ( $this->conns as $lockDb => $db ) { |
| 277 | + if ( $db->trxLevel() ) { // in transaction |
| 278 | + try { |
| 279 | + $db->rollback(); // finish transaction and kill any rows |
| 280 | + } catch ( DBError $e ) { |
| 281 | + $status->fatal( 'lockmanager-fail-db-release', $lockDb ); |
| 282 | + } |
| 283 | + } |
| 284 | + } |
| 285 | + return $status; |
| 286 | + } |
| 287 | + |
| 288 | + /** |
| 289 | + * Check if the last DB error for $lockDb indicates |
| 290 | + * that a requested resource was locked by another process. |
| 291 | + * This should avoid throwing any exceptions. |
| 292 | + * |
| 293 | + * @param $lockDb string |
| 294 | + * @return bool |
| 295 | + */ |
| 296 | + protected function lastErrorIndicatesLocked( $lockDb ) { |
| 297 | + if ( isset( $this->conns[$lockDb] ) ) { // sanity |
| 298 | + $db = $this->conns[$lockDb]; |
| 299 | + return ( $db->wasDeadlock() || $db->wasLockTimeout() ); |
| 300 | + } |
| 301 | + return false; |
| 302 | + } |
| 303 | + |
| 304 | + /** |
| 305 | + * Checks if the DB server did not recently restart. |
| 306 | + * This curtails the problem of locks falling off when DB servers restart. |
| 307 | + * |
| 308 | + * @param $lockDb string |
| 309 | + * @return bool |
| 310 | + * @throws DBError |
| 311 | + */ |
| 312 | + protected function checkUptime( $lockDb ) { |
| 313 | + if ( isset( $this->conns[$lockDb] ) ) { // sanity |
| 314 | + if ( $this->safeDelay > 0 ) { |
| 315 | + $db = $this->conns[$lockDb]; |
| 316 | + return ( $db->getServerUptime() > $this->safeDelay ); |
| 317 | + } |
| 318 | + return true; |
| 319 | + } |
| 320 | + return false; |
| 321 | + } |
| 322 | + |
| 323 | + /** |
| 324 | + * Checks if the DB has not recently had connection/query errors. |
| 325 | + * When in "trust cache" mode, this curtails the problem of peers occasionally |
| 326 | + * missing locks. Otherwise, it just avoids wasting time on connection attempts. |
| 327 | + * |
| 328 | + * @param $lockDb string |
| 329 | + * @return bool |
| 330 | + */ |
| 331 | + protected function cacheCheckFailures( $lockDb ) { |
| 332 | + if ( $this->statusCache && $this->safeDelay > 0 ) { |
| 333 | + $key = $this->getMissKey( $lockDb ); |
| 334 | + $misses = $this->statusCache->get( $key ); |
| 335 | + return !$misses; |
| 336 | + } |
| 337 | + return true; |
| 338 | + } |
| 339 | + |
| 340 | + /** |
| 341 | + * Log a lock request failure to the cache. |
| 342 | + * |
| 343 | + * Worst case scenario is that a resource lock was only |
| 344 | + * on one peer and then that peer is restarted or goes down. |
| 345 | + * Clients trying to get locks need to know if a DB server is down. |
| 346 | + * |
| 347 | + * @param $lockDb string |
| 348 | + * @return bool Success |
| 349 | + */ |
| 350 | + protected function cacheRecordFailure( $lockDb ) { |
| 351 | + if ( $this->statusCache && $this->safeDelay > 0 ) { |
| 352 | + $key = $this->getMissKey( $lockDb ); |
| 353 | + $misses = $this->statusCache->get( $key ); |
| 354 | + if ( $misses ) { |
| 355 | + return $this->statusCache->incr( $key ); |
| 356 | + } else { |
| 357 | + return $this->statusCache->add( $key, 1, $this->safeDelay ); |
| 358 | + } |
| 359 | + } |
| 360 | + return true; |
| 361 | + } |
| 362 | + |
| 363 | + /** |
| 364 | + * Get a cache key for recent query misses for a DB |
| 365 | + * |
| 366 | + * @param $lockDb string |
| 367 | + * @return string |
| 368 | + */ |
| 369 | + protected function getMissKey( $lockDb ) { |
| 370 | + return 'lockmanager:querymisses:' . str_replace( ' ', '_', $lockDb ); |
| 371 | + } |
| 372 | + |
| 373 | + /** |
| 374 | + * Get the bucket for lock key. |
| 375 | + * This should avoid throwing any exceptions. |
| 376 | + * |
| 377 | + * @param $key string (40 char hex key) |
| 378 | + * @return integer |
| 379 | + */ |
| 380 | + protected function getBucketFromKey( $key ) { |
| 381 | + $prefix = substr( $key, 0, 2 ); // first 2 hex chars (8 bits) |
| 382 | + return intval( base_convert( $prefix, 16, 10 ) ) % count( $this->dbsByBucket ); |
| 383 | + } |
| 384 | + |
| 385 | + /** |
| 386 | + * Make sure remaining locks get cleared for sanity |
| 387 | + */ |
| 388 | + function __destruct() { |
| 389 | + foreach ( $this->conns as $lockDb => $db ) { |
| 390 | + if ( $db->trxLevel() ) { // in transaction |
| 391 | + try { |
| 392 | + $db->rollback(); // finish transaction and kill any rows |
| 393 | + } catch ( DBError $e ) { |
| 394 | + // oh well |
| 395 | + } |
| 396 | + } |
| 397 | + $db->close(); |
| 398 | + } |
| 399 | + } |
| 400 | +} |
| 401 | + |
| 402 | +/** |
| 403 | + * MySQL version of DBLockManager that supports shared locks. |
| 404 | + * All locks are non-blocking, which avoids deadlocks. |
| 405 | + */ |
| 406 | +class MySqlLockManager extends DBLockManager { |
| 407 | + /** @var Array Mapping of lock types to the type actually used */ |
| 408 | + protected $lockTypeMap = array( |
| 409 | + self::LOCK_SH => self::LOCK_SH, |
| 410 | + self::LOCK_UW => self::LOCK_SH, |
| 411 | + self::LOCK_EX => self::LOCK_EX |
| 412 | + ); |
| 413 | + |
| 414 | + protected function initConnection( $lockDb, DatabaseBase $db ) { |
| 415 | + # Let this transaction see lock rows from other transactions |
| 416 | + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" ); |
| 417 | + } |
| 418 | + |
| 419 | + protected function doLockingQuery( $lockDb, array $keys, $type ) { |
| 420 | + $db = $this->getConnection( $lockDb ); |
| 421 | + if ( !$db ) { |
| 422 | + return false; |
| 423 | + } |
| 424 | + $data = array(); |
| 425 | + foreach ( $keys as $key ) { |
| 426 | + $data[] = array( 'fls_key' => $key, 'fls_session' => $this->session ); |
| 427 | + } |
| 428 | + # Block new writers... |
| 429 | + $db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) ); |
| 430 | + # Actually do the locking queries... |
| 431 | + if ( $type == self::LOCK_SH ) { // reader locks |
| 432 | + # Bail if there are any existing writers... |
| 433 | + $blocked = $db->selectField( 'filelocks_exclusive', '1', |
| 434 | + array( 'fle_key' => $keys ), |
| 435 | + __METHOD__ |
| 436 | + ); |
| 437 | + # Prospective writers that haven't yet updated filelocks_exclusive |
| 438 | + # will recheck filelocks_shared after doing so and bail due to our entry. |
| 439 | + } else { // writer locks |
| 440 | + $encSession = $db->addQuotes( $this->session ); |
| 441 | + # Bail if there are any existing writers... |
| 442 | + # The may detect readers, but the safe check for them is below. |
| 443 | + # Note: if two writers come at the same time, both bail :) |
| 444 | + $blocked = $db->selectField( 'filelocks_shared', '1', |
| 445 | + array( 'fls_key' => $keys, "fls_session != $encSession" ), |
| 446 | + __METHOD__ |
| 447 | + ); |
| 448 | + if ( !$blocked ) { |
| 449 | + $data = array(); |
| 450 | + foreach ( $keys as $key ) { |
| 451 | + $data[] = array( 'fle_key' => $key ); |
| 452 | + } |
| 453 | + # Block new readers/writers... |
| 454 | + $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); |
| 455 | + # Bail if there are any existing readers... |
| 456 | + $blocked = $db->selectField( 'filelocks_shared', '1', |
| 457 | + array( 'fls_key' => $keys, "fls_session != $encSession" ), |
| 458 | + __METHOD__ |
| 459 | + ); |
| 460 | + } |
| 461 | + } |
| 462 | + return !$blocked; |
| 463 | + } |
| 464 | +} |
Property changes on: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/DBLockManager.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 465 | + native |
Index: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/FSLockManager.php |
— | — | @@ -0,0 +1,197 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * Simple version of LockManager based on using FS lock files. |
| 6 | + * All locks are non-blocking, which avoids deadlocks. |
| 7 | + * |
| 8 | + * This should work fine for small sites running off one server. |
| 9 | + * Do not use this with 'lockDir' set to an NFS mount unless the |
| 10 | + * NFS client is at least version 2.6.12. Otherwise, the BSD flock() |
| 11 | + * locks will be ignored; see http://nfs.sourceforge.net/#section_d. |
| 12 | + */ |
| 13 | +class FSLockManager extends LockManager { |
| 14 | + /** @var Array Mapping of lock types to the type actually used */ |
| 15 | + protected $lockTypeMap = array( |
| 16 | + self::LOCK_SH => self::LOCK_SH, |
| 17 | + self::LOCK_UW => self::LOCK_SH, |
| 18 | + self::LOCK_EX => self::LOCK_EX |
| 19 | + ); |
| 20 | + |
| 21 | + protected $lockDir; // global dir for all servers |
| 22 | + |
| 23 | + /** @var Array Map of (locked key => lock type => count) */ |
| 24 | + protected $locksHeld = array(); |
| 25 | + /** @var Array Map of (locked key => lock type => lock file handle) */ |
| 26 | + protected $handles = array(); |
| 27 | + |
| 28 | + function __construct( array $config ) { |
| 29 | + $this->lockDir = $config['lockDirectory']; |
| 30 | + } |
| 31 | + |
| 32 | + protected function doLock( array $keys, $type ) { |
| 33 | + $status = Status::newGood(); |
| 34 | + |
| 35 | + $lockedKeys = array(); // files locked in this attempt |
| 36 | + foreach ( $keys as $key ) { |
| 37 | + $subStatus = $this->doSingleLock( $key, $type ); |
| 38 | + $status->merge( $subStatus ); |
| 39 | + if ( $status->isOK() ) { |
| 40 | + // Don't append to $lockedKeys if $key is already locked. |
| 41 | + // We do NOT want to unlock the key if we have to rollback. |
| 42 | + if ( $subStatus->isGood() ) { // no warnings/fatals? |
| 43 | + $lockedKeys[] = $key; |
| 44 | + } |
| 45 | + } else { |
| 46 | + // Abort and unlock everything |
| 47 | + $status->merge( $this->doUnlock( $lockedKeys, $type ) ); |
| 48 | + return $status; |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + return $status; |
| 53 | + } |
| 54 | + |
| 55 | + protected function doUnlock( array $keys, $type ) { |
| 56 | + $status = Status::newGood(); |
| 57 | + |
| 58 | + foreach ( $keys as $key ) { |
| 59 | + $status->merge( $this->doSingleUnlock( $key, $type ) ); |
| 60 | + } |
| 61 | + |
| 62 | + return $status; |
| 63 | + } |
| 64 | + |
| 65 | + /** |
| 66 | + * Lock a single resource key |
| 67 | + * |
| 68 | + * @param $key string |
| 69 | + * @param $type integer |
| 70 | + * @return Status |
| 71 | + */ |
| 72 | + protected function doSingleLock( $key, $type ) { |
| 73 | + $status = Status::newGood(); |
| 74 | + |
| 75 | + if ( isset( $this->locksHeld[$key][$type] ) ) { |
| 76 | + ++$this->locksHeld[$key][$type]; |
| 77 | + } elseif ( isset( $this->locksHeld[$key][self::LOCK_EX] ) ) { |
| 78 | + $this->locksHeld[$key][$type] = 1; |
| 79 | + } else { |
| 80 | + wfSuppressWarnings(); |
| 81 | + $handle = fopen( $this->getLockPath( $key ), 'a+' ); |
| 82 | + wfRestoreWarnings(); |
| 83 | + if ( !$handle ) { // lock dir missing? |
| 84 | + wfMkdirParents( $this->lockDir ); |
| 85 | + wfSuppressWarnings(); |
| 86 | + $handle = fopen( $this->getLockPath( $key ), 'a+' ); // try again |
| 87 | + wfRestoreWarnings(); |
| 88 | + } |
| 89 | + if ( $handle ) { |
| 90 | + // Either a shared or exclusive lock |
| 91 | + $lock = ( $type == self::LOCK_SH ) ? LOCK_SH : LOCK_EX; |
| 92 | + if ( flock( $handle, $lock | LOCK_NB ) ) { |
| 93 | + // Record this lock as active |
| 94 | + $this->locksHeld[$key][$type] = 1; |
| 95 | + $this->handles[$key][$type] = $handle; |
| 96 | + } else { |
| 97 | + fclose( $handle ); |
| 98 | + $status->fatal( 'lockmanager-fail-acquirelock', $key ); |
| 99 | + } |
| 100 | + } else { |
| 101 | + $status->fatal( 'lockmanager-fail-openlock', $key ); |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + return $status; |
| 106 | + } |
| 107 | + |
| 108 | + /** |
| 109 | + * Unlock a single resource key |
| 110 | + * |
| 111 | + * @param $key string |
| 112 | + * @param $type integer |
| 113 | + * @return Status |
| 114 | + */ |
| 115 | + protected function doSingleUnlock( $key, $type ) { |
| 116 | + $status = Status::newGood(); |
| 117 | + |
| 118 | + if ( !isset( $this->locksHeld[$key] ) ) { |
| 119 | + $status->warning( 'lockmanager-notlocked', $key ); |
| 120 | + } elseif ( !isset( $this->locksHeld[$key][$type] ) ) { |
| 121 | + $status->warning( 'lockmanager-notlocked', $key ); |
| 122 | + } else { |
| 123 | + $handlesToClose = array(); |
| 124 | + --$this->locksHeld[$key][$type]; |
| 125 | + if ( $this->locksHeld[$key][$type] <= 0 ) { |
| 126 | + unset( $this->locksHeld[$key][$type] ); |
| 127 | + // If a LOCK_SH comes in while we have a LOCK_EX, we don't |
| 128 | + // actually add a handler, so check for handler existence. |
| 129 | + if ( isset( $this->handles[$key][$type] ) ) { |
| 130 | + // Mark this handle to be unlocked and closed |
| 131 | + $handlesToClose[] = $this->handles[$key][$type]; |
| 132 | + unset( $this->handles[$key][$type] ); |
| 133 | + } |
| 134 | + } |
| 135 | + // Unlock handles to release locks and delete |
| 136 | + // any lock files that end up with no locks on them... |
| 137 | + if ( wfIsWindows() ) { |
| 138 | + // Windows: for any process, including this one, |
| 139 | + // calling unlink() on a locked file will fail |
| 140 | + $status->merge( $this->closeLockHandles( $key, $handlesToClose ) ); |
| 141 | + $status->merge( $this->pruneKeyLockFiles( $key ) ); |
| 142 | + } else { |
| 143 | + // Unix: unlink() can be used on files currently open by this |
| 144 | + // process and we must do so in order to avoid race conditions |
| 145 | + $status->merge( $this->pruneKeyLockFiles( $key ) ); |
| 146 | + $status->merge( $this->closeLockHandles( $key, $handlesToClose ) ); |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + return $status; |
| 151 | + } |
| 152 | + |
| 153 | + private function closeLockHandles( $key, array $handlesToClose ) { |
| 154 | + $status = Status::newGood(); |
| 155 | + foreach ( $handlesToClose as $handle ) { |
| 156 | + wfSuppressWarnings(); |
| 157 | + if ( !flock( $handle, LOCK_UN ) ) { |
| 158 | + $status->fatal( 'lockmanager-fail-releaselock', $key ); |
| 159 | + } |
| 160 | + if ( !fclose( $handle ) ) { |
| 161 | + $status->warning( 'lockmanager-fail-closelock', $key ); |
| 162 | + } |
| 163 | + wfRestoreWarnings(); |
| 164 | + } |
| 165 | + return $status; |
| 166 | + } |
| 167 | + |
| 168 | + private function pruneKeyLockFiles( $key ) { |
| 169 | + $status = Status::newGood(); |
| 170 | + if ( !count( $this->locksHeld[$key] ) ) { |
| 171 | + wfSuppressWarnings(); |
| 172 | + # No locks are held for the lock file anymore |
| 173 | + if ( !unlink( $this->getLockPath( $key ) ) ) { |
| 174 | + $status->warning( 'lockmanager-fail-deletelock', $key ); |
| 175 | + } |
| 176 | + wfRestoreWarnings(); |
| 177 | + unset( $this->locksHeld[$key] ); |
| 178 | + unset( $this->handles[$key] ); |
| 179 | + } |
| 180 | + return $status; |
| 181 | + } |
| 182 | + |
| 183 | + /** |
| 184 | + * Get the path to the lock file for a key |
| 185 | + * @param $key string |
| 186 | + * @return string |
| 187 | + */ |
| 188 | + protected function getLockPath( $key ) { |
| 189 | + return "{$this->lockDir}/{$key}.lock"; |
| 190 | + } |
| 191 | + |
| 192 | + function __destruct() { |
| 193 | + // Make sure remaining locks get cleared for sanity |
| 194 | + foreach ( $this->locksHeld as $key => $locks ) { |
| 195 | + $this->doSingleUnlock( $key, 0 ); |
| 196 | + } |
| 197 | + } |
| 198 | +} |
Property changes on: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/FSLockManager.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 199 | + native |
Index: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/LockManagerGroup.php |
— | — | @@ -0,0 +1,68 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * Class to handle file lock manager registration |
| 5 | + * |
| 6 | + * @ingroup FileBackend |
| 7 | + */ |
| 8 | +class LockManagerGroup { |
| 9 | + protected static $instance = null; |
| 10 | + |
| 11 | + /** @var Array of (name => ('class' =>, 'config' =>, 'instance' =>)) */ |
| 12 | + protected $managers = array(); |
| 13 | + |
| 14 | + protected function __construct() {} |
| 15 | + protected function __clone() {} |
| 16 | + |
| 17 | + public static function singleton() { |
| 18 | + if ( self::$instance == null ) { |
| 19 | + self::$instance = new self(); |
| 20 | + } |
| 21 | + return self::$instance; |
| 22 | + } |
| 23 | + |
| 24 | + /** |
| 25 | + * Register an array of file lock manager configurations |
| 26 | + * |
| 27 | + * @param $configs Array |
| 28 | + * @return void |
| 29 | + * @throws MWException |
| 30 | + */ |
| 31 | + public function register( array $configs ) { |
| 32 | + foreach ( $configs as $config ) { |
| 33 | + if ( !isset( $config['name'] ) ) { |
| 34 | + throw new MWException( "Cannot register a lock manager with no name." ); |
| 35 | + } |
| 36 | + $name = $config['name']; |
| 37 | + if ( !isset( $config['class'] ) ) { |
| 38 | + throw new MWException( "Cannot register lock manager `{$name}` with no class." ); |
| 39 | + } |
| 40 | + $class = $config['class']; |
| 41 | + unset( $config['class'] ); // lock manager won't need this |
| 42 | + $this->managers[$name] = array( |
| 43 | + 'class' => $class, |
| 44 | + 'config' => $config, |
| 45 | + 'instance' => null |
| 46 | + ); |
| 47 | + } |
| 48 | + } |
| 49 | + |
| 50 | + /** |
| 51 | + * Get the lock manager object with a given name |
| 52 | + * |
| 53 | + * @param $name string |
| 54 | + * @return LockManager |
| 55 | + * @throws MWException |
| 56 | + */ |
| 57 | + public function get( $name ) { |
| 58 | + if ( !isset( $this->managers[$name] ) ) { |
| 59 | + throw new MWException( "No lock manager defined with the name `$name`." ); |
| 60 | + } |
| 61 | + // Lazy-load the actual backend instance |
| 62 | + if ( !isset( $this->managers[$name]['instance'] ) ) { |
| 63 | + $class = $this->managers[$name]['class']; |
| 64 | + $config = $this->managers[$name]['config']; |
| 65 | + $this->managers[$name]['instance'] = new $class( $config ); |
| 66 | + } |
| 67 | + return $this->managers[$name]['instance']; |
| 68 | + } |
| 69 | +} |
Property changes on: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/LockManagerGroup.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 70 | + native |
Index: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/LockManager.php |
— | — | @@ -0,0 +1,152 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * Class for handling resource locking. |
| 5 | + * Locks on resource keys can either be shared or exclusive. |
| 6 | + * |
| 7 | + * Implementations must keep track of what is locked by this proccess |
| 8 | + * in-memory and support nested locking calls (using reference counting). |
| 9 | + * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op. |
| 10 | + * Locks should either be non-blocking or have low wait timeouts. |
| 11 | + * |
| 12 | + * Subclasses should avoid throwing exceptions at all costs. |
| 13 | + * |
| 14 | + * @ingroup FileBackend |
| 15 | + */ |
| 16 | +abstract class LockManager { |
| 17 | + /* Lock types; stronger locks have higher values */ |
| 18 | + const LOCK_SH = 1; // shared lock (for reads) |
| 19 | + const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) |
| 20 | + const LOCK_EX = 3; // exclusive lock (for writes) |
| 21 | + |
| 22 | + /** @var Array Mapping of lock types to the type actually used */ |
| 23 | + protected $lockTypeMap = array( |
| 24 | + self::LOCK_SH => self::LOCK_SH, |
| 25 | + self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH |
| 26 | + self::LOCK_EX => self::LOCK_EX |
| 27 | + ); |
| 28 | + |
| 29 | + /** |
| 30 | + * Construct a new instance from configuration |
| 31 | + * |
| 32 | + * @param $config Array |
| 33 | + */ |
| 34 | + public function __construct( array $config ) {} |
| 35 | + |
| 36 | + /** |
| 37 | + * Lock the resources at the given abstract paths |
| 38 | + * |
| 39 | + * @param $paths Array List of resource names |
| 40 | + * @param $type integer LockManager::LOCK_* constant |
| 41 | + * @return Status |
| 42 | + */ |
| 43 | + final public function lock( array $paths, $type = self::LOCK_EX ) { |
| 44 | + $keys = array_unique( array_map( 'sha1', $paths ) ); |
| 45 | + return $this->doLock( $keys, $this->lockTypeMap[$type] ); |
| 46 | + } |
| 47 | + |
| 48 | + /** |
| 49 | + * Unlock the resources at the given abstract paths |
| 50 | + * |
| 51 | + * @param $paths Array List of storage paths |
| 52 | + * @param $type integer LockManager::LOCK_* constant |
| 53 | + * @return Status |
| 54 | + */ |
| 55 | + final public function unlock( array $paths, $type = self::LOCK_EX ) { |
| 56 | + $keys = array_unique( array_map( 'sha1', $paths ) ); |
| 57 | + return $this->doUnlock( $keys, $this->lockTypeMap[$type] ); |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * Lock resources with the given keys and lock type |
| 62 | + * |
| 63 | + * @param $key Array List of keys to lock (40 char hex hashes) |
| 64 | + * @param $type integer LockManager::LOCK_* constant |
| 65 | + * @return string |
| 66 | + */ |
| 67 | + abstract protected function doLock( array $keys, $type ); |
| 68 | + |
| 69 | + /** |
| 70 | + * Unlock resources with the given keys and lock type |
| 71 | + * |
| 72 | + * @param $key Array List of keys to unlock (40 char hex hashes) |
| 73 | + * @param $type integer LockManager::LOCK_* constant |
| 74 | + * @return string |
| 75 | + */ |
| 76 | + abstract protected function doUnlock( array $keys, $type ); |
| 77 | +} |
| 78 | + |
| 79 | +/** |
| 80 | + * LockManager helper class to handle scoped locks, which |
| 81 | + * release when an object is destroyed or goes out of scope. |
| 82 | + */ |
| 83 | +class ScopedLock { |
| 84 | + /** @var LockManager */ |
| 85 | + protected $manager; |
| 86 | + /** @var Status */ |
| 87 | + protected $status; |
| 88 | + /** @var Array List of resource paths*/ |
| 89 | + protected $paths; |
| 90 | + |
| 91 | + protected $type; // integer lock type |
| 92 | + |
| 93 | + /** |
| 94 | + * @param $manager LockManager |
| 95 | + * @param $paths Array List of storage paths |
| 96 | + * @param $type integer LockManager::LOCK_* constant |
| 97 | + * @param $status Status |
| 98 | + */ |
| 99 | + protected function __construct( |
| 100 | + LockManager $manager, array $paths, $type, Status $status |
| 101 | + ) { |
| 102 | + $this->manager = $manager; |
| 103 | + $this->paths = $paths; |
| 104 | + $this->status = $status; |
| 105 | + $this->type = $type; |
| 106 | + } |
| 107 | + |
| 108 | + protected function __clone() {} |
| 109 | + |
| 110 | + /** |
| 111 | + * Get a ScopedLock object representing a lock on resource paths. |
| 112 | + * Any locks are released once this object goes out of scope. |
| 113 | + * The status object is updated with any errors or warnings. |
| 114 | + * |
| 115 | + * @param $manager LockManager |
| 116 | + * @param $paths Array List of storage paths |
| 117 | + * @param $type integer LockManager::LOCK_* constant |
| 118 | + * @param $status Status |
| 119 | + * @return ScopedLock|null Returns null on failure |
| 120 | + */ |
| 121 | + public static function factory( |
| 122 | + LockManager $manager, array $paths, $type, Status $status |
| 123 | + ) { |
| 124 | + $lockStatus = $manager->lock( $paths, $type ); |
| 125 | + $status->merge( $lockStatus ); |
| 126 | + if ( $lockStatus->isOK() ) { |
| 127 | + return new self( $manager, $paths, $type, $status ); |
| 128 | + } |
| 129 | + return null; |
| 130 | + } |
| 131 | + |
| 132 | + function __destruct() { |
| 133 | + $wasOk = $this->status->isOK(); |
| 134 | + $this->status->merge( $this->manager->unlock( $this->paths, $this->type ) ); |
| 135 | + if ( $wasOk ) { |
| 136 | + // Make sure status is OK, despite any unlockFiles() fatals |
| 137 | + $this->status->setResult( true, $this->status->value ); |
| 138 | + } |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +/** |
| 143 | + * Simple version of LockManager that does nothing |
| 144 | + */ |
| 145 | +class NullLockManager extends LockManager { |
| 146 | + protected function doLock( array $keys, $type ) { |
| 147 | + return Status::newGood(); |
| 148 | + } |
| 149 | + |
| 150 | + protected function doUnlock( array $keys, $type ) { |
| 151 | + return Status::newGood(); |
| 152 | + } |
| 153 | +} |
Property changes on: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/LockManager.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 154 | + native |
Property changes on: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager |
___________________________________________________________________ |
Added: bugtraq:number |
2 | 155 | + true |
Index: branches/FileBackend/phase3/includes/filerepo/FileRepo.php |
— | — | @@ -507,7 +507,7 @@ |
508 | 508 | * |
509 | 509 | * @return integer |
510 | 510 | */ |
511 | | - function getHashLevels() { |
| 511 | + public function getHashLevels() { |
512 | 512 | return $this->hashLevels; |
513 | 513 | } |
514 | 514 | |
Index: branches/FileBackend/phase3/includes/AutoLoader.php |
— | — | @@ -491,13 +491,13 @@ |
492 | 492 | 'FileBackendMultiWrite' => 'includes/filerepo/backend/FileBackendMultiWrite.php', |
493 | 493 | 'FSFileBackend' => 'includes/filerepo/backend/FSFileBackend.php', |
494 | 494 | 'FSFileIterator' => 'includes/filerepo/backend/FSFileBackend.php', |
495 | | - 'LockManagerGroup' => 'includes/filerepo/backend/LockManagerGroup.php', |
496 | | - 'LockManager' => 'includes/filerepo/backend/LockManager.php', |
497 | | - 'ScopedLock' => 'includes/filerepo/backend/LockManager.php', |
498 | | - 'FSLockManager' => 'includes/filerepo/backend/LockManager.php', |
499 | | - 'DBLockManager' => 'includes/filerepo/backend/LockManager.php', |
500 | | - 'MySqlLockManager'=> 'includes/filerepo/backend/LockManager.php', |
501 | | - 'NullLockManager' => 'includes/filerepo/backend/LockManager.php', |
| 495 | + 'LockManagerGroup' => 'includes/filerepo/backend/lockmanager/LockManagerGroup.php', |
| 496 | + 'LockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php', |
| 497 | + 'ScopedLock' => 'includes/filerepo/backend/lockmanager/LockManager.php', |
| 498 | + 'FSLockManager' => 'includes/filerepo/backend/lockmanager/FSLockManager.php', |
| 499 | + 'DBLockManager' => 'includes/filerepo/backend/lockmanager/DBLockManager.php', |
| 500 | + 'MySqlLockManager'=> 'includes/filerepo/backend/lockmanager/LockManager.php', |
| 501 | + 'NullLockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php', |
502 | 502 | 'FileOp' => 'includes/filerepo/backend/FileOp.php', |
503 | 503 | 'StoreFileOp' => 'includes/filerepo/backend/FileOp.php', |
504 | 504 | 'CopyFileOp' => 'includes/filerepo/backend/FileOp.php', |