From: Aaron Schulz Date: Fri, 1 Feb 2013 06:28:55 +0000 (-0800) Subject: [LockManager] Split QuorumLockManager into its own file. X-Git-Tag: 1.31.0-rc.0~20833^2 X-Git-Url: http://git.cyclocoop.org/url?a=commitdiff_plain;h=7224c402ca9028d6162387bc3ff723fe7efeee21;p=lhc%2Fweb%2Fwiklou.git [LockManager] Split QuorumLockManager into its own file. Change-Id: Ib487a39b04ac9de3449695ac388c95ae45933469 --- diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 8096ed6dc1..8a886e9fe4 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -573,7 +573,7 @@ $wgAutoloadLocalClasses = array( 'DBLockManager' => 'includes/filebackend/lockmanager/DBLockManager.php', 'LSLockManager' => 'includes/filebackend/lockmanager/LSLockManager.php', 'MemcLockManager' => 'includes/filebackend/lockmanager/MemcLockManager.php', - 'QuorumLockManager' => 'includes/filebackend/lockmanager/LockManager.php', + 'QuorumLockManager' => 'includes/filebackend/lockmanager/QuorumLockManager.php', 'MySqlLockManager'=> 'includes/filebackend/lockmanager/DBLockManager.php', 'NullLockManager' => 'includes/filebackend/lockmanager/LockManager.php', 'FileOp' => 'includes/filebackend/FileOp.php', diff --git a/includes/filebackend/lockmanager/LockManager.php b/includes/filebackend/lockmanager/LockManager.php index fd6f0f853a..119006a385 100644 --- a/includes/filebackend/lockmanager/LockManager.php +++ b/includes/filebackend/lockmanager/LockManager.php @@ -131,216 +131,6 @@ abstract class LockManager { abstract protected function doUnlock( array $paths, $type ); } -/** - * 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 specific locks if possible, or at least release - // all locks once this process is currently not holding any locks. - foreach ( $pathsToUnlock as $bucket => $paths ) { - $status->merge( $this->doUnlockingRequestBucket( $bucket, $paths, $type ) ); - } - 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. - * - * Subclasses must effectively implement this or releaseAllLocks(). - * - * @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. - * - * Subclasses must effectively implement this or freeLocksOnServer(). - * - * @return Status - */ - abstract protected function releaseAllLocks(); -} - /** * Simple version of LockManager that does nothing * @since 1.19 diff --git a/includes/filebackend/lockmanager/QuorumLockManager.php b/includes/filebackend/lockmanager/QuorumLockManager.php new file mode 100644 index 0000000000..4198764e2d --- /dev/null +++ b/includes/filebackend/lockmanager/QuorumLockManager.php @@ -0,0 +1,232 @@ + (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 specific locks if possible, or at least release + // all locks once this process is currently not holding any locks. + foreach ( $pathsToUnlock as $bucket => $paths ) { + $status->merge( $this->doUnlockingRequestBucket( $bucket, $paths, $type ) ); + } + 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. + * + * Subclasses must effectively implement this or releaseAllLocks(). + * + * @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. + * + * Subclasses must effectively implement this or freeLocksOnServer(). + * + * @return Status + */ + abstract protected function releaseAllLocks(); +}