Index: branches/FileBackend/phase3/includes/filerepo/backend/LockManager.php |
— | — | @@ -3,18 +3,28 @@ |
4 | 4 | * FileBackend helper class for handling file locking. |
5 | 5 | * Locks on resource keys can either be shared or exclusive. |
6 | 6 | * |
7 | | - * Implementations can keep track of what is locked in the process cache. |
8 | | - * This can reduce hits to external resources for lock()/unlock() calls. |
| 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. |
9 | 11 | * |
10 | 12 | * Subclasses should avoid throwing exceptions at all costs. |
11 | 13 | * |
12 | 14 | * @ingroup FileBackend |
13 | 15 | */ |
14 | 16 | abstract class LockManager { |
15 | | - /* Lock types; stronger locks have high values */ |
| 17 | + /* Lock types; stronger locks have higher values */ |
16 | 18 | const LOCK_SH = 1; // shared lock (for reads) |
17 | | - const LOCK_EX = 2; // exclusive lock (for writes) |
| 19 | + const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) |
| 20 | + const LOCK_EX = 3; // exclusive lock (for writes) |
18 | 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 | + |
19 | 29 | /** |
20 | 30 | * Construct a new instance from configuration |
21 | 31 | * |
— | — | @@ -26,31 +36,31 @@ |
27 | 37 | * Lock the resources at the given abstract paths |
28 | 38 | * |
29 | 39 | * @param $paths Array List of resource names |
30 | | - * @param $type integer LockManager::LOCK_EX, LockManager::LOCK_SH |
| 40 | + * @param $type integer LockManager::LOCK_* constant |
31 | 41 | * @return Status |
32 | 42 | */ |
33 | 43 | final public function lock( array $paths, $type = self::LOCK_EX ) { |
34 | 44 | $keys = array_unique( array_map( 'sha1', $paths ) ); |
35 | | - return $this->doLock( $keys, $type ); |
| 45 | + return $this->doLock( $keys, $this->lockTypeMap[$type] ); |
36 | 46 | } |
37 | 47 | |
38 | 48 | /** |
39 | 49 | * Unlock the resources at the given abstract paths |
40 | 50 | * |
41 | 51 | * @param $paths Array List of storage paths |
42 | | - * @param $type integer LockManager::LOCK_EX, LockManager::LOCK_SH |
| 52 | + * @param $type integer LockManager::LOCK_* constant |
43 | 53 | * @return Status |
44 | 54 | */ |
45 | 55 | final public function unlock( array $paths, $type = self::LOCK_EX ) { |
46 | 56 | $keys = array_unique( array_map( 'sha1', $paths ) ); |
47 | | - return $this->doUnlock( $keys, $type ); |
| 57 | + return $this->doUnlock( $keys, $this->lockTypeMap[$type] ); |
48 | 58 | } |
49 | 59 | |
50 | 60 | /** |
51 | 61 | * Lock resources with the given keys and lock type |
52 | 62 | * |
53 | 63 | * @param $key Array List of keys to lock (40 char hex hashes) |
54 | | - * @param $type integer LockManager::LOCK_EX, LockManager::LOCK_SH |
| 64 | + * @param $type integer LockManager::LOCK_* constant |
55 | 65 | * @return string |
56 | 66 | */ |
57 | 67 | abstract protected function doLock( array $keys, $type ); |
— | — | @@ -59,7 +69,7 @@ |
60 | 70 | * Unlock resources with the given keys and lock type |
61 | 71 | * |
62 | 72 | * @param $key Array List of keys to unlock (40 char hex hashes) |
63 | | - * @param $type integer LockManager::LOCK_EX, LockManager::LOCK_SH |
| 73 | + * @param $type integer LockManager::LOCK_* constant |
64 | 74 | * @return string |
65 | 75 | */ |
66 | 76 | abstract protected function doUnlock( array $keys, $type ); |
— | — | @@ -74,6 +84,13 @@ |
75 | 85 | * locks will be ignored; see http://nfs.sourceforge.net/#section_d. |
76 | 86 | */ |
77 | 87 | class FSLockManager extends LockManager { |
| 88 | + /** @var Array Mapping of lock types to the type actually used */ |
| 89 | + protected $lockTypeMap = array( |
| 90 | + self::LOCK_SH => self::LOCK_SH, |
| 91 | + self::LOCK_UW => self::LOCK_SH, |
| 92 | + self::LOCK_EX => self::LOCK_EX |
| 93 | + ); |
| 94 | + |
78 | 95 | protected $lockDir; // global dir for all servers |
79 | 96 | |
80 | 97 | /** @var Array Map of (locked key => lock type => count) */ |
— | — | @@ -259,7 +276,7 @@ |
260 | 277 | * |
261 | 278 | * All lock requests for a resource, identified by a hash string, will |
262 | 279 | * map to one bucket. Each bucket maps to one or several peer DB servers, |
263 | | - * each having a `file_locks` table with row-level locking. |
| 280 | + * each having a the file_locks.sql tables with row-level locking. |
264 | 281 | * This does not use GET_LOCK() per http://bugs.mysql.com/bug.php?id=1118. |
265 | 282 | * |
266 | 283 | * A majority of peer servers must agree for a lock to be acquired. |
— | — | @@ -284,7 +301,7 @@ |
285 | 302 | |
286 | 303 | /** @var Array Map of (locked key => lock type => count) */ |
287 | 304 | protected $locksHeld = array(); |
288 | | - /** $var Array Map Lock-active database connections (server name => Database) */ |
| 305 | + /** @var Array Map Lock-active database connections (DB name => Database) */ |
289 | 306 | protected $activeConns = array(); |
290 | 307 | |
291 | 308 | /** |
— | — | @@ -363,7 +380,7 @@ |
364 | 381 | $status->merge( $this->doUnlock( $lockedKeys, $type ) ); |
365 | 382 | return $status; |
366 | 383 | } elseif ( $res !== true ) { |
367 | | - // Couldn't contact any servers for this bucket. |
| 384 | + // Couldn't contact any DBs for this bucket. |
368 | 385 | // Abort and unlock everything we just locked. |
369 | 386 | $status->fatal( 'lockmanager-fail-db-bucket', $bucket ); |
370 | 387 | $status->merge( $this->doUnlock( $lockedKeys, $type ) ); |
— | — | @@ -408,47 +425,26 @@ |
409 | 426 | } |
410 | 427 | |
411 | 428 | /** |
412 | | - * Get a DB connection to a lock server and acquire locks on $keys. |
| 429 | + * Get a connection to a lock DB and acquire locks on $keys |
413 | 430 | * |
414 | 431 | * @param $server string |
415 | 432 | * @param $keys Array |
416 | 433 | * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH |
417 | | - * @return void |
| 434 | + * @return bool Resources able to be locked |
| 435 | + * @throws DBError |
418 | 436 | */ |
419 | 437 | protected function doLockingQuery( $server, array $keys, $type ) { |
420 | | - if ( !isset( $this->activeConns[$server] ) ) { |
421 | | - $this->activeConns[$server] = wfGetDB( DB_MASTER, array(), $server ); |
422 | | - $this->activeConns[$server]->begin(); // start transaction |
423 | | - # If the connection drops, try to avoid letting the DB rollback |
424 | | - # and release the locks before the file operations are finished. |
425 | | - # This won't handle the case of server reboots however. |
426 | | - $options = array(); |
427 | | - if ( php_sapi_name() == 'cli' ) { // maintenance scripts |
428 | | - if ( $this->cliTimeout > 0 ) { |
429 | | - $options['connTimeout'] = $this->cliTimeout; |
430 | | - } |
431 | | - } else { // web requests |
432 | | - if ( $this->webTimeout > 0 ) { |
433 | | - $options['connTimeout'] = $this->webTimeout; |
434 | | - } |
435 | | - } |
436 | | - $this->activeConns[$server]->setSessionOptions( $options ); |
437 | | - } |
438 | | - $db = $this->activeConns[$server]; |
439 | | - # Try to get the locks...this should be the last query of this function |
440 | | - if ( $type == self::LOCK_SH ) { // reader locks |
441 | | - $db->select( 'file_locks', '1', |
442 | | - array( 'fl_key' => $keys ), |
443 | | - __METHOD__, |
444 | | - array( 'LOCK IN SHARE MODE' ) // single-row gap locks |
445 | | - ); |
446 | | - } else { // writer locks |
| 438 | + if ( $type == self::LOCK_EX ) { // writer locks |
| 439 | + $db = $this->getConnection( $server ); |
| 440 | + # Actually do the locking queries... |
447 | 441 | $data = array(); |
448 | 442 | foreach ( $keys as $key ) { |
449 | | - $data[] = array( 'fl_key' => $key ); |
| 443 | + $data[] = array( 'fle_key' => $key ); |
450 | 444 | } |
451 | | - $db->insert( 'file_locks', $data, __METHOD__ ); |
| 445 | + # Wait on any existing writers and block new ones if we get in |
| 446 | + $db->insert( 'file_locks_exclusive', $data, __METHOD__ ); |
452 | 447 | } |
| 448 | + return true; |
453 | 449 | } |
454 | 450 | |
455 | 451 | /** |
— | — | @@ -462,7 +458,7 @@ |
463 | 459 | */ |
464 | 460 | protected function doLockingQueryAll( $bucket, array $keys, $type ) { |
465 | 461 | $yesVotes = 0; // locks made on trustable servers |
466 | | - $votesLeft = count( $this->dbsByBucket[$bucket] ); // remaining servers |
| 462 | + $votesLeft = count( $this->dbsByBucket[$bucket] ); // remaining DBs |
467 | 463 | $quorum = floor( $votesLeft/2 + 1 ); // simple majority |
468 | 464 | // Get votes for each server, in order, until we have enough... |
469 | 465 | foreach ( $this->dbsByBucket[$bucket] as $index => $server ) { |
— | — | @@ -478,7 +474,9 @@ |
479 | 475 | if ( $this->cacheCheckFailures( $server ) ) { |
480 | 476 | try { |
481 | 477 | // Attempt to acquire the lock on this server |
482 | | - $this->doLockingQuery( $server, $keys, $type ); |
| 478 | + if ( !$this->doLockingQuery( $server, $keys, $type ) ) { |
| 479 | + return 'cantacquire'; // vetoed; resource locked |
| 480 | + } |
483 | 481 | // Check that server has no signs of lock loss |
484 | 482 | if ( $this->checkUptime( $server ) ) { |
485 | 483 | ++$yesVotes; // success for this peer |
— | — | @@ -490,11 +488,11 @@ |
491 | 489 | return true; // lock obtained |
492 | 490 | } |
493 | 491 | } |
| 492 | + } catch ( DBConnectionError $e ) { |
| 493 | + $this->cacheRecordFailure( $server ); |
494 | 494 | } catch ( DBError $e ) { |
495 | 495 | if ( $this->lastErrorIndicatesLocked( $server ) ) { |
496 | 496 | return 'cantacquire'; // vetoed; resource locked |
497 | | - } else { // can't connect? |
498 | | - $this->cacheRecordFailure( $server ); |
499 | 497 | } |
500 | 498 | } |
501 | 499 | } |
— | — | @@ -513,6 +511,46 @@ |
514 | 512 | } |
515 | 513 | |
516 | 514 | /** |
| 515 | + * Get a new connection to a lock DB |
| 516 | + * |
| 517 | + * @param $server string |
| 518 | + * @return Database |
| 519 | + * @throws DBError |
| 520 | + */ |
| 521 | + protected function getConnection( $server ) { |
| 522 | + if ( !isset( $this->activeConns[$server] ) ) { |
| 523 | + $this->activeConns[$server] = wfGetDB( DB_MASTER, array(), $server ); |
| 524 | + $this->activeConns[$server]->begin(); // start transaction |
| 525 | + # If the connection drops, try to avoid letting the DB rollback |
| 526 | + # and release the locks before the file operations are finished. |
| 527 | + # This won't handle the case of server reboots however. |
| 528 | + $options = array(); |
| 529 | + if ( php_sapi_name() == 'cli' ) { // maintenance scripts |
| 530 | + if ( $this->cliTimeout > 0 ) { |
| 531 | + $options['connTimeout'] = $this->cliTimeout; |
| 532 | + } |
| 533 | + } else { // web requests |
| 534 | + if ( $this->webTimeout > 0 ) { |
| 535 | + $options['connTimeout'] = $this->webTimeout; |
| 536 | + } |
| 537 | + } |
| 538 | + $this->activeConns[$server]->setSessionOptions( $options ); |
| 539 | + $this->initConnection( $server, $this->activeConns[$server] ); |
| 540 | + } |
| 541 | + return $this->activeConns[$server]; |
| 542 | + } |
| 543 | + |
| 544 | + /** |
| 545 | + * Do additional initialization for new lock DB connection |
| 546 | + * |
| 547 | + * @param $server string |
| 548 | + * @param $db Database |
| 549 | + * @return void |
| 550 | + * @throws DBError |
| 551 | + */ |
| 552 | + protected function initConnection( $server, DatabaseBase $db ) {} |
| 553 | + |
| 554 | + /** |
517 | 555 | * Commit all changes to lock-active databases. |
518 | 556 | * This should avoid throwing any exceptions. |
519 | 557 | * |
— | — | @@ -554,6 +592,7 @@ |
555 | 593 | * |
556 | 594 | * @param $server string |
557 | 595 | * @return bool |
| 596 | + * @throws DBError |
558 | 597 | */ |
559 | 598 | protected function checkUptime( $server ) { |
560 | 599 | if ( isset( $this->activeConns[$server] ) ) { // sanity |
— | — | @@ -679,12 +718,70 @@ |
680 | 719 | return intval( base_convert( $prefix, 16, 10 ) ) % count( $this->dbsByBucket ); |
681 | 720 | } |
682 | 721 | |
| 722 | + /** |
| 723 | + * Make sure remaining locks get cleared for sanity |
| 724 | + */ |
683 | 725 | function __destruct() { |
684 | | - // Make sure remaining locks get cleared for sanity |
685 | 726 | $this->finishLockTransactions(); |
686 | 727 | } |
687 | 728 | } |
688 | 729 | |
| 730 | +class MySqlLockManager extends DBLockManager { |
| 731 | + /** @var Array Mapping of lock types to the type actually used */ |
| 732 | + protected $lockTypeMap = array( |
| 733 | + self::LOCK_SH => self::LOCK_SH, |
| 734 | + self::LOCK_UW => self::LOCK_SH, |
| 735 | + self::LOCK_EX => self::LOCK_EX |
| 736 | + ); |
| 737 | + |
| 738 | + /** @var Array Map of (DB name => original transaction isolation) */ |
| 739 | + protected $trxIso = array(); |
| 740 | + |
| 741 | + protected function initConnection( $server, DatabaseBase $db ) { |
| 742 | + # Get the original transaction level for the server. |
| 743 | + $row = $db->query( "SELECT @@tx_isolation AS tx_iso;" )->fetchObject(); |
| 744 | + # Convert "REPEATABLE-READ" => "REPEATABLE READ" for SET query |
| 745 | + $this->trxIso[$server] = str_replace( '-', ' ', $row->tx_iso ); |
| 746 | + } |
| 747 | + |
| 748 | + protected function doLockingQuery( $server, array $keys, $type ) { |
| 749 | + $ok = true; |
| 750 | + # Actually do the locking queries... |
| 751 | + if ( $type == self::LOCK_SH ) { // reader locks |
| 752 | + $db = $this->getConnection( $server ); |
| 753 | + $data = array(); |
| 754 | + foreach ( $keys as $key ) { |
| 755 | + $data[] = array( 'fls_key' => $key ); |
| 756 | + } |
| 757 | + # Block new writers... |
| 758 | + $db->insert( 'file_locks_shared', $data, __METHOD__ ); |
| 759 | + # Wait on any existing writers... |
| 760 | + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED" ); |
| 761 | + $ok = !$db->selectField( 'file_locks_exclusive', '1', |
| 762 | + array( 'fle_key' => $keys ), |
| 763 | + __METHOD__ |
| 764 | + ); |
| 765 | + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL {$this->trxIso[$server]}" ); |
| 766 | + } elseif ( $type == self::LOCK_EX ) { // writer locks |
| 767 | + $db = $this->getConnection( $server ); |
| 768 | + $data = array(); |
| 769 | + foreach ( $keys as $key ) { |
| 770 | + $data[] = array( 'fle_key' => $key ); |
| 771 | + } |
| 772 | + # Block new readers/writers and wait on any existing writers |
| 773 | + $db->insert( 'file_locks_exclusive', $data, __METHOD__ ); |
| 774 | + # Wait on any existing readers... |
| 775 | + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED" ); |
| 776 | + $ok = !$db->selectField( 'file_locks_shared', '1', |
| 777 | + array( 'fls_key' => $keys ), |
| 778 | + __METHOD__ |
| 779 | + ); |
| 780 | + $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL {$this->trxIso[$server]}" ); |
| 781 | + } |
| 782 | + return $ok; |
| 783 | + } |
| 784 | +} |
| 785 | + |
689 | 786 | /** |
690 | 787 | * Simple version of LockManager that does nothing |
691 | 788 | */ |
Index: branches/FileBackend/phase3/includes/AutoLoader.php |
— | — | @@ -499,6 +499,7 @@ |
500 | 500 | 'LockManager' => 'includes/filerepo/backend/LockManager.php', |
501 | 501 | 'FSLockManager' => 'includes/filerepo/backend/LockManager.php', |
502 | 502 | 'DBLockManager' => 'includes/filerepo/backend/LockManager.php', |
| 503 | + 'MySqlLockManager'=> 'includes/filerepo/backend/LockManager.php', |
503 | 504 | 'NullLockManager' => 'includes/filerepo/backend/LockManager.php', |
504 | 505 | 'FileOp' => 'includes/filerepo/backend/FileOp.php', |
505 | 506 | 'StoreFileOp' => 'includes/filerepo/backend/FileOp.php', |