MediaWiki
REL1_22
|
00001 <?php 00039 abstract class DBLockManager extends QuorumLockManager { 00041 protected $dbServers; // (DB name => server config array) 00043 protected $statusCache; 00044 00045 protected $lockExpiry; // integer number of seconds 00046 protected $safeDelay; // integer number of seconds 00047 00048 protected $session = 0; // random integer 00050 protected $conns = array(); 00051 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 00113 // @TODO: change this code to work in one batch 00114 protected function getLocksOnServer( $lockSrv, array $pathsByType ) { 00115 $status = Status::newGood(); 00116 foreach ( $pathsByType as $type => $paths ) { 00117 $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) ); 00118 } 00119 return $status; 00120 } 00121 00122 protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { 00123 return Status::newGood(); 00124 } 00125 00130 protected function isServerUp( $lockSrv ) { 00131 if ( !$this->cacheCheckFailures( $lockSrv ) ) { 00132 return false; // recent failure to connect 00133 } 00134 try { 00135 $this->getConnection( $lockSrv ); 00136 } catch ( DBError $e ) { 00137 $this->cacheRecordFailure( $lockSrv ); 00138 return false; // failed to connect 00139 } 00140 return true; 00141 } 00142 00150 protected function getConnection( $lockDb ) { 00151 if ( !isset( $this->conns[$lockDb] ) ) { 00152 $db = null; 00153 if ( $lockDb === 'localDBMaster' ) { 00154 $lb = wfGetLBFactory()->getMainLB( $this->domain ); 00155 $db = $lb->getConnection( DB_MASTER, array(), $this->domain ); 00156 } elseif ( isset( $this->dbServers[$lockDb] ) ) { 00157 $config = $this->dbServers[$lockDb]; 00158 $db = DatabaseBase::factory( $config['type'], $config ); 00159 } 00160 if ( !$db ) { 00161 return null; // config error? 00162 } 00163 $this->conns[$lockDb] = $db; 00164 $this->conns[$lockDb]->clearFlag( DBO_TRX ); 00165 # If the connection drops, try to avoid letting the DB rollback 00166 # and release the locks before the file operations are finished. 00167 # This won't handle the case of DB server restarts however. 00168 $options = array(); 00169 if ( $this->lockExpiry > 0 ) { 00170 $options['connTimeout'] = $this->lockExpiry; 00171 } 00172 $this->conns[$lockDb]->setSessionOptions( $options ); 00173 $this->initConnection( $lockDb, $this->conns[$lockDb] ); 00174 } 00175 if ( !$this->conns[$lockDb]->trxLevel() ) { 00176 $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction 00177 } 00178 return $this->conns[$lockDb]; 00179 } 00180 00189 protected function initConnection( $lockDb, DatabaseBase $db ) {} 00190 00198 protected function cacheCheckFailures( $lockDb ) { 00199 return ( $this->statusCache && $this->safeDelay > 0 ) 00200 ? !$this->statusCache->get( $this->getMissKey( $lockDb ) ) 00201 : true; 00202 } 00203 00210 protected function cacheRecordFailure( $lockDb ) { 00211 return ( $this->statusCache && $this->safeDelay > 0 ) 00212 ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay ) 00213 : true; 00214 } 00215 00222 protected function getMissKey( $lockDb ) { 00223 $lockDb = ( $lockDb === 'localDBMaster' ) ? wfWikiID() : $lockDb; // non-relative 00224 return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb ); 00225 } 00226 00230 function __destruct() { 00231 $this->releaseAllLocks(); 00232 foreach ( $this->conns as $db ) { 00233 $db->close(); 00234 } 00235 } 00236 } 00237 00244 class MySqlLockManager extends DBLockManager { 00246 protected $lockTypeMap = array( 00247 self::LOCK_SH => self::LOCK_SH, 00248 self::LOCK_UW => self::LOCK_SH, 00249 self::LOCK_EX => self::LOCK_EX 00250 ); 00251 00256 protected function initConnection( $lockDb, DatabaseBase $db ) { 00257 # Let this transaction see lock rows from other transactions 00258 $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" ); 00259 } 00260 00268 protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { 00269 $status = Status::newGood(); 00270 00271 $db = $this->getConnection( $lockSrv ); // checked in isServerUp() 00272 00273 $keys = array(); // list of hash keys for the paths 00274 $data = array(); // list of rows to insert 00275 $checkEXKeys = array(); // list of hash keys that this has no EX lock on 00276 # Build up values for INSERT clause 00277 foreach ( $paths as $path ) { 00278 $key = $this->sha1Base36Absolute( $path ); 00279 $keys[] = $key; 00280 $data[] = array( 'fls_key' => $key, 'fls_session' => $this->session ); 00281 if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { 00282 $checkEXKeys[] = $key; 00283 } 00284 } 00285 00286 # Block new writers (both EX and SH locks leave entries here)... 00287 $db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) ); 00288 # Actually do the locking queries... 00289 if ( $type == self::LOCK_SH ) { // reader locks 00290 $blocked = false; 00291 # Bail if there are any existing writers... 00292 if ( count( $checkEXKeys ) ) { 00293 $blocked = $db->selectField( 'filelocks_exclusive', '1', 00294 array( 'fle_key' => $checkEXKeys ), 00295 __METHOD__ 00296 ); 00297 } 00298 # Other prospective writers that haven't yet updated filelocks_exclusive 00299 # will recheck filelocks_shared after doing so and bail due to this entry. 00300 } else { // writer locks 00301 $encSession = $db->addQuotes( $this->session ); 00302 # Bail if there are any existing writers... 00303 # This may detect readers, but the safe check for them is below. 00304 # Note: if two writers come at the same time, both bail :) 00305 $blocked = $db->selectField( 'filelocks_shared', '1', 00306 array( 'fls_key' => $keys, "fls_session != $encSession" ), 00307 __METHOD__ 00308 ); 00309 if ( !$blocked ) { 00310 # Build up values for INSERT clause 00311 $data = array(); 00312 foreach ( $keys as $key ) { 00313 $data[] = array( 'fle_key' => $key ); 00314 } 00315 # Block new readers/writers... 00316 $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); 00317 # Bail if there are any existing readers... 00318 $blocked = $db->selectField( 'filelocks_shared', '1', 00319 array( 'fls_key' => $keys, "fls_session != $encSession" ), 00320 __METHOD__ 00321 ); 00322 } 00323 } 00324 00325 if ( $blocked ) { 00326 foreach ( $paths as $path ) { 00327 $status->fatal( 'lockmanager-fail-acquirelock', $path ); 00328 } 00329 } 00330 00331 return $status; 00332 } 00333 00338 protected function releaseAllLocks() { 00339 $status = Status::newGood(); 00340 00341 foreach ( $this->conns as $lockDb => $db ) { 00342 if ( $db->trxLevel() ) { // in transaction 00343 try { 00344 $db->rollback( __METHOD__ ); // finish transaction and kill any rows 00345 } catch ( DBError $e ) { 00346 $status->fatal( 'lockmanager-fail-db-release', $lockDb ); 00347 } 00348 } 00349 } 00350 00351 return $status; 00352 } 00353 } 00354 00361 class PostgreSqlLockManager extends DBLockManager { 00363 protected $lockTypeMap = array( 00364 self::LOCK_SH => self::LOCK_SH, 00365 self::LOCK_UW => self::LOCK_SH, 00366 self::LOCK_EX => self::LOCK_EX 00367 ); 00368 00369 protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { 00370 $status = Status::newGood(); 00371 if ( !count( $paths ) ) { 00372 return $status; // nothing to lock 00373 } 00374 00375 $db = $this->getConnection( $lockSrv ); // checked in isServerUp() 00376 $bigints = array_unique( array_map( 00377 function( $key ) { 00378 return wfBaseConvert( substr( $key, 0, 15 ), 16, 10 ); 00379 }, 00380 array_map( array( $this, 'sha1Base16Absolute' ), $paths ) 00381 ) ); 00382 00383 // Try to acquire all the locks... 00384 $fields = array(); 00385 foreach ( $bigints as $bigint ) { 00386 $fields[] = ( $type == self::LOCK_SH ) 00387 ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint" 00388 : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint"; 00389 } 00390 $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ ); 00391 $row = (array)$res->fetchObject(); 00392 00393 if ( in_array( 'f', $row ) ) { 00394 // Release any acquired locks if some could not be acquired... 00395 $fields = array(); 00396 foreach ( $row as $kbigint => $ok ) { 00397 if ( $ok === 't' ) { // locked 00398 $bigint = substr( $kbigint, 1 ); // strip off the "K" 00399 $fields[] = ( $type == self::LOCK_SH ) 00400 ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})" 00401 : "pg_advisory_unlock({$db->addQuotes( $bigint )})"; 00402 } 00403 } 00404 if ( count( $fields ) ) { 00405 $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ ); 00406 } 00407 foreach ( $paths as $path ) { 00408 $status->fatal( 'lockmanager-fail-acquirelock', $path ); 00409 } 00410 } 00411 00412 return $status; 00413 } 00414 00419 protected function releaseAllLocks() { 00420 $status = Status::newGood(); 00421 00422 foreach ( $this->conns as $lockDb => $db ) { 00423 try { 00424 $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ ); 00425 } catch ( DBError $e ) { 00426 $status->fatal( 'lockmanager-fail-db-release', $lockDb ); 00427 } 00428 } 00429 00430 return $status; 00431 } 00432 }