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