MediaWiki  REL1_20
RedisBagOStuff.php
Go to the documentation of this file.
00001 <?php
00024 class RedisBagOStuff extends BagOStuff {
00025         protected $connectTimeout, $persistent, $password, $automaticFailover;
00026 
00030         protected $servers;
00031 
00036         protected $conns = array();
00037 
00045         protected $deadServers = array();
00046 
00073         function __construct( $params ) {
00074                 if ( !extension_loaded( 'redis' ) ) {
00075                         throw new MWException( __CLASS__. ' requires the phpredis extension: ' .
00076                                 'https://github.com/nicolasff/phpredis' );
00077                 }
00078 
00079                 $this->servers = $params['servers'];
00080                 $this->connectTimeout = isset( $params['connectTimeout'] )
00081                         ? $params['connectTimeout'] : 1;
00082                 $this->persistent = !empty( $params['persistent'] );
00083                 if ( isset( $params['password'] ) ) {
00084                         $this->password = $params['password'];
00085                 }
00086                 if ( isset( $params['automaticFailover'] ) ) {
00087                         $this->automaticFailover = $params['automaticFailover'];
00088                 } else {
00089                         $this->automaticFailover = true;
00090                 }
00091         }
00092 
00093         public function get( $key ) {
00094                 wfProfileIn( __METHOD__ );
00095                 list( $server, $conn ) = $this->getConnection( $key );
00096                 if ( !$conn ) {
00097                         wfProfileOut( __METHOD__ );
00098                         return false;
00099                 }
00100                 try {
00101                         $result = $conn->get( $key );
00102                 } catch ( RedisException $e ) {
00103                         $result = false;
00104                         $this->handleException( $server, $e );
00105                 }
00106                 $this->logRequest( 'get', $key, $server, $result );
00107                 wfProfileOut( __METHOD__ );
00108                 return $result;
00109         }
00110 
00111         public function set( $key, $value, $expiry = 0 ) {
00112                 wfProfileIn( __METHOD__ );
00113                 list( $server, $conn ) = $this->getConnection( $key );
00114                 if ( !$conn ) {
00115                         wfProfileOut( __METHOD__ );
00116                         return false;
00117                 }
00118                 $expiry = $this->convertToRelative( $expiry );
00119                 try {
00120                         if ( !$expiry ) {
00121                                 // No expiry, that is very different from zero expiry in Redis
00122                                 $result = $conn->set( $key, $value );
00123                         } else {
00124                                 $result = $conn->setex( $key, $expiry, $value );
00125                         }
00126                 } catch ( RedisException $e ) {
00127                         $result = false;
00128                         $this->handleException( $server, $e );
00129                 }
00130 
00131                 $this->logRequest( 'set', $key, $server, $result );
00132                 wfProfileOut( __METHOD__ );
00133                 return $result;
00134         }
00135 
00136         public function delete( $key, $time = 0 ) {
00137                 wfProfileIn( __METHOD__ );
00138                 list( $server, $conn ) = $this->getConnection( $key );
00139                 if ( !$conn ) {
00140                         wfProfileOut( __METHOD__ );
00141                         return false;
00142                 }
00143                 try {
00144                         $conn->delete( $key );
00145                         // Return true even if the key didn't exist
00146                         $result = true;
00147                 } catch ( RedisException $e ) {
00148                         $result = false;
00149                         $this->handleException( $server, $e );
00150                 }
00151                 $this->logRequest( 'delete', $key, $server, $result );
00152                 wfProfileOut( __METHOD__ );
00153                 return $result;
00154         }
00155 
00156         public function getMulti( array $keys ) {
00157                 wfProfileIn( __METHOD__ );
00158                 $batches = array();
00159                 $conns = array();
00160                 foreach ( $keys as $key ) {
00161                         list( $server, $conn ) = $this->getConnection( $key );
00162                         if ( !$conn ) {
00163                                 continue;
00164                         }
00165                         $conns[$server] = $conn;
00166                         $batches[$server][] = $key;
00167                 }
00168                 $result = array();
00169                 foreach ( $batches as $server => $batchKeys ) {
00170                         $conn = $conns[$server];
00171                         try {
00172                                 $conn->multi( Redis::PIPELINE );
00173                                 foreach ( $batchKeys as $key ) {
00174                                         $conn->get( $key );
00175                                 }
00176                                 $batchResult = $conn->exec();
00177                                 if ( $batchResult === false ) {
00178                                         $this->debug( "multi request to $server failed" );
00179                                         continue;
00180                                 }
00181                                 foreach ( $batchResult as $i => $value ) {
00182                                         if ( $value !== false ) {
00183                                                 $result[$batchKeys[$i]] = $value;
00184                                         }
00185                                 }
00186                         } catch ( RedisException $e ) {
00187                                 $this->handleException( $server, $e );
00188                         }
00189                 }
00190 
00191                 $this->debug( "getMulti for " . count( $keys ) . " keys " .
00192                         "returned " . count( $result ) . " results" );
00193                 wfProfileOut( __METHOD__ );
00194                 return $result;
00195         }
00196 
00197         public function add( $key, $value, $expiry = 0 ) {
00198                 wfProfileIn( __METHOD__ );
00199                 list( $server, $conn ) = $this->getConnection( $key );
00200                 if ( !$conn ) {
00201                         wfProfileOut( __METHOD__ );
00202                         return false;
00203                 }
00204                 $expiry = $this->convertToRelative( $expiry );
00205                 try {
00206                         $result = $conn->setnx( $key, $value );
00207                         if ( $result && $expiry ) {
00208                                 $conn->expire( $key, $expiry );
00209                         }
00210                 } catch ( RedisException $e ) {
00211                         $result = false;
00212                         $this->handleException( $server, $e );
00213                 }
00214                 $this->logRequest( 'add', $key, $server, $result );
00215                 wfProfileOut( __METHOD__ );
00216                 return $result;
00217         }
00218 
00223         public function replace( $key, $value, $expiry = 0 ) {
00224                 wfProfileIn( __METHOD__ );
00225                 list( $server, $conn ) = $this->getConnection( $key );
00226                 if ( !$conn ) {
00227                         wfProfileOut( __METHOD__ );
00228                         return false;
00229                 }
00230                 if ( !$conn->exists( $key ) ) {
00231                         wfProfileOut( __METHOD__ );
00232                         return false;
00233                 }
00234 
00235                 $expiry = $this->convertToRelative( $expiry );
00236                 try {
00237                         if ( !$expiry ) {
00238                                 $result = $conn->set( $key, $value );
00239                         } else {
00240                                 $result = $conn->setex( $key, $expiry, $value );
00241                         }
00242                 } catch ( RedisException $e ) {
00243                         $result = false;
00244                         $this->handleException( $server, $e );
00245                 }
00246 
00247                 $this->logRequest( 'replace', $key, $server, $result );
00248                 wfProfileOut( __METHOD__ );
00249                 return $result;
00250         }
00251 
00261         public function incr( $key, $value = 1 ) {
00262                 wfProfileIn( __METHOD__ );
00263                 list( $server, $conn ) = $this->getConnection( $key );
00264                 if ( !$conn ) {
00265                         wfProfileOut( __METHOD__ );
00266                         return false;
00267                 }
00268                 if ( !$conn->exists( $key ) ) {
00269                         wfProfileOut( __METHOD__ );
00270                         return null;
00271                 }
00272                 try {
00273                         $result = $conn->incrBy( $key, $value );
00274                 } catch ( RedisException $e ) {
00275                         $result = false;
00276                         $this->handleException( $server, $e );
00277                 }
00278 
00279                 $this->logRequest( 'incr', $key, $server, $result );
00280                 wfProfileOut( __METHOD__ );
00281                 return $result;
00282         }
00283 
00287         protected function getConnection( $key ) {
00288                 if ( count( $this->servers ) === 1 ) {
00289                         $candidates = $this->servers;
00290                 } else {
00291                         // Use consistent hashing
00292                         $hashes = array();
00293                         foreach ( $this->servers as $server ) {
00294                                 $hashes[$server] = md5( $server . '/' . $key );
00295                         }
00296                         asort( $hashes );
00297                         if ( !$this->automaticFailover ) {
00298                                 reset( $hashes );
00299                                 $candidates = array( key( $hashes ) );
00300                         } else {
00301                                 $candidates = array_keys( $hashes );
00302                         }
00303                 }
00304 
00305                 foreach ( $candidates as $server ) {
00306                         $conn = $this->getConnectionToServer( $server );
00307                         if ( $conn ) {
00308                                 return array( $server, $conn );
00309                         }
00310                 }
00311                 return array( false, false );
00312         }
00313 
00320         protected function getConnectionToServer( $server ) {
00321                 if ( isset( $this->deadServers[$server] ) ) {
00322                         $now = time();
00323                         if ( $now > $this->deadServers[$server] ) {
00324                                 // Dead time expired
00325                                 unset( $this->deadServers[$server] );
00326                         } else {
00327                                 // Server is dead
00328                                 $this->debug( "server $server is marked down for another " .
00329                                         ($this->deadServers[$server] - $now ) .
00330                                         " seconds, can't get connection" );
00331                                 return false;
00332                         }
00333                 }
00334 
00335                 if ( isset( $this->conns[$server] ) ) {
00336                         return $this->conns[$server];
00337                 }
00338 
00339                 if ( substr( $server, 0, 1 ) === '/' ) {
00340                         // UNIX domain socket
00341                         // These are required by the redis extension to start with a slash, but
00342                         // we still need to set the port to a special value to make it work.
00343                         $host = $server;
00344                         $port = 0;
00345                 } else {
00346                         // TCP connection
00347                         $hostPort = IP::splitHostAndPort( $server );
00348                         if ( !$hostPort ) {
00349                                 throw new MWException( __CLASS__.": invalid configured server \"$server\"" );
00350                         }
00351                         list( $host, $port ) = $hostPort;
00352                         if ( $port === false ) {
00353                                 $port = 6379;
00354                         }
00355                 }
00356                 $conn = new Redis;
00357                 try {
00358                         if ( $this->persistent ) {
00359                                 $this->debug( "opening persistent connection to $host:$port" );
00360                                 $result = $conn->pconnect( $host, $port, $this->connectTimeout );
00361                         } else {
00362                                 $this->debug( "opening non-persistent connection to $host:$port" );
00363                                 $result = $conn->connect( $host, $port, $this->connectTimeout );
00364                         }
00365                         if ( !$result ) {
00366                                 $this->logError( "could not connect to server $server" );
00367                                 // Mark server down for 30s to avoid further timeouts
00368                                 $this->deadServers[$server] = time() + 30;
00369                                 return false;
00370                         }
00371                         if ( $this->password !== null ) {
00372                                 if ( !$conn->auth( $this->password ) ) {
00373                                         $this->logError( "authentication error connecting to $server" );
00374                                 }
00375                         }
00376                 } catch ( RedisException $e ) {
00377                         $this->deadServers[$server] = time() + 30;
00378                         wfDebugLog( 'redis', "Redis exception: " . $e->getMessage() . "\n" );
00379                         return false;
00380                 }
00381 
00382                 $conn->setOption( Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP );
00383                 $this->conns[$server] = $conn;
00384                 return $conn;
00385         }
00386 
00390         protected function logError( $msg ) {
00391                 wfDebugLog( 'redis', "Redis error: $msg\n" );
00392         }
00393 
00400         protected function handleException( $server, $e ) {
00401                 wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() . "\n" );
00402                 unset( $this->conns[$server] );
00403         }
00404 
00408         public function logRequest( $method, $key, $server, $result ) {
00409                 $this->debug( "$method $key on $server: " .
00410                         ( $result === false ? "failure" : "success" ) );
00411         }
00412 }
00413