MediaWiki
REL1_19
|
00001 <?php 00002 00008 class SqlBagOStuff extends BagOStuff { 00009 00013 var $lb; 00014 00018 var $db; 00019 var $serverInfo; 00020 var $lastExpireAll = 0; 00021 var $purgePeriod = 100; 00022 var $shards = 1; 00023 var $tableName = 'objectcache'; 00024 00049 public function __construct( $params ) { 00050 if ( isset( $params['server'] ) ) { 00051 $this->serverInfo = $params['server']; 00052 $this->serverInfo['load'] = 1; 00053 } 00054 if ( isset( $params['purgePeriod'] ) ) { 00055 $this->purgePeriod = intval( $params['purgePeriod'] ); 00056 } 00057 if ( isset( $params['tableName'] ) ) { 00058 $this->tableName = $params['tableName']; 00059 } 00060 if ( isset( $params['shards'] ) ) { 00061 $this->shards = intval( $params['shards'] ); 00062 } 00063 } 00064 00068 protected function getDB() { 00069 if ( !isset( $this->db ) ) { 00070 # If server connection info was given, use that 00071 if ( $this->serverInfo ) { 00072 $this->lb = new LoadBalancer( array( 00073 'servers' => array( $this->serverInfo ) ) ); 00074 $this->db = $this->lb->getConnection( DB_MASTER ); 00075 $this->db->clearFlag( DBO_TRX ); 00076 } else { 00077 # We must keep a separate connection to MySQL in order to avoid deadlocks 00078 # However, SQLite has an opposite behaviour. 00079 # @todo Investigate behaviour for other databases 00080 if ( wfGetDB( DB_MASTER )->getType() == 'sqlite' ) { 00081 $this->db = wfGetDB( DB_MASTER ); 00082 } else { 00083 $this->lb = wfGetLBFactory()->newMainLB(); 00084 $this->db = $this->lb->getConnection( DB_MASTER ); 00085 $this->db->clearFlag( DBO_TRX ); 00086 } 00087 } 00088 } 00089 00090 return $this->db; 00091 } 00092 00096 protected function getTableByKey( $key ) { 00097 if ( $this->shards > 1 ) { 00098 $hash = hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff; 00099 return $this->getTableByShard( $hash % $this->shards ); 00100 } else { 00101 return $this->tableName; 00102 } 00103 } 00104 00108 protected function getTableByShard( $index ) { 00109 if ( $this->shards > 1 ) { 00110 $decimals = strlen( $this->shards - 1 ); 00111 return $this->tableName . 00112 sprintf( "%0{$decimals}d", $index ); 00113 } else { 00114 return $this->tableName; 00115 } 00116 } 00117 00118 public function get( $key ) { 00119 # expire old entries if any 00120 $this->garbageCollect(); 00121 $db = $this->getDB(); 00122 $tableName = $this->getTableByKey( $key ); 00123 $row = $db->selectRow( $tableName, array( 'value', 'exptime' ), 00124 array( 'keyname' => $key ), __METHOD__ ); 00125 00126 if ( !$row ) { 00127 $this->debug( 'get: no matching rows' ); 00128 return false; 00129 } 00130 00131 $this->debug( "get: retrieved data; expiry time is " . $row->exptime ); 00132 00133 if ( $this->isExpired( $row->exptime ) ) { 00134 $this->debug( "get: key has expired, deleting" ); 00135 try { 00136 $db->begin( __METHOD__ ); 00137 # Put the expiry time in the WHERE condition to avoid deleting a 00138 # newly-inserted value 00139 $db->delete( $tableName, 00140 array( 00141 'keyname' => $key, 00142 'exptime' => $row->exptime 00143 ), __METHOD__ ); 00144 $db->commit( __METHOD__ ); 00145 } catch ( DBQueryError $e ) { 00146 $this->handleWriteError( $e ); 00147 } 00148 00149 return false; 00150 } 00151 00152 return $this->unserialize( $db->decodeBlob( $row->value ) ); 00153 } 00154 00155 public function set( $key, $value, $exptime = 0 ) { 00156 $db = $this->getDB(); 00157 $exptime = intval( $exptime ); 00158 00159 if ( $exptime < 0 ) { 00160 $exptime = 0; 00161 } 00162 00163 if ( $exptime == 0 ) { 00164 $encExpiry = $this->getMaxDateTime(); 00165 } else { 00166 if ( $exptime < 3.16e8 ) { # ~10 years 00167 $exptime += time(); 00168 } 00169 00170 $encExpiry = $db->timestamp( $exptime ); 00171 } 00172 try { 00173 $db->begin( __METHOD__ ); 00174 // (bug 24425) use a replace if the db supports it instead of 00175 // delete/insert to avoid clashes with conflicting keynames 00176 $db->replace( 00177 $this->getTableByKey( $key ), 00178 array( 'keyname' ), 00179 array( 00180 'keyname' => $key, 00181 'value' => $db->encodeBlob( $this->serialize( $value ) ), 00182 'exptime' => $encExpiry 00183 ), __METHOD__ ); 00184 $db->commit( __METHOD__ ); 00185 } catch ( DBQueryError $e ) { 00186 $this->handleWriteError( $e ); 00187 00188 return false; 00189 } 00190 00191 return true; 00192 } 00193 00194 public function delete( $key, $time = 0 ) { 00195 $db = $this->getDB(); 00196 00197 try { 00198 $db->begin( __METHOD__ ); 00199 $db->delete( 00200 $this->getTableByKey( $key ), 00201 array( 'keyname' => $key ), 00202 __METHOD__ ); 00203 $db->commit( __METHOD__ ); 00204 } catch ( DBQueryError $e ) { 00205 $this->handleWriteError( $e ); 00206 00207 return false; 00208 } 00209 00210 return true; 00211 } 00212 00213 public function incr( $key, $step = 1 ) { 00214 $db = $this->getDB(); 00215 $tableName = $this->getTableByKey( $key ); 00216 $step = intval( $step ); 00217 00218 try { 00219 $db->begin( __METHOD__ ); 00220 $row = $db->selectRow( 00221 $tableName, 00222 array( 'value', 'exptime' ), 00223 array( 'keyname' => $key ), 00224 __METHOD__, 00225 array( 'FOR UPDATE' ) ); 00226 if ( $row === false ) { 00227 // Missing 00228 $db->commit( __METHOD__ ); 00229 00230 return null; 00231 } 00232 $db->delete( $tableName, array( 'keyname' => $key ), __METHOD__ ); 00233 if ( $this->isExpired( $row->exptime ) ) { 00234 // Expired, do not reinsert 00235 $db->commit( __METHOD__ ); 00236 00237 return null; 00238 } 00239 00240 $oldValue = intval( $this->unserialize( $db->decodeBlob( $row->value ) ) ); 00241 $newValue = $oldValue + $step; 00242 $db->insert( $tableName, 00243 array( 00244 'keyname' => $key, 00245 'value' => $db->encodeBlob( $this->serialize( $newValue ) ), 00246 'exptime' => $row->exptime 00247 ), __METHOD__, 'IGNORE' ); 00248 00249 if ( $db->affectedRows() == 0 ) { 00250 // Race condition. See bug 28611 00251 $newValue = null; 00252 } 00253 $db->commit( __METHOD__ ); 00254 } catch ( DBQueryError $e ) { 00255 $this->handleWriteError( $e ); 00256 00257 return null; 00258 } 00259 00260 return $newValue; 00261 } 00262 00263 public function keys() { 00264 $db = $this->getDB(); 00265 $result = array(); 00266 00267 for ( $i = 0; $i < $this->shards; $i++ ) { 00268 $res = $db->select( $this->getTableByShard( $i ), 00269 array( 'keyname' ), false, __METHOD__ ); 00270 foreach ( $res as $row ) { 00271 $result[] = $row->keyname; 00272 } 00273 } 00274 00275 return $result; 00276 } 00277 00278 protected function isExpired( $exptime ) { 00279 return $exptime != $this->getMaxDateTime() && wfTimestamp( TS_UNIX, $exptime ) < time(); 00280 } 00281 00282 protected function getMaxDateTime() { 00283 if ( time() > 0x7fffffff ) { 00284 return $this->getDB()->timestamp( 1 << 62 ); 00285 } else { 00286 return $this->getDB()->timestamp( 0x7fffffff ); 00287 } 00288 } 00289 00290 protected function garbageCollect() { 00291 if ( !$this->purgePeriod ) { 00292 // Disabled 00293 return; 00294 } 00295 // Only purge on one in every $this->purgePeriod requests. 00296 if ( $this->purgePeriod !== 1 && mt_rand( 0, $this->purgePeriod - 1 ) ) { 00297 return; 00298 } 00299 $now = time(); 00300 // Avoid repeating the delete within a few seconds 00301 if ( $now > ( $this->lastExpireAll + 1 ) ) { 00302 $this->lastExpireAll = $now; 00303 $this->expireAll(); 00304 } 00305 } 00306 00307 public function expireAll() { 00308 $this->deleteObjectsExpiringBefore( wfTimestampNow() ); 00309 } 00310 00314 public function deleteObjectsExpiringBefore( $timestamp, $progressCallback = false ) { 00315 $db = $this->getDB(); 00316 $dbTimestamp = $db->timestamp( $timestamp ); 00317 $totalSeconds = false; 00318 $baseConds = array( 'exptime < ' . $db->addQuotes( $dbTimestamp ) ); 00319 00320 try { 00321 for ( $i = 0; $i < $this->shards; $i++ ) { 00322 $maxExpTime = false; 00323 while ( true ) { 00324 $conds = $baseConds; 00325 if ( $maxExpTime !== false ) { 00326 $conds[] = 'exptime > ' . $db->addQuotes( $maxExpTime ); 00327 } 00328 $rows = $db->select( 00329 $this->getTableByShard( $i ), 00330 array( 'keyname', 'exptime' ), 00331 $conds, 00332 __METHOD__, 00333 array( 'LIMIT' => 100, 'ORDER BY' => 'exptime' ) ); 00334 if ( !$rows->numRows() ) { 00335 break; 00336 } 00337 $keys = array(); 00338 $row = $rows->current(); 00339 $minExpTime = $row->exptime; 00340 if ( $totalSeconds === false ) { 00341 $totalSeconds = wfTimestamp( TS_UNIX, $timestamp ) 00342 - wfTimestamp( TS_UNIX, $minExpTime ); 00343 } 00344 foreach ( $rows as $row ) { 00345 $keys[] = $row->keyname; 00346 $maxExpTime = $row->exptime; 00347 } 00348 00349 $db->begin( __METHOD__ ); 00350 $db->delete( 00351 $this->getTableByShard( $i ), 00352 array( 00353 'exptime >= ' . $db->addQuotes( $minExpTime ), 00354 'exptime < ' . $db->addQuotes( $dbTimestamp ), 00355 'keyname' => $keys 00356 ), 00357 __METHOD__ ); 00358 $db->commit( __METHOD__ ); 00359 00360 if ( $progressCallback ) { 00361 if ( intval( $totalSeconds ) === 0 ) { 00362 $percent = 0; 00363 } else { 00364 $remainingSeconds = wfTimestamp( TS_UNIX, $timestamp ) 00365 - wfTimestamp( TS_UNIX, $maxExpTime ); 00366 if ( $remainingSeconds > $totalSeconds ) { 00367 $totalSeconds = $remainingSeconds; 00368 } 00369 $percent = ( $i + $remainingSeconds / $totalSeconds ) 00370 / $this->shards * 100; 00371 } 00372 call_user_func( $progressCallback, $percent ); 00373 } 00374 } 00375 } 00376 } catch ( DBQueryError $e ) { 00377 $this->handleWriteError( $e ); 00378 } 00379 return true; 00380 } 00381 00382 public function deleteAll() { 00383 $db = $this->getDB(); 00384 00385 try { 00386 for ( $i = 0; $i < $this->shards; $i++ ) { 00387 $db->begin( __METHOD__ ); 00388 $db->delete( $this->getTableByShard( $i ), '*', __METHOD__ ); 00389 $db->commit( __METHOD__ ); 00390 } 00391 } catch ( DBQueryError $e ) { 00392 $this->handleWriteError( $e ); 00393 } 00394 } 00395 00404 protected function serialize( &$data ) { 00405 $serial = serialize( $data ); 00406 00407 if ( function_exists( 'gzdeflate' ) ) { 00408 return gzdeflate( $serial ); 00409 } else { 00410 return $serial; 00411 } 00412 } 00413 00419 protected function unserialize( $serial ) { 00420 if ( function_exists( 'gzinflate' ) ) { 00421 wfSuppressWarnings(); 00422 $decomp = gzinflate( $serial ); 00423 wfRestoreWarnings(); 00424 00425 if ( false !== $decomp ) { 00426 $serial = $decomp; 00427 } 00428 } 00429 00430 $ret = unserialize( $serial ); 00431 00432 return $ret; 00433 } 00434 00439 protected function handleWriteError( $exception ) { 00440 $db = $this->getDB(); 00441 00442 if ( !$db->wasReadOnlyError() ) { 00443 throw $exception; 00444 } 00445 00446 try { 00447 $db->rollback(); 00448 } catch ( DBQueryError $e ) { 00449 } 00450 00451 wfDebug( __METHOD__ . ": ignoring query error\n" ); 00452 $db->ignoreErrors( false ); 00453 } 00454 00458 public function createTables() { 00459 $db = $this->getDB(); 00460 if ( $db->getType() !== 'mysql' 00461 || version_compare( $db->getServerVersion(), '4.1.0', '<' ) ) 00462 { 00463 throw new MWException( __METHOD__ . ' is not supported on this DB server' ); 00464 } 00465 00466 for ( $i = 0; $i < $this->shards; $i++ ) { 00467 $db->begin( __METHOD__ ); 00468 $db->query( 00469 'CREATE TABLE ' . $db->tableName( $this->getTableByShard( $i ) ) . 00470 ' LIKE ' . $db->tableName( 'objectcache' ), 00471 __METHOD__ ); 00472 $db->commit( __METHOD__ ); 00473 } 00474 } 00475 } 00476 00480 class MediaWikiBagOStuff extends SqlBagOStuff { } 00481