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