[LockManager] Factored QuorumLockManager class out of LSLockManager.
authorAaron <aschulz@wikimedia.org>
Tue, 19 Jun 2012 18:39:53 +0000 (11:39 -0700)
committerAaron <aschulz@wikimedia.org>
Tue, 19 Jun 2012 19:52:47 +0000 (12:52 -0700)
Change-Id: I4031085faef4a1a7ce49dbeeb0b3ddf94d41132c

includes/AutoLoader.php
includes/filerepo/backend/lockmanager/LSLockManager.php
includes/filerepo/backend/lockmanager/LockManager.php
languages/messages/MessagesEn.php
maintenance/language/messages.inc

index ad7ccd1..480f3c7 100644 (file)
@@ -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',
index 7910285..024e11b 100644 (file)
@@ -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 );
                }
index e41c777..0fd3fb6 100644 (file)
@@ -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
index b7275f8..86954ba 100644 (file)
@@ -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
index fbdef20..30c3d9a 100644 (file)
@@ -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',