From 75b0c3230c03ea071e2907d43069d7a838256012 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 19 Jun 2012 11:39:53 -0700 Subject: [PATCH] [LockManager] Factored QuorumLockManager class out of LSLockManager. Change-Id: I4031085faef4a1a7ce49dbeeb0b3ddf94d41132c --- includes/AutoLoader.php | 1 + .../backend/lockmanager/LSLockManager.php | 194 ++++----------- .../backend/lockmanager/LockManager.php | 225 +++++++++++++++++- languages/messages/MessagesEn.php | 1 + maintenance/language/messages.inc | 1 + 5 files changed, 260 insertions(+), 162 deletions(-) diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index ad7ccd1c6a..480f3c7359 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -572,6 +572,7 @@ $wgAutoloadLocalClasses = array( 'FSLockManager' => 'includes/filerepo/backend/lockmanager/FSLockManager.php', 'DBLockManager' => 'includes/filerepo/backend/lockmanager/DBLockManager.php', 'LSLockManager' => 'includes/filerepo/backend/lockmanager/LSLockManager.php', + 'QuorumLockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php', 'MySqlLockManager'=> 'includes/filerepo/backend/lockmanager/DBLockManager.php', 'NullLockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php', 'FileOp' => 'includes/filerepo/backend/FileOp.php', diff --git a/includes/filerepo/backend/lockmanager/LSLockManager.php b/includes/filerepo/backend/lockmanager/LSLockManager.php index 79102851c4..024e11b717 100644 --- a/includes/filerepo/backend/lockmanager/LSLockManager.php +++ b/includes/filerepo/backend/lockmanager/LSLockManager.php @@ -36,7 +36,7 @@ * @ingroup LockManager * @since 1.19 */ -class LSLockManager extends LockManager { +class LSLockManager extends QuorumLockManager { /** @var Array Mapping of lock types to the type actually used */ protected $lockTypeMap = array( self::LOCK_SH => self::LOCK_SH, @@ -46,8 +46,6 @@ class LSLockManager extends LockManager { /** @var Array Map of server names to server config */ protected $lockServers; // (server name => server config array) - /** @var Array Map of bucket indexes to peer server lists */ - protected $srvsByBucket; // (bucket index => (lsrv1, lsrv2, ...)) /** @var Array Map Server connections (server name => resource) */ protected $conns = array(); @@ -57,7 +55,7 @@ class LSLockManager extends LockManager { /** * Construct a new instance from configuration. - * + * * $config paramaters include: * 'lockServers' : Associative array of server names to configuration. * Configuration is an associative array that includes: @@ -68,7 +66,7 @@ class LSLockManager extends LockManager { * each having an odd-numbered list of server names (peers) as values. * 'connTimeout' : Lock server connection attempt timeout. [optional] * - * @param Array $config + * @param Array $config */ public function __construct( array $config ) { parent::__construct( $config ); @@ -84,123 +82,74 @@ class LSLockManager extends LockManager { $this->connTimeout = 3; // use some sane amount } - $this->session = ''; - for ( $i = 0; $i < 5; $i++ ) { - $this->session .= mt_rand( 0, 2147483647 ); - } - $this->session = wfBaseConvert( sha1( $this->session ), 16, 36, 31 ); + $this->session = wfRandomString( 31 ); } /** - * @see LockManager::doLock() - * @param $paths array - * @param $type int + * @see QuorumLockManager::getLocksOnServer() * @return Status */ - protected function doLock( array $paths, $type ) { + protected function getLocksOnServer( $lockSrv, array $paths, $type ) { $status = Status::newGood(); - $pathsToLock = array(); - // Get locks that need to be acquired (buckets => locks)... - foreach ( $paths as $path ) { - if ( isset( $this->locksHeld[$path][$type] ) ) { - ++$this->locksHeld[$path][$type]; - } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { - $this->locksHeld[$path][$type] = 1; - } else { - $bucket = $this->getBucketFromKey( $path ); - $pathsToLock[$bucket][] = $path; - } - } + // Send out the command and get the response... + $type = ( $type == self::LOCK_SH ) ? 'SH' : 'EX'; + $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) ); + $response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys ); - $lockedPaths = array(); // files locked in this attempt - // Attempt to acquire these locks... - foreach ( $pathsToLock as $bucket => $paths ) { - // Try to acquire the locks for this bucket - $res = $this->doLockingRequestAll( $bucket, $paths, $type ); - if ( $res === 'cantacquire' ) { - // Resources already locked by another process. - // Abort and unlock everything we just locked. - foreach ( $paths as $path ) { - $status->fatal( 'lockmanager-fail-acquirelock', $path ); - } - $status->merge( $this->doUnlock( $lockedPaths, $type ) ); - return $status; - } elseif ( $res !== true ) { - // Couldn't contact any servers for this bucket. - // Abort and unlock everything we just locked. - foreach ( $paths as $path ) { - $status->fatal( 'lockmanager-fail-acquirelock', $path ); - } - $status->merge( $this->doUnlock( $lockedPaths, $type ) ); - return $status; - } - // Record these locks as active + if ( $response !== 'ACQUIRED' ) { foreach ( $paths as $path ) { - $this->locksHeld[$path][$type] = 1; // locked + $status->fatal( 'lockmanager-fail-acquirelock', $path ); } - // Keep track of what locks were made in this attempt - $lockedPaths = array_merge( $lockedPaths, $paths ); } return $status; } /** - * @see LockManager::doUnlock() - * @param $paths array - * @param $type int + * @see QuorumLockManager::freeLocksOnServer() * @return Status */ - protected function doUnlock( array $paths, $type ) { + protected function freeLocksOnServer( $lockSrv, array $paths, $type ) { $status = Status::newGood(); - foreach ( $paths as $path ) { - if ( !isset( $this->locksHeld[$path] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } elseif ( !isset( $this->locksHeld[$path][$type] ) ) { - $status->warning( 'lockmanager-notlocked', $path ); - } else { - --$this->locksHeld[$path][$type]; - if ( $this->locksHeld[$path][$type] <= 0 ) { - unset( $this->locksHeld[$path][$type] ); - } - if ( !count( $this->locksHeld[$path] ) ) { - unset( $this->locksHeld[$path] ); // no SH or EX locks left for key - } - } - } + // Send out the command and get the response... + $type = ( $type == self::LOCK_SH ) ? 'SH' : 'EX'; + $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) ); + $response = $this->sendCommand( $lockSrv, 'RELEASE', $type, $keys ); - // Reference count the locks held and release locks when zero - if ( !count( $this->locksHeld ) ) { - $status->merge( $this->releaseLocks() ); + if ( $response !== 'RELEASED' ) { + foreach ( $paths as $path ) { + $status->fatal( 'lockmanager-fail-releaselock', $path ); + } } return $status; } /** - * Get a connection to a lock server and acquire locks on $paths - * - * @param $lockSrv string - * @param $paths Array - * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH - * @return bool Resources able to be locked + * @see QuorumLockManager::releaseAllLocks() + * @return Status */ - protected function doLockingRequest( $lockSrv, array $paths, $type ) { - if ( $type == self::LOCK_SH ) { // reader locks - $type = 'SH'; - } elseif ( $type == self::LOCK_EX ) { // writer locks - $type = 'EX'; - } else { - return true; // ok... + protected function releaseAllLocks() { + $status = Status::newGood(); + + foreach ( $this->conns as $lockSrv => $conn ) { + $response = $this->sendCommand( $lockSrv, 'RELEASE_ALL', '', array() ); + if ( $response !== 'RELEASED_ALL' ) { + $status->fatal( 'lockmanager-fail-svr-release', $lockSrv ); + } } - // Send out the command and get the response... - $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) ); - $response = $this->sendCommand( $lockSrv, 'ACQUIRE', $type, $keys ); + return $status; + } - return ( $response === 'ACQUIRED' ); + /** + * @see QuorumLockManager::isServerUp() + * @return bool + */ + protected function isServerUp( $lockSrv ) { + return (bool)$this->getConnection( $lockSrv ); } /** @@ -233,39 +182,6 @@ class LSLockManager extends LockManager { return trim( $response ); } - /** - * Attempt to acquire locks with the peers for a bucket - * - * @param $bucket integer - * @param $paths Array List of resource keys to lock - * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH - * @return bool|string One of (true, 'cantacquire', 'srverrors') - */ - protected function doLockingRequestAll( $bucket, array $paths, $type ) { - $yesVotes = 0; // locks made on trustable servers - $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers - $quorum = floor( $votesLeft/2 + 1 ); // simple majority - // Get votes for each peer, in order, until we have enough... - foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { - // Attempt to acquire the lock on this peer - if ( !$this->doLockingRequest( $lockSrv, $paths, $type ) ) { - return 'cantacquire'; // vetoed; resource locked - } - ++$yesVotes; // success for this peer - if ( $yesVotes >= $quorum ) { - return true; // lock obtained - } - --$votesLeft; - $votesNeeded = $quorum - $yesVotes; - if ( $votesNeeded > $votesLeft ) { - // In "trust cache" mode we don't have to meet the quorum - break; // short-circuit - } - } - // At this point, we must not have meet the quorum - return 'srverrors'; // not enough votes to ensure correctness - } - /** * Get (or reuse) a connection to a lock server * @@ -290,39 +206,11 @@ class LSLockManager extends LockManager { return $this->conns[$lockSrv]; } - /** - * Release all locks that this session is holding - * - * @return Status - */ - protected function releaseLocks() { - $status = Status::newGood(); - foreach ( $this->conns as $lockSrv => $conn ) { - $response = $this->sendCommand( $lockSrv, 'RELEASE_ALL', '', array() ); - if ( $response !== 'RELEASED_ALL' ) { - $status->fatal( 'lockmanager-fail-svr-release', $lockSrv ); - } - } - return $status; - } - - /** - * Get the bucket for resource path. - * This should avoid throwing any exceptions. - * - * @param $path string - * @return integer - */ - protected function getBucketFromKey( $path ) { - $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits) - return intval( base_convert( $prefix, 16, 10 ) ) % count( $this->srvsByBucket ); - } - /** * Make sure remaining locks get cleared for sanity */ function __destruct() { - $this->releaseLocks(); + $this->releaseAllLocks(); foreach ( $this->conns as $conn ) { fclose( $conn ); } diff --git a/includes/filerepo/backend/lockmanager/LockManager.php b/includes/filerepo/backend/lockmanager/LockManager.php index e41c7770a7..0fd3fb6622 100644 --- a/includes/filerepo/backend/lockmanager/LockManager.php +++ b/includes/filerepo/backend/lockmanager/LockManager.php @@ -67,10 +67,10 @@ abstract class LockManager { /** * Lock the resources at the given abstract paths - * + * * @param $paths Array List of resource names * @param $type integer LockManager::LOCK_* constant - * @return Status + * @return Status */ final public function lock( array $paths, $type = self::LOCK_EX ) { wfProfileIn( __METHOD__ ); @@ -81,10 +81,10 @@ abstract class LockManager { /** * Unlock the resources at the given abstract paths - * + * * @param $paths Array List of storage paths * @param $type integer LockManager::LOCK_* constant - * @return Status + * @return Status */ final public function unlock( array $paths, $type = self::LOCK_EX ) { wfProfileIn( __METHOD__ ); @@ -95,7 +95,7 @@ abstract class LockManager { /** * Get the base 36 SHA-1 of a string, padded to 31 digits - * + * * @param $path string * @return string */ @@ -105,7 +105,7 @@ abstract class LockManager { /** * Lock resources with the given keys and lock type - * + * * @param $paths Array List of storage paths * @param $type integer LockManager::LOCK_* constant * @return string @@ -114,7 +114,7 @@ abstract class LockManager { /** * Unlock resources with the given keys and lock type - * + * * @param $paths Array List of storage paths * @param $type integer LockManager::LOCK_* constant * @return string @@ -123,7 +123,7 @@ abstract class LockManager { } /** - * Self releasing locks + * Self-releasing locks * * LockManager helper class to handle scoped locks, which * release when an object is destroyed or goes out of scope. @@ -160,7 +160,7 @@ class ScopedLock { * Get a ScopedLock object representing a lock on resource paths. * Any locks are released once this object goes out of scope. * The status object is updated with any errors or warnings. - * + * * @param $manager LockManager * @param $paths Array List of storage paths * @param $type integer LockManager::LOCK_* constant @@ -188,6 +188,213 @@ class ScopedLock { } } +/** + * Version of LockManager that uses a quorum from peer servers for locks. + * The resource space can also be sharded into separate peer groups. + * + * @ingroup LockManager + * @since 1.20 + */ +abstract class QuorumLockManager extends LockManager { + /** @var Array Map of bucket indexes to peer server lists */ + protected $srvsByBucket = array(); // (bucket index => (lsrv1, lsrv2, ...)) + + /** + * @see LockManager::doLock() + * @param $paths array + * @param $type int + * @return Status + */ + final protected function doLock( array $paths, $type ) { + $status = Status::newGood(); + + $pathsToLock = array(); // (bucket => paths) + // Get locks that need to be acquired (buckets => locks)... + foreach ( $paths as $path ) { + if ( isset( $this->locksHeld[$path][$type] ) ) { + ++$this->locksHeld[$path][$type]; + } elseif ( isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { + $this->locksHeld[$path][$type] = 1; + } else { + $bucket = $this->getBucketFromKey( $path ); + $pathsToLock[$bucket][] = $path; + } + } + + $lockedPaths = array(); // files locked in this attempt + // Attempt to acquire these locks... + foreach ( $pathsToLock as $bucket => $paths ) { + // Try to acquire the locks for this bucket + $status->merge( $this->doLockingRequestBucket( $bucket, $paths, $type ) ); + if ( !$status->isOK() ) { + $status->merge( $this->doUnlock( $lockedPaths, $type ) ); + return $status; + } + // Record these locks as active + foreach ( $paths as $path ) { + $this->locksHeld[$path][$type] = 1; // locked + } + // Keep track of what locks were made in this attempt + $lockedPaths = array_merge( $lockedPaths, $paths ); + } + + return $status; + } + + /** + * @see LockManager::doUnlock() + * @param $paths array + * @param $type int + * @return Status + */ + final protected function doUnlock( array $paths, $type ) { + $status = Status::newGood(); + + $pathsToUnlock = array(); + foreach ( $paths as $path ) { + if ( !isset( $this->locksHeld[$path][$type] ) ) { + $status->warning( 'lockmanager-notlocked', $path ); + } else { + --$this->locksHeld[$path][$type]; + // Reference count the locks held and release locks when zero + if ( $this->locksHeld[$path][$type] <= 0 ) { + unset( $this->locksHeld[$path][$type] ); + $bucket = $this->getBucketFromKey( $path ); + $pathsToUnlock[$bucket][] = $path; + } + if ( !count( $this->locksHeld[$path] ) ) { + unset( $this->locksHeld[$path] ); // no SH or EX locks left for key + } + } + } + + // Remove these single locks if the medium supports it + foreach ( $pathsToUnlock as $bucket => $paths ) { + $status->merge( $this->doUnlockingRequestBucket( $bucket, $paths, $type ) ); + } + + // Reference count the locks held and release locks when zero + if ( !count( $this->locksHeld ) ) { + $status->merge( $this->releaseAllLocks() ); + } + + return $status; + } + + /** + * Attempt to acquire locks with the peers for a bucket. + * This is all or nothing; if any key is locked then this totally fails. + * + * @param $bucket integer + * @param $paths Array List of resource keys to lock + * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH + * @return Status + */ + final protected function doLockingRequestBucket( $bucket, array $paths, $type ) { + $status = Status::newGood(); + + $yesVotes = 0; // locks made on trustable servers + $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers + $quorum = floor( $votesLeft/2 + 1 ); // simple majority + // Get votes for each peer, in order, until we have enough... + foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { + if ( !$this->isServerUp( $lockSrv ) ) { + --$votesLeft; + $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv ); + continue; // server down? + } + // Attempt to acquire the lock on this peer + $status->merge( $this->getLocksOnServer( $lockSrv, $paths, $type ) ); + if ( !$status->isOK() ) { + return $status; // vetoed; resource locked + } + ++$yesVotes; // success for this peer + if ( $yesVotes >= $quorum ) { + return $status; // lock obtained + } + --$votesLeft; + $votesNeeded = $quorum - $yesVotes; + if ( $votesNeeded > $votesLeft ) { + break; // short-circuit + } + } + // At this point, we must not have met the quorum + $status->setResult( false ); + + return $status; + } + + /** + * Attempt to release locks with the peers for a bucket + * + * @param $bucket integer + * @param $paths Array List of resource keys to lock + * @param $type integer LockManager::LOCK_EX or LockManager::LOCK_SH + * @return Status + */ + final protected function doUnlockingRequestBucket( $bucket, array $paths, $type ) { + $status = Status::newGood(); + + foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) { + if ( !$this->isServerUp( $lockSrv ) ) { + $status->fatal( 'lockmanager-fail-svr-release', $lockSrv ); + // Attempt to release the lock on this peer + } else { + $status->merge( $this->freeLocksOnServer( $lockSrv, $paths, $type ) ); + } + } + + return $status; + } + + /** + * Get the bucket for resource path. + * This should avoid throwing any exceptions. + * + * @param $path string + * @return integer + */ + protected function getBucketFromKey( $path ) { + $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits) + return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket ); + } + + /** + * Check if a lock server is up + * + * @param $lockSrv string + * @return bool + */ + abstract protected function isServerUp( $lockSrv ); + + /** + * Get a connection to a lock server and acquire locks on $paths + * + * @param $lockSrv string + * @param $paths array + * @param $type integer + * @return Status + */ + abstract protected function getLocksOnServer( $lockSrv, array $paths, $type ); + + /** + * Get a connection to a lock server and release locks on $paths + * + * @param $lockSrv string + * @param $paths array + * @param $type integer + * @return Status + */ + abstract protected function freeLocksOnServer( $lockSrv, array $paths, $type ); + + /** + * Release all locks that this session is holding + * + * @return Status + */ + abstract protected function releaseAllLocks(); +} + /** * Simple version of LockManager that does nothing * @since 1.19 diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index b7275f89a2..86954ba4d7 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -2310,6 +2310,7 @@ If the problem persists, contact an [[Special:ListUsers/sysop|administrator]].', 'lockmanager-fail-releaselock' => 'Could not release lock for "$1".', 'lockmanager-fail-db-bucket' => 'Could not contact enough lock databases in bucket $1.', 'lockmanager-fail-db-release' => 'Could not release locks on database $1.', +'lockmanager-fail-svr-acquire' => 'Could not acquire locks on server $1.', 'lockmanager-fail-svr-release' => 'Could not release locks on server $1.', # ZipDirectoryReader diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc index fbdef20a69..30c3d9ab31 100644 --- a/maintenance/language/messages.inc +++ b/maintenance/language/messages.inc @@ -1407,6 +1407,7 @@ $wgMessageStructure = array( 'lockmanager-fail-deletelock', 'lockmanager-fail-acquirelock', 'lockmanager-fail-openlock', + 'lockmanager-fail-acquirelock', 'lockmanager-fail-releaselock', 'lockmanager-fail-db-bucket', 'lockmanager-fail-db-release', -- 2.20.1