MediaWiki  REL1_19
SqlBagOStuff.php
Go to the documentation of this file.
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