MediaWiki  REL1_24
QuorumLockManager.php
Go to the documentation of this file.
00001 <?php
00031 abstract class QuorumLockManager extends LockManager {
00033     protected $srvsByBucket = array(); // (bucket index => (lsrv1, lsrv2, ...))
00034 
00036     protected $degradedBuckets = array(); // (buckey index => UNIX timestamp)
00037 
00038     final protected function doLock( array $paths, $type ) {
00039         return $this->doLockByType( array( $type => $paths ) );
00040     }
00041 
00042     final protected function doUnlock( array $paths, $type ) {
00043         return $this->doUnlockByType( array( $type => $paths ) );
00044     }
00045 
00046     protected function doLockByType( array $pathsByType ) {
00047         $status = Status::newGood();
00048 
00049         $pathsToLock = array(); // (bucket => type => paths)
00050         // Get locks that need to be acquired (buckets => locks)...
00051         foreach ( $pathsByType as $type => $paths ) {
00052             foreach ( $paths as $path ) {
00053                 if ( isset( $this->locksHeld[$path][$type] ) ) {
00054                     ++$this->locksHeld[$path][$type];
00055                 } else {
00056                     $bucket = $this->getBucketFromPath( $path );
00057                     $pathsToLock[$bucket][$type][] = $path;
00058                 }
00059             }
00060         }
00061 
00062         $lockedPaths = array(); // files locked in this attempt (type => paths)
00063         // Attempt to acquire these locks...
00064         foreach ( $pathsToLock as $bucket => $pathsToLockByType ) {
00065             // Try to acquire the locks for this bucket
00066             $status->merge( $this->doLockingRequestBucket( $bucket, $pathsToLockByType ) );
00067             if ( !$status->isOK() ) {
00068                 $status->merge( $this->doUnlockByType( $lockedPaths ) );
00069 
00070                 return $status;
00071             }
00072             // Record these locks as active
00073             foreach ( $pathsToLockByType as $type => $paths ) {
00074                 foreach ( $paths as $path ) {
00075                     $this->locksHeld[$path][$type] = 1; // locked
00076                     // Keep track of what locks were made in this attempt
00077                     $lockedPaths[$type][] = $path;
00078                 }
00079             }
00080         }
00081 
00082         return $status;
00083     }
00084 
00085     protected function doUnlockByType( array $pathsByType ) {
00086         $status = Status::newGood();
00087 
00088         $pathsToUnlock = array(); // (bucket => type => paths)
00089         foreach ( $pathsByType as $type => $paths ) {
00090             foreach ( $paths as $path ) {
00091                 if ( !isset( $this->locksHeld[$path][$type] ) ) {
00092                     $status->warning( 'lockmanager-notlocked', $path );
00093                 } else {
00094                     --$this->locksHeld[$path][$type];
00095                     // Reference count the locks held and release locks when zero
00096                     if ( $this->locksHeld[$path][$type] <= 0 ) {
00097                         unset( $this->locksHeld[$path][$type] );
00098                         $bucket = $this->getBucketFromPath( $path );
00099                         $pathsToUnlock[$bucket][$type][] = $path;
00100                     }
00101                     if ( !count( $this->locksHeld[$path] ) ) {
00102                         unset( $this->locksHeld[$path] ); // no SH or EX locks left for key
00103                     }
00104                 }
00105             }
00106         }
00107 
00108         // Remove these specific locks if possible, or at least release
00109         // all locks once this process is currently not holding any locks.
00110         foreach ( $pathsToUnlock as $bucket => $pathsToUnlockByType ) {
00111             $status->merge( $this->doUnlockingRequestBucket( $bucket, $pathsToUnlockByType ) );
00112         }
00113         if ( !count( $this->locksHeld ) ) {
00114             $status->merge( $this->releaseAllLocks() );
00115             $this->degradedBuckets = array(); // safe to retry the normal quorum
00116         }
00117 
00118         return $status;
00119     }
00120 
00129     final protected function doLockingRequestBucket( $bucket, array $pathsByType ) {
00130         $status = Status::newGood();
00131 
00132         $yesVotes = 0; // locks made on trustable servers
00133         $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
00134         $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
00135         // Get votes for each peer, in order, until we have enough...
00136         foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
00137             if ( !$this->isServerUp( $lockSrv ) ) {
00138                 --$votesLeft;
00139                 $status->warning( 'lockmanager-fail-svr-acquire', $lockSrv );
00140                 $this->degradedBuckets[$bucket] = time();
00141                 continue; // server down?
00142             }
00143             // Attempt to acquire the lock on this peer
00144             $status->merge( $this->getLocksOnServer( $lockSrv, $pathsByType ) );
00145             if ( !$status->isOK() ) {
00146                 return $status; // vetoed; resource locked
00147             }
00148             ++$yesVotes; // success for this peer
00149             if ( $yesVotes >= $quorum ) {
00150                 return $status; // lock obtained
00151             }
00152             --$votesLeft;
00153             $votesNeeded = $quorum - $yesVotes;
00154             if ( $votesNeeded > $votesLeft ) {
00155                 break; // short-circuit
00156             }
00157         }
00158         // At this point, we must not have met the quorum
00159         $status->setResult( false );
00160 
00161         return $status;
00162     }
00163 
00171     final protected function doUnlockingRequestBucket( $bucket, array $pathsByType ) {
00172         $status = Status::newGood();
00173 
00174         $yesVotes = 0; // locks freed on trustable servers
00175         $votesLeft = count( $this->srvsByBucket[$bucket] ); // remaining peers
00176         $quorum = floor( $votesLeft / 2 + 1 ); // simple majority
00177         $isDegraded = isset( $this->degradedBuckets[$bucket] ); // not the normal quorum?
00178         foreach ( $this->srvsByBucket[$bucket] as $lockSrv ) {
00179             if ( !$this->isServerUp( $lockSrv ) ) {
00180                 $status->warning( 'lockmanager-fail-svr-release', $lockSrv );
00181             } else {
00182                 // Attempt to release the lock on this peer
00183                 $status->merge( $this->freeLocksOnServer( $lockSrv, $pathsByType ) );
00184                 ++$yesVotes; // success for this peer
00185                 // Normally the first peers form the quorum, and the others are ignored.
00186                 // Ignore them in this case, but not when an alternative quorum was used.
00187                 if ( $yesVotes >= $quorum && !$isDegraded ) {
00188                     break; // lock released
00189                 }
00190             }
00191         }
00192         // Set a bad status if the quorum was not met.
00193         // Assumes the same "up" servers as during the acquire step.
00194         $status->setResult( $yesVotes >= $quorum );
00195 
00196         return $status;
00197     }
00198 
00206     protected function getBucketFromPath( $path ) {
00207         $prefix = substr( sha1( $path ), 0, 2 ); // first 2 hex chars (8 bits)
00208         return (int)base_convert( $prefix, 16, 10 ) % count( $this->srvsByBucket );
00209     }
00210 
00218     abstract protected function isServerUp( $lockSrv );
00219 
00227     abstract protected function getLocksOnServer( $lockSrv, array $pathsByType );
00228 
00238     abstract protected function freeLocksOnServer( $lockSrv, array $pathsByType );
00239 
00247     abstract protected function releaseAllLocks();
00248 }