MediaWiki
REL1_24
|
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 00074 public function __construct( array $config ) { 00075 parent::__construct( $config ); 00076 00077 $this->dbServers = isset( $config['dbServers'] ) 00078 ? $config['dbServers'] 00079 : array(); // likely just using 'localDBMaster' 00080 // Sanitize srvsByBucket config to prevent PHP errors 00081 $this->srvsByBucket = array_filter( $config['dbsByBucket'], 'is_array' ); 00082 $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive 00083 00084 if ( isset( $config['lockExpiry'] ) ) { 00085 $this->lockExpiry = $config['lockExpiry']; 00086 } else { 00087 $met = ini_get( 'max_execution_time' ); 00088 $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0 00089 } 00090 $this->safeDelay = ( $this->lockExpiry <= 0 ) 00091 ? 60 // pick a safe-ish number to match DB timeout default 00092 : $this->lockExpiry; // cover worst case 00093 00094 foreach ( $this->srvsByBucket as $bucket ) { 00095 if ( count( $bucket ) > 1 ) { // multiple peers 00096 // Tracks peers that couldn't be queried recently to avoid lengthy 00097 // connection timeouts. This is useless if each bucket has one peer. 00098 try { 00099 $this->statusCache = ObjectCache::newAccelerator( array() ); 00100 } catch ( MWException $e ) { 00101 trigger_error( __CLASS__ . 00102 " using multiple DB peers without apc, xcache, or wincache." ); 00103 } 00104 break; 00105 } 00106 } 00107 00108 $this->session = wfRandomString( 31 ); 00109 } 00110 00111 // @todo change this code to work in one batch 00112 protected function getLocksOnServer( $lockSrv, array $pathsByType ) { 00113 $status = Status::newGood(); 00114 foreach ( $pathsByType as $type => $paths ) { 00115 $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) ); 00116 } 00117 00118 return $status; 00119 } 00120 00121 protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { 00122 return Status::newGood(); 00123 } 00124 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 00139 return false; // failed to connect 00140 } 00141 00142 return true; 00143 } 00144 00152 protected function getConnection( $lockDb ) { 00153 if ( !isset( $this->conns[$lockDb] ) ) { 00154 $db = null; 00155 if ( $lockDb === 'localDBMaster' ) { 00156 $lb = wfGetLBFactory()->getMainLB( $this->domain ); 00157 $db = $lb->getConnection( DB_MASTER, array(), $this->domain ); 00158 } elseif ( isset( $this->dbServers[$lockDb] ) ) { 00159 $config = $this->dbServers[$lockDb]; 00160 $db = DatabaseBase::factory( $config['type'], $config ); 00161 } 00162 if ( !$db ) { 00163 return null; // config error? 00164 } 00165 $this->conns[$lockDb] = $db; 00166 $this->conns[$lockDb]->clearFlag( DBO_TRX ); 00167 # If the connection drops, try to avoid letting the DB rollback 00168 # and release the locks before the file operations are finished. 00169 # This won't handle the case of DB server restarts however. 00170 $options = array(); 00171 if ( $this->lockExpiry > 0 ) { 00172 $options['connTimeout'] = $this->lockExpiry; 00173 } 00174 $this->conns[$lockDb]->setSessionOptions( $options ); 00175 $this->initConnection( $lockDb, $this->conns[$lockDb] ); 00176 } 00177 if ( !$this->conns[$lockDb]->trxLevel() ) { 00178 $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction 00179 } 00180 00181 return $this->conns[$lockDb]; 00182 } 00183 00191 protected function initConnection( $lockDb, DatabaseBase $db ) { 00192 } 00193 00201 protected function cacheCheckFailures( $lockDb ) { 00202 return ( $this->statusCache && $this->safeDelay > 0 ) 00203 ? !$this->statusCache->get( $this->getMissKey( $lockDb ) ) 00204 : true; 00205 } 00206 00213 protected function cacheRecordFailure( $lockDb ) { 00214 return ( $this->statusCache && $this->safeDelay > 0 ) 00215 ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay ) 00216 : true; 00217 } 00218 00225 protected function getMissKey( $lockDb ) { 00226 $lockDb = ( $lockDb === 'localDBMaster' ) ? wfWikiID() : $lockDb; // non-relative 00227 return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb ); 00228 } 00229 00233 function __destruct() { 00234 $this->releaseAllLocks(); 00235 foreach ( $this->conns as $db ) { 00236 $db->close(); 00237 } 00238 } 00239 } 00240 00247 class MySqlLockManager extends DBLockManager { 00249 protected $lockTypeMap = array( 00250 self::LOCK_SH => self::LOCK_SH, 00251 self::LOCK_UW => self::LOCK_SH, 00252 self::LOCK_EX => self::LOCK_EX 00253 ); 00254 00259 protected function initConnection( $lockDb, DatabaseBase $db ) { 00260 # Let this transaction see lock rows from other transactions 00261 $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" ); 00262 } 00263 00274 protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { 00275 $status = Status::newGood(); 00276 00277 $db = $this->getConnection( $lockSrv ); // checked in isServerUp() 00278 00279 $keys = array(); // list of hash keys for the paths 00280 $data = array(); // list of rows to insert 00281 $checkEXKeys = array(); // list of hash keys that this has no EX lock on 00282 # Build up values for INSERT clause 00283 foreach ( $paths as $path ) { 00284 $key = $this->sha1Base36Absolute( $path ); 00285 $keys[] = $key; 00286 $data[] = array( 'fls_key' => $key, 'fls_session' => $this->session ); 00287 if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { 00288 $checkEXKeys[] = $key; 00289 } 00290 } 00291 00292 # Block new writers (both EX and SH locks leave entries here)... 00293 $db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) ); 00294 # Actually do the locking queries... 00295 if ( $type == self::LOCK_SH ) { // reader locks 00296 $blocked = false; 00297 # Bail if there are any existing writers... 00298 if ( count( $checkEXKeys ) ) { 00299 $blocked = $db->selectField( 'filelocks_exclusive', '1', 00300 array( 'fle_key' => $checkEXKeys ), 00301 __METHOD__ 00302 ); 00303 } 00304 # Other prospective writers that haven't yet updated filelocks_exclusive 00305 # will recheck filelocks_shared after doing so and bail due to this entry. 00306 } else { // writer locks 00307 $encSession = $db->addQuotes( $this->session ); 00308 # Bail if there are any existing writers... 00309 # This may detect readers, but the safe check for them is below. 00310 # Note: if two writers come at the same time, both bail :) 00311 $blocked = $db->selectField( 'filelocks_shared', '1', 00312 array( 'fls_key' => $keys, "fls_session != $encSession" ), 00313 __METHOD__ 00314 ); 00315 if ( !$blocked ) { 00316 # Build up values for INSERT clause 00317 $data = array(); 00318 foreach ( $keys as $key ) { 00319 $data[] = array( 'fle_key' => $key ); 00320 } 00321 # Block new readers/writers... 00322 $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); 00323 # Bail if there are any existing readers... 00324 $blocked = $db->selectField( 'filelocks_shared', '1', 00325 array( 'fls_key' => $keys, "fls_session != $encSession" ), 00326 __METHOD__ 00327 ); 00328 } 00329 } 00330 00331 if ( $blocked ) { 00332 foreach ( $paths as $path ) { 00333 $status->fatal( 'lockmanager-fail-acquirelock', $path ); 00334 } 00335 } 00336 00337 return $status; 00338 } 00339 00344 protected function releaseAllLocks() { 00345 $status = Status::newGood(); 00346 00347 foreach ( $this->conns as $lockDb => $db ) { 00348 if ( $db->trxLevel() ) { // in transaction 00349 try { 00350 $db->rollback( __METHOD__ ); // finish transaction and kill any rows 00351 } catch ( DBError $e ) { 00352 $status->fatal( 'lockmanager-fail-db-release', $lockDb ); 00353 } 00354 } 00355 } 00356 00357 return $status; 00358 } 00359 } 00360 00367 class PostgreSqlLockManager extends DBLockManager { 00369 protected $lockTypeMap = array( 00370 self::LOCK_SH => self::LOCK_SH, 00371 self::LOCK_UW => self::LOCK_SH, 00372 self::LOCK_EX => self::LOCK_EX 00373 ); 00374 00375 protected function doGetLocksOnServer( $lockSrv, array $paths, $type ) { 00376 $status = Status::newGood(); 00377 if ( !count( $paths ) ) { 00378 return $status; // nothing to lock 00379 } 00380 00381 $db = $this->getConnection( $lockSrv ); // checked in isServerUp() 00382 $bigints = array_unique( array_map( 00383 function ( $key ) { 00384 return wfBaseConvert( substr( $key, 0, 15 ), 16, 10 ); 00385 }, 00386 array_map( array( $this, 'sha1Base16Absolute' ), $paths ) 00387 ) ); 00388 00389 // Try to acquire all the locks... 00390 $fields = array(); 00391 foreach ( $bigints as $bigint ) { 00392 $fields[] = ( $type == self::LOCK_SH ) 00393 ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint" 00394 : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint"; 00395 } 00396 $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ ); 00397 $row = (array)$res->fetchObject(); 00398 00399 if ( in_array( 'f', $row ) ) { 00400 // Release any acquired locks if some could not be acquired... 00401 $fields = array(); 00402 foreach ( $row as $kbigint => $ok ) { 00403 if ( $ok === 't' ) { // locked 00404 $bigint = substr( $kbigint, 1 ); // strip off the "K" 00405 $fields[] = ( $type == self::LOCK_SH ) 00406 ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})" 00407 : "pg_advisory_unlock({$db->addQuotes( $bigint )})"; 00408 } 00409 } 00410 if ( count( $fields ) ) { 00411 $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ ); 00412 } 00413 foreach ( $paths as $path ) { 00414 $status->fatal( 'lockmanager-fail-acquirelock', $path ); 00415 } 00416 } 00417 00418 return $status; 00419 } 00420 00425 protected function releaseAllLocks() { 00426 $status = Status::newGood(); 00427 00428 foreach ( $this->conns as $lockDb => $db ) { 00429 try { 00430 $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ ); 00431 } catch ( DBError $e ) { 00432 $status->fatal( 'lockmanager-fail-db-release', $lockDb ); 00433 } 00434 } 00435 00436 return $status; 00437 } 00438 }