MediaWiki
REL1_23
|
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 }