MediaWiki  REL1_20
DBLockManager.php
Go to the documentation of this file.
00001 <?php
00040 class DBLockManager extends QuorumLockManager {
00042         protected $dbServers; // (DB name => server config array)
00044         protected $statusCache;
00045 
00046         protected $lockExpiry; // integer number of seconds
00047         protected $safeDelay; // integer number of seconds
00048 
00049         protected $session = 0; // random integer
00051         protected $conns = array();
00052 
00076         public function __construct( array $config ) {
00077                 parent::__construct( $config );
00078 
00079                 $this->dbServers = isset( $config['dbServers'] )
00080                         ? $config['dbServers']
00081                         : array(); // likely just using 'localDBMaster'
00082                 // Sanitize srvsByBucket config to prevent PHP errors
00083                 $this->srvsByBucket = array_filter( $config['dbsByBucket'], 'is_array' );
00084                 $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
00085 
00086                 if ( isset( $config['lockExpiry'] ) ) {
00087                         $this->lockExpiry = $config['lockExpiry'];
00088                 } else {
00089                         $met = ini_get( 'max_execution_time' );
00090                         $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0
00091                 }
00092                 $this->safeDelay = ( $this->lockExpiry <= 0 )
00093                         ? 60 // pick a safe-ish number to match DB timeout default
00094                         : $this->lockExpiry; // cover worst case
00095 
00096                 foreach ( $this->srvsByBucket as $bucket ) {
00097                         if ( count( $bucket ) > 1 ) { // multiple peers
00098                                 // Tracks peers that couldn't be queried recently to avoid lengthy
00099                                 // connection timeouts. This is useless if each bucket has one peer.
00100                                 try {
00101                                         $this->statusCache = ObjectCache::newAccelerator( array() );
00102                                 } catch ( MWException $e ) {
00103                                         trigger_error( __CLASS__ .
00104                                                 " using multiple DB peers without apc, xcache, or wincache." );
00105                                 }
00106                                 break;
00107                         }
00108                 }
00109 
00110                 $this->session = wfRandomString( 31 );
00111         }
00112 
00120         protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
00121                 $status = Status::newGood();
00122 
00123                 if ( $type == self::LOCK_EX ) { // writer locks
00124                         try {
00125                                 $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
00126                                 # Build up values for INSERT clause
00127                                 $data = array();
00128                                 foreach ( $keys as $key ) {
00129                                         $data[] = array( 'fle_key' => $key );
00130                                 }
00131                                 # Wait on any existing writers and block new ones if we get in
00132                                 $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
00133                                 $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
00134                         } catch ( DBError $e ) {
00135                                 foreach ( $paths as $path ) {
00136                                         $status->fatal( 'lockmanager-fail-acquirelock', $path );
00137                                 }
00138                         }
00139                 }
00140 
00141                 return $status;
00142         }
00143 
00148         protected function freeLocksOnServer( $lockSrv, array $paths, $type ) {
00149                 return Status::newGood(); // not supported
00150         }
00151 
00156         protected function releaseAllLocks() {
00157                 $status = Status::newGood();
00158 
00159                 foreach ( $this->conns as $lockDb => $db ) {
00160                         if ( $db->trxLevel() ) { // in transaction
00161                                 try {
00162                                         $db->rollback( __METHOD__ ); // finish transaction and kill any rows
00163                                 } catch ( DBError $e ) {
00164                                         $status->fatal( 'lockmanager-fail-db-release', $lockDb );
00165                                 }
00166                         }
00167                 }
00168 
00169                 return $status;
00170         }
00171 
00176         protected function isServerUp( $lockSrv ) {
00177                 if ( !$this->cacheCheckFailures( $lockSrv ) ) {
00178                         return false; // recent failure to connect
00179                 }
00180                 try {
00181                         $this->getConnection( $lockSrv );
00182                 } catch ( DBError $e ) {
00183                         $this->cacheRecordFailure( $lockSrv );
00184                         return false; // failed to connect
00185                 }
00186                 return true;
00187         }
00188 
00196         protected function getConnection( $lockDb ) {
00197                 if ( !isset( $this->conns[$lockDb] ) ) {
00198                         $db = null;
00199                         if ( $lockDb === 'localDBMaster' ) {
00200                                 $lb = wfGetLBFactory()->newMainLB();
00201                                 $db = $lb->getConnection( DB_MASTER );
00202                         } elseif ( isset( $this->dbServers[$lockDb] ) ) {
00203                                 $config = $this->dbServers[$lockDb];
00204                                 $db = DatabaseBase::factory( $config['type'], $config );
00205                         }
00206                         if ( !$db ) {
00207                                 return null; // config error?
00208                         }
00209                         $this->conns[$lockDb] = $db;
00210                         $this->conns[$lockDb]->clearFlag( DBO_TRX );
00211                         # If the connection drops, try to avoid letting the DB rollback
00212                         # and release the locks before the file operations are finished.
00213                         # This won't handle the case of DB server restarts however.
00214                         $options = array();
00215                         if ( $this->lockExpiry > 0 ) {
00216                                 $options['connTimeout'] = $this->lockExpiry;
00217                         }
00218                         $this->conns[$lockDb]->setSessionOptions( $options );
00219                         $this->initConnection( $lockDb, $this->conns[$lockDb] );
00220                 }
00221                 if ( !$this->conns[$lockDb]->trxLevel() ) {
00222                         $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction
00223                 }
00224                 return $this->conns[$lockDb];
00225         }
00226 
00235         protected function initConnection( $lockDb, DatabaseBase $db ) {}
00236 
00244         protected function cacheCheckFailures( $lockDb ) {
00245                 return ( $this->statusCache && $this->safeDelay > 0 )
00246                         ? !$this->statusCache->get( $this->getMissKey( $lockDb ) )
00247                         : true;
00248         }
00249 
00256         protected function cacheRecordFailure( $lockDb ) {
00257                 return ( $this->statusCache && $this->safeDelay > 0 )
00258                         ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay )
00259                         : true;
00260         }
00261 
00268         protected function getMissKey( $lockDb ) {
00269                 $lockDb = ( $lockDb === 'localDBMaster' ) ? wfWikiID() : $lockDb; // non-relative
00270                 return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb );
00271         }
00272 
00276         function __destruct() {
00277                 foreach ( $this->conns as $db ) {
00278                         if ( $db->trxLevel() ) { // in transaction
00279                                 try {
00280                                         $db->rollback( __METHOD__ ); // finish transaction and kill any rows
00281                                 } catch ( DBError $e ) {
00282                                         // oh well
00283                                 }
00284                         }
00285                         $db->close();
00286                 }
00287         }
00288 }
00289 
00296 class MySqlLockManager extends DBLockManager {
00298         protected $lockTypeMap = array(
00299                 self::LOCK_SH => self::LOCK_SH,
00300                 self::LOCK_UW => self::LOCK_SH,
00301                 self::LOCK_EX => self::LOCK_EX
00302         );
00303 
00308         protected function initConnection( $lockDb, DatabaseBase $db ) {
00309                 # Let this transaction see lock rows from other transactions
00310                 $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
00311         }
00312 
00320         protected function getLocksOnServer( $lockSrv, array $paths, $type ) {
00321                 $status = Status::newGood();
00322 
00323                 $db = $this->getConnection( $lockSrv ); // checked in isServerUp()
00324                 $keys = array_unique( array_map( 'LockManager::sha1Base36', $paths ) );
00325                 # Build up values for INSERT clause
00326                 $data = array();
00327                 foreach ( $keys as $key ) {
00328                         $data[] = array( 'fls_key' => $key, 'fls_session' => $this->session );
00329                 }
00330                 # Block new writers...
00331                 $db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) );
00332                 # Actually do the locking queries...
00333                 if ( $type == self::LOCK_SH ) { // reader locks
00334                         # Bail if there are any existing writers...
00335                         $blocked = $db->selectField( 'filelocks_exclusive', '1',
00336                                 array( 'fle_key' => $keys ),
00337                                 __METHOD__
00338                         );
00339                         # Prospective writers that haven't yet updated filelocks_exclusive
00340                         # will recheck filelocks_shared after doing so and bail due to our entry.
00341                 } else { // writer locks
00342                         $encSession = $db->addQuotes( $this->session );
00343                         # Bail if there are any existing writers...
00344                         # The may detect readers, but the safe check for them is below.
00345                         # Note: if two writers come at the same time, both bail :)
00346                         $blocked = $db->selectField( 'filelocks_shared', '1',
00347                                 array( 'fls_key' => $keys, "fls_session != $encSession" ),
00348                                 __METHOD__
00349                         );
00350                         if ( !$blocked ) {
00351                                 # Build up values for INSERT clause
00352                                 $data = array();
00353                                 foreach ( $keys as $key ) {
00354                                         $data[] = array( 'fle_key' => $key );
00355                                 }
00356                                 # Block new readers/writers...
00357                                 $db->insert( 'filelocks_exclusive', $data, __METHOD__ );
00358                                 # Bail if there are any existing readers...
00359                                 $blocked = $db->selectField( 'filelocks_shared', '1',
00360                                         array( 'fls_key' => $keys, "fls_session != $encSession" ),
00361                                         __METHOD__
00362                                 );
00363                         }
00364                 }
00365 
00366                 if ( $blocked ) {
00367                         foreach ( $paths as $path ) {
00368                                 $status->fatal( 'lockmanager-fail-acquirelock', $path );
00369                         }
00370                 }
00371 
00372                 return $status;
00373         }
00374 }