MediaWiki
REL1_21
|
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 00117 protected function isServerUp( $lockSrv ) { 00118 if ( !$this->cacheCheckFailures( $lockSrv ) ) { 00119 return false; // recent failure to connect 00120 } 00121 try { 00122 $this->getConnection( $lockSrv ); 00123 } catch ( DBError $e ) { 00124 $this->cacheRecordFailure( $lockSrv ); 00125 return false; // failed to connect 00126 } 00127 return true; 00128 } 00129 00137 protected function getConnection( $lockDb ) { 00138 if ( !isset( $this->conns[$lockDb] ) ) { 00139 $db = null; 00140 if ( $lockDb === 'localDBMaster' ) { 00141 $lb = wfGetLBFactory()->getMainLB( $this->domain ); 00142 $db = $lb->getConnection( DB_MASTER, array(), $this->domain ); 00143 } elseif ( isset( $this->dbServers[$lockDb] ) ) { 00144 $config = $this->dbServers[$lockDb]; 00145 $db = DatabaseBase::factory( $config['type'], $config ); 00146 } 00147 if ( !$db ) { 00148 return null; // config error? 00149 } 00150 $this->conns[$lockDb] = $db; 00151 $this->conns[$lockDb]->clearFlag( DBO_TRX ); 00152 # If the connection drops, try to avoid letting the DB rollback 00153 # and release the locks before the file operations are finished. 00154 # This won't handle the case of DB server restarts however. 00155 $options = array(); 00156 if ( $this->lockExpiry > 0 ) { 00157 $options['connTimeout'] = $this->lockExpiry; 00158 } 00159 $this->conns[$lockDb]->setSessionOptions( $options ); 00160 $this->initConnection( $lockDb, $this->conns[$lockDb] ); 00161 } 00162 if ( !$this->conns[$lockDb]->trxLevel() ) { 00163 $this->conns[$lockDb]->begin( __METHOD__ ); // start transaction 00164 } 00165 return $this->conns[$lockDb]; 00166 } 00167 00176 protected function initConnection( $lockDb, DatabaseBase $db ) {} 00177 00185 protected function cacheCheckFailures( $lockDb ) { 00186 return ( $this->statusCache && $this->safeDelay > 0 ) 00187 ? !$this->statusCache->get( $this->getMissKey( $lockDb ) ) 00188 : true; 00189 } 00190 00197 protected function cacheRecordFailure( $lockDb ) { 00198 return ( $this->statusCache && $this->safeDelay > 0 ) 00199 ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay ) 00200 : true; 00201 } 00202 00209 protected function getMissKey( $lockDb ) { 00210 $lockDb = ( $lockDb === 'localDBMaster' ) ? wfWikiID() : $lockDb; // non-relative 00211 return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb ); 00212 } 00213 00217 function __destruct() { 00218 $this->releaseAllLocks(); 00219 foreach ( $this->conns as $db ) { 00220 $db->close(); 00221 } 00222 } 00223 } 00224 00231 class MySqlLockManager extends DBLockManager { 00233 protected $lockTypeMap = array( 00234 self::LOCK_SH => self::LOCK_SH, 00235 self::LOCK_UW => self::LOCK_SH, 00236 self::LOCK_EX => self::LOCK_EX 00237 ); 00238 00243 protected function initConnection( $lockDb, DatabaseBase $db ) { 00244 # Let this transaction see lock rows from other transactions 00245 $db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" ); 00246 } 00247 00255 protected function getLocksOnServer( $lockSrv, array $paths, $type ) { 00256 $status = Status::newGood(); 00257 00258 $db = $this->getConnection( $lockSrv ); // checked in isServerUp() 00259 00260 $keys = array(); // list of hash keys for the paths 00261 $data = array(); // list of rows to insert 00262 $checkEXKeys = array(); // list of hash keys that this has no EX lock on 00263 # Build up values for INSERT clause 00264 foreach ( $paths as $path ) { 00265 $key = $this->sha1Base36Absolute( $path ); 00266 $keys[] = $key; 00267 $data[] = array( 'fls_key' => $key, 'fls_session' => $this->session ); 00268 if ( !isset( $this->locksHeld[$path][self::LOCK_EX] ) ) { 00269 $checkEXKeys[] = $key; 00270 } 00271 } 00272 00273 # Block new writers (both EX and SH locks leave entries here)... 00274 $db->insert( 'filelocks_shared', $data, __METHOD__, array( 'IGNORE' ) ); 00275 # Actually do the locking queries... 00276 if ( $type == self::LOCK_SH ) { // reader locks 00277 $blocked = false; 00278 # Bail if there are any existing writers... 00279 if ( count( $checkEXKeys ) ) { 00280 $blocked = $db->selectField( 'filelocks_exclusive', '1', 00281 array( 'fle_key' => $checkEXKeys ), 00282 __METHOD__ 00283 ); 00284 } 00285 # Other prospective writers that haven't yet updated filelocks_exclusive 00286 # will recheck filelocks_shared after doing so and bail due to this entry. 00287 } else { // writer locks 00288 $encSession = $db->addQuotes( $this->session ); 00289 # Bail if there are any existing writers... 00290 # This may detect readers, but the safe check for them is below. 00291 # Note: if two writers come at the same time, both bail :) 00292 $blocked = $db->selectField( 'filelocks_shared', '1', 00293 array( 'fls_key' => $keys, "fls_session != $encSession" ), 00294 __METHOD__ 00295 ); 00296 if ( !$blocked ) { 00297 # Build up values for INSERT clause 00298 $data = array(); 00299 foreach ( $keys as $key ) { 00300 $data[] = array( 'fle_key' => $key ); 00301 } 00302 # Block new readers/writers... 00303 $db->insert( 'filelocks_exclusive', $data, __METHOD__ ); 00304 # Bail if there are any existing readers... 00305 $blocked = $db->selectField( 'filelocks_shared', '1', 00306 array( 'fls_key' => $keys, "fls_session != $encSession" ), 00307 __METHOD__ 00308 ); 00309 } 00310 } 00311 00312 if ( $blocked ) { 00313 foreach ( $paths as $path ) { 00314 $status->fatal( 'lockmanager-fail-acquirelock', $path ); 00315 } 00316 } 00317 00318 return $status; 00319 } 00320 00325 protected function freeLocksOnServer( $lockSrv, array $paths, $type ) { 00326 return Status::newGood(); // not supported 00327 } 00328 00333 protected function releaseAllLocks() { 00334 $status = Status::newGood(); 00335 00336 foreach ( $this->conns as $lockDb => $db ) { 00337 if ( $db->trxLevel() ) { // in transaction 00338 try { 00339 $db->rollback( __METHOD__ ); // finish transaction and kill any rows 00340 } catch ( DBError $e ) { 00341 $status->fatal( 'lockmanager-fail-db-release', $lockDb ); 00342 } 00343 } 00344 } 00345 00346 return $status; 00347 } 00348 } 00349 00356 class PostgreSqlLockManager extends DBLockManager { 00358 protected $lockTypeMap = array( 00359 self::LOCK_SH => self::LOCK_SH, 00360 self::LOCK_UW => self::LOCK_SH, 00361 self::LOCK_EX => self::LOCK_EX 00362 ); 00363 00364 protected function getLocksOnServer( $lockSrv, array $paths, $type ) { 00365 $status = Status::newGood(); 00366 if ( !count( $paths ) ) { 00367 return $status; // nothing to lock 00368 } 00369 00370 $db = $this->getConnection( $lockSrv ); // checked in isServerUp() 00371 $bigints = array_unique( array_map( 00372 function( $key ) { return wfBaseConvert( substr( $key, 0, 15 ), 16, 10 ); }, 00373 array_map( array( $this, 'sha1Base16Absolute' ), $paths ) 00374 ) ); 00375 00376 // Try to acquire all the locks... 00377 $fields = array(); 00378 foreach ( $bigints as $bigint ) { 00379 $fields[] = ( $type == self::LOCK_SH ) 00380 ? "pg_try_advisory_lock_shared({$db->addQuotes( $bigint )}) AS K$bigint" 00381 : "pg_try_advisory_lock({$db->addQuotes( $bigint )}) AS K$bigint"; 00382 } 00383 $res = $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ ); 00384 $row = (array)$res->fetchObject(); 00385 00386 if ( in_array( 'f', $row ) ) { 00387 // Release any acquired locks if some could not be acquired... 00388 $fields = array(); 00389 foreach ( $row as $kbigint => $ok ) { 00390 if ( $ok === 't' ) { // locked 00391 $bigint = substr( $kbigint, 1 ); // strip off the "K" 00392 $fields[] = ( $type == self::LOCK_SH ) 00393 ? "pg_advisory_unlock_shared({$db->addQuotes( $bigint )})" 00394 : "pg_advisory_unlock({$db->addQuotes( $bigint )})"; 00395 } 00396 } 00397 if ( count( $fields ) ) { 00398 $db->query( 'SELECT ' . implode( ', ', $fields ), __METHOD__ ); 00399 } 00400 foreach ( $paths as $path ) { 00401 $status->fatal( 'lockmanager-fail-acquirelock', $path ); 00402 } 00403 } 00404 00405 return $status; 00406 } 00407 00412 protected function freeLocksOnServer( $lockSrv, array $paths, $type ) { 00413 return Status::newGood(); // not supported 00414 } 00415 00420 protected function releaseAllLocks() { 00421 $status = Status::newGood(); 00422 00423 foreach ( $this->conns as $lockDb => $db ) { 00424 try { 00425 $db->query( "SELECT pg_advisory_unlock_all()", __METHOD__ ); 00426 } catch ( DBError $e ) { 00427 $status->fatal( 'lockmanager-fail-db-release', $lockDb ); 00428 } 00429 } 00430 00431 return $status; 00432 } 00433 }