MediaWiki  REL1_23
RedisBagOStuff.php
Go to the documentation of this file.
00001 <?php
00023 class RedisBagOStuff extends BagOStuff {
00025     protected $redisPool;
00027     protected $servers;
00029     protected $automaticFailover;
00030 
00057     function __construct( $params ) {
00058         $redisConf = array( 'serializer' => 'none' ); // manage that in this class
00059         foreach ( array( 'connectTimeout', 'persistent', 'password' ) as $opt ) {
00060             if ( isset( $params[$opt] ) ) {
00061                 $redisConf[$opt] = $params[$opt];
00062             }
00063         }
00064         $this->redisPool = RedisConnectionPool::singleton( $redisConf );
00065 
00066         $this->servers = $params['servers'];
00067         if ( isset( $params['automaticFailover'] ) ) {
00068             $this->automaticFailover = $params['automaticFailover'];
00069         } else {
00070             $this->automaticFailover = true;
00071         }
00072     }
00073 
00074     public function get( $key, &$casToken = null ) {
00075         $section = new ProfileSection( __METHOD__ );
00076 
00077         list( $server, $conn ) = $this->getConnection( $key );
00078         if ( !$conn ) {
00079             return false;
00080         }
00081         try {
00082             $value = $conn->get( $key );
00083             $casToken = $value;
00084             $result = $this->unserialize( $value );
00085         } catch ( RedisException $e ) {
00086             $result = false;
00087             $this->handleException( $conn, $e );
00088         }
00089 
00090         $this->logRequest( 'get', $key, $server, $result );
00091         return $result;
00092     }
00093 
00094     public function set( $key, $value, $expiry = 0 ) {
00095         $section = new ProfileSection( __METHOD__ );
00096 
00097         list( $server, $conn ) = $this->getConnection( $key );
00098         if ( !$conn ) {
00099             return false;
00100         }
00101         $expiry = $this->convertToRelative( $expiry );
00102         try {
00103             if ( $expiry ) {
00104                 $result = $conn->setex( $key, $expiry, $this->serialize( $value ) );
00105             } else {
00106                 // No expiry, that is very different from zero expiry in Redis
00107                 $result = $conn->set( $key, $this->serialize( $value ) );
00108             }
00109         } catch ( RedisException $e ) {
00110             $result = false;
00111             $this->handleException( $conn, $e );
00112         }
00113 
00114         $this->logRequest( 'set', $key, $server, $result );
00115         return $result;
00116     }
00117 
00118     public function cas( $casToken, $key, $value, $expiry = 0 ) {
00119         $section = new ProfileSection( __METHOD__ );
00120 
00121         list( $server, $conn ) = $this->getConnection( $key );
00122         if ( !$conn ) {
00123             return false;
00124         }
00125         $expiry = $this->convertToRelative( $expiry );
00126         try {
00127             $conn->watch( $key );
00128 
00129             if ( $this->serialize( $this->get( $key ) ) !== $casToken ) {
00130                 $conn->unwatch();
00131                 return false;
00132             }
00133 
00134             // multi()/exec() will fail atomically if the key changed since watch()
00135             $conn->multi();
00136             if ( $expiry ) {
00137                 $conn->setex( $key, $expiry, $this->serialize( $value ) );
00138             } else {
00139                 // No expiry, that is very different from zero expiry in Redis
00140                 $conn->set( $key, $this->serialize( $value ) );
00141             }
00142             $result = ( $conn->exec() == array( true ) );
00143         } catch ( RedisException $e ) {
00144             $result = false;
00145             $this->handleException( $conn, $e );
00146         }
00147 
00148         $this->logRequest( 'cas', $key, $server, $result );
00149         return $result;
00150     }
00151 
00152     public function delete( $key, $time = 0 ) {
00153         $section = new ProfileSection( __METHOD__ );
00154 
00155         list( $server, $conn ) = $this->getConnection( $key );
00156         if ( !$conn ) {
00157             return false;
00158         }
00159         try {
00160             $conn->delete( $key );
00161             // Return true even if the key didn't exist
00162             $result = true;
00163         } catch ( RedisException $e ) {
00164             $result = false;
00165             $this->handleException( $conn, $e );
00166         }
00167 
00168         $this->logRequest( 'delete', $key, $server, $result );
00169         return $result;
00170     }
00171 
00172     public function getMulti( array $keys ) {
00173         $section = new ProfileSection( __METHOD__ );
00174 
00175         $batches = array();
00176         $conns = array();
00177         foreach ( $keys as $key ) {
00178             list( $server, $conn ) = $this->getConnection( $key );
00179             if ( !$conn ) {
00180                 continue;
00181             }
00182             $conns[$server] = $conn;
00183             $batches[$server][] = $key;
00184         }
00185         $result = array();
00186         foreach ( $batches as $server => $batchKeys ) {
00187             $conn = $conns[$server];
00188             try {
00189                 $conn->multi( Redis::PIPELINE );
00190                 foreach ( $batchKeys as $key ) {
00191                     $conn->get( $key );
00192                 }
00193                 $batchResult = $conn->exec();
00194                 if ( $batchResult === false ) {
00195                     $this->debug( "multi request to $server failed" );
00196                     continue;
00197                 }
00198                 foreach ( $batchResult as $i => $value ) {
00199                     if ( $value !== false ) {
00200                         $result[$batchKeys[$i]] = $this->unserialize( $value );
00201                     }
00202                 }
00203             } catch ( RedisException $e ) {
00204                 $this->handleException( $conn, $e );
00205             }
00206         }
00207 
00208         $this->debug( "getMulti for " . count( $keys ) . " keys " .
00209             "returned " . count( $result ) . " results" );
00210         return $result;
00211     }
00212 
00213     public function add( $key, $value, $expiry = 0 ) {
00214         $section = new ProfileSection( __METHOD__ );
00215 
00216         list( $server, $conn ) = $this->getConnection( $key );
00217         if ( !$conn ) {
00218             return false;
00219         }
00220         $expiry = $this->convertToRelative( $expiry );
00221         try {
00222             if ( $expiry ) {
00223                 $conn->multi();
00224                 $conn->setnx( $key, $this->serialize( $value ) );
00225                 $conn->expire( $key, $expiry );
00226                 $result = ( $conn->exec() == array( true, true ) );
00227             } else {
00228                 $result = $conn->setnx( $key, $this->serialize( $value ) );
00229             }
00230         } catch ( RedisException $e ) {
00231             $result = false;
00232             $this->handleException( $conn, $e );
00233         }
00234 
00235         $this->logRequest( 'add', $key, $server, $result );
00236         return $result;
00237     }
00238 
00248     public function incr( $key, $value = 1 ) {
00249         $section = new ProfileSection( __METHOD__ );
00250 
00251         list( $server, $conn ) = $this->getConnection( $key );
00252         if ( !$conn ) {
00253             return false;
00254         }
00255         if ( !$conn->exists( $key ) ) {
00256             return null;
00257         }
00258         try {
00259             $result = $this->unserialize( $conn->incrBy( $key, $value ) );
00260         } catch ( RedisException $e ) {
00261             $result = false;
00262             $this->handleException( $conn, $e );
00263         }
00264 
00265         $this->logRequest( 'incr', $key, $server, $result );
00266         return $result;
00267     }
00268 
00273     protected function serialize( $data ) {
00274         // Ignore digit strings and ints so INCR/DECR work
00275         return ( is_int( $data ) || ctype_digit( $data ) ) ? $data : serialize( $data );
00276     }
00277 
00282     protected function unserialize( $data ) {
00283         // Ignore digit strings and ints so INCR/DECR work
00284         return ( is_int( $data ) || ctype_digit( $data ) ) ? $data : unserialize( $data );
00285     }
00286 
00291     protected function getConnection( $key ) {
00292         if ( count( $this->servers ) === 1 ) {
00293             $candidates = $this->servers;
00294         } else {
00295             $candidates = $this->servers;
00296             ArrayUtils::consistentHashSort( $candidates, $key, '/' );
00297             if ( !$this->automaticFailover ) {
00298                 $candidates = array_slice( $candidates, 0, 1 );
00299             }
00300         }
00301 
00302         foreach ( $candidates as $server ) {
00303             $conn = $this->redisPool->getConnection( $server );
00304             if ( $conn ) {
00305                 return array( $server, $conn );
00306             }
00307         }
00308         $this->setLastError( BagOStuff::ERR_UNREACHABLE );
00309         return array( false, false );
00310     }
00311 
00315     protected function logError( $msg ) {
00316         wfDebugLog( 'redis', "Redis error: $msg" );
00317     }
00318 
00325     protected function handleException( RedisConnRef $conn, $e ) {
00326         $this->setLastError( BagOStuff::ERR_UNEXPECTED );
00327         $this->redisPool->handleError( $conn, $e );
00328     }
00329 
00333     public function logRequest( $method, $key, $server, $result ) {
00334         $this->debug( "$method $key on $server: " .
00335             ( $result === false ? "failure" : "success" ) );
00336     }
00337 }