MediaWiki  REL1_24
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 
00058     function __construct( $params ) {
00059         $redisConf = array( 'serializer' => 'none' ); // manage that in this class
00060         foreach ( array( 'connectTimeout', 'persistent', 'password' ) as $opt ) {
00061             if ( isset( $params[$opt] ) ) {
00062                 $redisConf[$opt] = $params[$opt];
00063             }
00064         }
00065         $this->redisPool = RedisConnectionPool::singleton( $redisConf );
00066 
00067         $this->servers = $params['servers'];
00068         if ( isset( $params['automaticFailover'] ) ) {
00069             $this->automaticFailover = $params['automaticFailover'];
00070         } else {
00071             $this->automaticFailover = true;
00072         }
00073     }
00074 
00075     public function get( $key, &$casToken = null ) {
00076         $section = new ProfileSection( __METHOD__ );
00077 
00078         list( $server, $conn ) = $this->getConnection( $key );
00079         if ( !$conn ) {
00080             return false;
00081         }
00082         try {
00083             $value = $conn->get( $key );
00084             $casToken = $value;
00085             $result = $this->unserialize( $value );
00086         } catch ( RedisException $e ) {
00087             $result = false;
00088             $this->handleException( $conn, $e );
00089         }
00090 
00091         $this->logRequest( 'get', $key, $server, $result );
00092         return $result;
00093     }
00094 
00095     public function set( $key, $value, $expiry = 0 ) {
00096         $section = new ProfileSection( __METHOD__ );
00097 
00098         list( $server, $conn ) = $this->getConnection( $key );
00099         if ( !$conn ) {
00100             return false;
00101         }
00102         $expiry = $this->convertToRelative( $expiry );
00103         try {
00104             if ( $expiry ) {
00105                 $result = $conn->setex( $key, $expiry, $this->serialize( $value ) );
00106             } else {
00107                 // No expiry, that is very different from zero expiry in Redis
00108                 $result = $conn->set( $key, $this->serialize( $value ) );
00109             }
00110         } catch ( RedisException $e ) {
00111             $result = false;
00112             $this->handleException( $conn, $e );
00113         }
00114 
00115         $this->logRequest( 'set', $key, $server, $result );
00116         return $result;
00117     }
00118 
00119     public function cas( $casToken, $key, $value, $expiry = 0 ) {
00120         $section = new ProfileSection( __METHOD__ );
00121 
00122         list( $server, $conn ) = $this->getConnection( $key );
00123         if ( !$conn ) {
00124             return false;
00125         }
00126         $expiry = $this->convertToRelative( $expiry );
00127         try {
00128             $conn->watch( $key );
00129 
00130             if ( $this->serialize( $this->get( $key ) ) !== $casToken ) {
00131                 $conn->unwatch();
00132                 return false;
00133             }
00134 
00135             // multi()/exec() will fail atomically if the key changed since watch()
00136             $conn->multi();
00137             if ( $expiry ) {
00138                 $conn->setex( $key, $expiry, $this->serialize( $value ) );
00139             } else {
00140                 // No expiry, that is very different from zero expiry in Redis
00141                 $conn->set( $key, $this->serialize( $value ) );
00142             }
00143             $result = ( $conn->exec() == array( true ) );
00144         } catch ( RedisException $e ) {
00145             $result = false;
00146             $this->handleException( $conn, $e );
00147         }
00148 
00149         $this->logRequest( 'cas', $key, $server, $result );
00150         return $result;
00151     }
00152 
00153     public function delete( $key, $time = 0 ) {
00154         $section = new ProfileSection( __METHOD__ );
00155 
00156         list( $server, $conn ) = $this->getConnection( $key );
00157         if ( !$conn ) {
00158             return false;
00159         }
00160         try {
00161             $conn->delete( $key );
00162             // Return true even if the key didn't exist
00163             $result = true;
00164         } catch ( RedisException $e ) {
00165             $result = false;
00166             $this->handleException( $conn, $e );
00167         }
00168 
00169         $this->logRequest( 'delete', $key, $server, $result );
00170         return $result;
00171     }
00172 
00173     public function getMulti( array $keys ) {
00174         $section = new ProfileSection( __METHOD__ );
00175 
00176         $batches = array();
00177         $conns = array();
00178         foreach ( $keys as $key ) {
00179             list( $server, $conn ) = $this->getConnection( $key );
00180             if ( !$conn ) {
00181                 continue;
00182             }
00183             $conns[$server] = $conn;
00184             $batches[$server][] = $key;
00185         }
00186         $result = array();
00187         foreach ( $batches as $server => $batchKeys ) {
00188             $conn = $conns[$server];
00189             try {
00190                 $conn->multi( Redis::PIPELINE );
00191                 foreach ( $batchKeys as $key ) {
00192                     $conn->get( $key );
00193                 }
00194                 $batchResult = $conn->exec();
00195                 if ( $batchResult === false ) {
00196                     $this->debug( "multi request to $server failed" );
00197                     continue;
00198                 }
00199                 foreach ( $batchResult as $i => $value ) {
00200                     if ( $value !== false ) {
00201                         $result[$batchKeys[$i]] = $this->unserialize( $value );
00202                     }
00203                 }
00204             } catch ( RedisException $e ) {
00205                 $this->handleException( $conn, $e );
00206             }
00207         }
00208 
00209         $this->debug( "getMulti for " . count( $keys ) . " keys " .
00210             "returned " . count( $result ) . " results" );
00211         return $result;
00212     }
00213 
00219     public function setMulti( array $data, $expiry = 0 ) {
00220         $section = new ProfileSection( __METHOD__ );
00221 
00222         $batches = array();
00223         $conns = array();
00224         foreach ( $data as $key => $value ) {
00225             list( $server, $conn ) = $this->getConnection( $key );
00226             if ( !$conn ) {
00227                 continue;
00228             }
00229             $conns[$server] = $conn;
00230             $batches[$server][] = $key;
00231         }
00232 
00233         $expiry = $this->convertToRelative( $expiry );
00234         $result = true;
00235         foreach ( $batches as $server => $batchKeys ) {
00236             $conn = $conns[$server];
00237             try {
00238                 $conn->multi( Redis::PIPELINE );
00239                 foreach ( $batchKeys as $key ) {
00240                     if ( $expiry ) {
00241                         $conn->setex( $key, $expiry, $this->serialize( $data[$key] ) );
00242                     } else {
00243                         $conn->set( $key, $this->serialize( $data[$key] ) );
00244                     }
00245                 }
00246                 $batchResult = $conn->exec();
00247                 if ( $batchResult === false ) {
00248                     $this->debug( "setMulti request to $server failed" );
00249                     continue;
00250                 }
00251                 foreach ( $batchResult as $value ) {
00252                     if ( $value === false ) {
00253                         $result = false;
00254                     }
00255                 }
00256             } catch ( RedisException $e ) {
00257                 $this->handleException( $server, $conn, $e );
00258                 $result = false;
00259             }
00260         }
00261 
00262         return $result;
00263     }
00264 
00265 
00266 
00267     public function add( $key, $value, $expiry = 0 ) {
00268         $section = new ProfileSection( __METHOD__ );
00269 
00270         list( $server, $conn ) = $this->getConnection( $key );
00271         if ( !$conn ) {
00272             return false;
00273         }
00274         $expiry = $this->convertToRelative( $expiry );
00275         try {
00276             if ( $expiry ) {
00277                 $conn->multi();
00278                 $conn->setnx( $key, $this->serialize( $value ) );
00279                 $conn->expire( $key, $expiry );
00280                 $result = ( $conn->exec() == array( true, true ) );
00281             } else {
00282                 $result = $conn->setnx( $key, $this->serialize( $value ) );
00283             }
00284         } catch ( RedisException $e ) {
00285             $result = false;
00286             $this->handleException( $conn, $e );
00287         }
00288 
00289         $this->logRequest( 'add', $key, $server, $result );
00290         return $result;
00291     }
00292 
00305     public function incr( $key, $value = 1 ) {
00306         $section = new ProfileSection( __METHOD__ );
00307 
00308         list( $server, $conn ) = $this->getConnection( $key );
00309         if ( !$conn ) {
00310             return false;
00311         }
00312         if ( !$conn->exists( $key ) ) {
00313             return null;
00314         }
00315         try {
00316             $result = $conn->incrBy( $key, $value );
00317         } catch ( RedisException $e ) {
00318             $result = false;
00319             $this->handleException( $conn, $e );
00320         }
00321 
00322         $this->logRequest( 'incr', $key, $server, $result );
00323         return $result;
00324     }
00329     protected function serialize( $data ) {
00330         // Serialize anything but integers so INCR/DECR work
00331         // Do not store integer-like strings as integers to avoid type confusion (bug 60563)
00332         return is_int( $data ) ? $data : serialize( $data );
00333     }
00334 
00339     protected function unserialize( $data ) {
00340         return ctype_digit( $data ) ? intval( $data ) : unserialize( $data );
00341     }
00342 
00348     protected function getConnection( $key ) {
00349         if ( count( $this->servers ) === 1 ) {
00350             $candidates = $this->servers;
00351         } else {
00352             $candidates = $this->servers;
00353             ArrayUtils::consistentHashSort( $candidates, $key, '/' );
00354             if ( !$this->automaticFailover ) {
00355                 $candidates = array_slice( $candidates, 0, 1 );
00356             }
00357         }
00358 
00359         foreach ( $candidates as $server ) {
00360             $conn = $this->redisPool->getConnection( $server );
00361             if ( $conn ) {
00362                 return array( $server, $conn );
00363             }
00364         }
00365         $this->setLastError( BagOStuff::ERR_UNREACHABLE );
00366         return array( false, false );
00367     }
00368 
00373     protected function logError( $msg ) {
00374         wfDebugLog( 'redis', "Redis error: $msg" );
00375     }
00376 
00385     protected function handleException( RedisConnRef $conn, $e ) {
00386         $this->setLastError( BagOStuff::ERR_UNEXPECTED );
00387         $this->redisPool->handleError( $conn, $e );
00388     }
00389 
00397     public function logRequest( $method, $key, $server, $result ) {
00398         $this->debug( "$method $key on $server: " .
00399             ( $result === false ? "failure" : "success" ) );
00400     }
00401 }