r106532 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r106531‎ | r106532 | r106533 >
Date:23:00, 17 December 2011
Author:aaron
Status:deferred
Tags:
Comment:
Move lock manager classes to their own files under a /lockmanager directory
Modified paths:
  • /branches/FileBackend/phase3/includes/AutoLoader.php (modified) (history)
  • /branches/FileBackend/phase3/includes/filerepo/FileRepo.php (modified) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/LockManager.php (deleted) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/LockManagerGroup.php (deleted) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/lockmanager (added) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/DBLockManager.php (added) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/FSLockManager.php (added) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/LockManager.php (added) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/lockmanager/LockManagerGroup.php (added) (history)

Diff [purge]

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
1465 + 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
1199 + 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
170 + 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
1154 + native
Property changes on: branches/FileBackend/phase3/includes/filerepo/backend/lockmanager
___________________________________________________________________
Added: bugtraq:number
2155 + true
Index: branches/FileBackend/phase3/includes/filerepo/FileRepo.php
@@ -507,7 +507,7 @@
508508 *
509509 * @return integer
510510 */
511 - function getHashLevels() {
 511+ public function getHashLevels() {
512512 return $this->hashLevels;
513513 }
514514
Index: branches/FileBackend/phase3/includes/AutoLoader.php
@@ -491,13 +491,13 @@
492492 'FileBackendMultiWrite' => 'includes/filerepo/backend/FileBackendMultiWrite.php',
493493 'FSFileBackend' => 'includes/filerepo/backend/FSFileBackend.php',
494494 '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',
502502 'FileOp' => 'includes/filerepo/backend/FileOp.php',
503503 'StoreFileOp' => 'includes/filerepo/backend/FileOp.php',
504504 'CopyFileOp' => 'includes/filerepo/backend/FileOp.php',

Status & tagging log