[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Object caching using Redis (http://redis.io/). 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 */ 22 23 class RedisBagOStuff extends BagOStuff { 24 /** @var RedisConnectionPool */ 25 protected $redisPool; 26 /** @var array List of server names */ 27 protected $servers; 28 /** @var bool */ 29 protected $automaticFailover; 30 31 /** 32 * Construct a RedisBagOStuff object. Parameters are: 33 * 34 * - servers: An array of server names. A server name may be a hostname, 35 * a hostname/port combination or the absolute path of a UNIX socket. 36 * If a hostname is specified but no port, the standard port number 37 * 6379 will be used. Required. 38 * 39 * - connectTimeout: The timeout for new connections, in seconds. Optional, 40 * default is 1 second. 41 * 42 * - persistent: Set this to true to allow connections to persist across 43 * multiple web requests. False by default. 44 * 45 * - password: The authentication password, will be sent to Redis in 46 * clear text. Optional, if it is unspecified, no AUTH command will be 47 * sent. 48 * 49 * - automaticFailover: If this is false, then each key will be mapped to 50 * a single server, and if that server is down, any requests for that key 51 * will fail. If this is true, a connection failure will cause the client 52 * to immediately try the next server in the list (as determined by a 53 * consistent hashing algorithm). True by default. This has the 54 * potential to create consistency issues if a server is slow enough to 55 * flap, for example if it is in swap death. 56 * @param array $params 57 */ 58 function __construct( $params ) { 59 $redisConf = array( 'serializer' => 'none' ); // manage that in this class 60 foreach ( array( 'connectTimeout', 'persistent', 'password' ) as $opt ) { 61 if ( isset( $params[$opt] ) ) { 62 $redisConf[$opt] = $params[$opt]; 63 } 64 } 65 $this->redisPool = RedisConnectionPool::singleton( $redisConf ); 66 67 $this->servers = $params['servers']; 68 if ( isset( $params['automaticFailover'] ) ) { 69 $this->automaticFailover = $params['automaticFailover']; 70 } else { 71 $this->automaticFailover = true; 72 } 73 } 74 75 public function get( $key, &$casToken = null ) { 76 $section = new ProfileSection( __METHOD__ ); 77 78 list( $server, $conn ) = $this->getConnection( $key ); 79 if ( !$conn ) { 80 return false; 81 } 82 try { 83 $value = $conn->get( $key ); 84 $casToken = $value; 85 $result = $this->unserialize( $value ); 86 } catch ( RedisException $e ) { 87 $result = false; 88 $this->handleException( $conn, $e ); 89 } 90 91 $this->logRequest( 'get', $key, $server, $result ); 92 return $result; 93 } 94 95 public function set( $key, $value, $expiry = 0 ) { 96 $section = new ProfileSection( __METHOD__ ); 97 98 list( $server, $conn ) = $this->getConnection( $key ); 99 if ( !$conn ) { 100 return false; 101 } 102 $expiry = $this->convertToRelative( $expiry ); 103 try { 104 if ( $expiry ) { 105 $result = $conn->setex( $key, $expiry, $this->serialize( $value ) ); 106 } else { 107 // No expiry, that is very different from zero expiry in Redis 108 $result = $conn->set( $key, $this->serialize( $value ) ); 109 } 110 } catch ( RedisException $e ) { 111 $result = false; 112 $this->handleException( $conn, $e ); 113 } 114 115 $this->logRequest( 'set', $key, $server, $result ); 116 return $result; 117 } 118 119 public function cas( $casToken, $key, $value, $expiry = 0 ) { 120 $section = new ProfileSection( __METHOD__ ); 121 122 list( $server, $conn ) = $this->getConnection( $key ); 123 if ( !$conn ) { 124 return false; 125 } 126 $expiry = $this->convertToRelative( $expiry ); 127 try { 128 $conn->watch( $key ); 129 130 if ( $this->serialize( $this->get( $key ) ) !== $casToken ) { 131 $conn->unwatch(); 132 return false; 133 } 134 135 // multi()/exec() will fail atomically if the key changed since watch() 136 $conn->multi(); 137 if ( $expiry ) { 138 $conn->setex( $key, $expiry, $this->serialize( $value ) ); 139 } else { 140 // No expiry, that is very different from zero expiry in Redis 141 $conn->set( $key, $this->serialize( $value ) ); 142 } 143 $result = ( $conn->exec() == array( true ) ); 144 } catch ( RedisException $e ) { 145 $result = false; 146 $this->handleException( $conn, $e ); 147 } 148 149 $this->logRequest( 'cas', $key, $server, $result ); 150 return $result; 151 } 152 153 public function delete( $key, $time = 0 ) { 154 $section = new ProfileSection( __METHOD__ ); 155 156 list( $server, $conn ) = $this->getConnection( $key ); 157 if ( !$conn ) { 158 return false; 159 } 160 try { 161 $conn->delete( $key ); 162 // Return true even if the key didn't exist 163 $result = true; 164 } catch ( RedisException $e ) { 165 $result = false; 166 $this->handleException( $conn, $e ); 167 } 168 169 $this->logRequest( 'delete', $key, $server, $result ); 170 return $result; 171 } 172 173 public function getMulti( array $keys ) { 174 $section = new ProfileSection( __METHOD__ ); 175 176 $batches = array(); 177 $conns = array(); 178 foreach ( $keys as $key ) { 179 list( $server, $conn ) = $this->getConnection( $key ); 180 if ( !$conn ) { 181 continue; 182 } 183 $conns[$server] = $conn; 184 $batches[$server][] = $key; 185 } 186 $result = array(); 187 foreach ( $batches as $server => $batchKeys ) { 188 $conn = $conns[$server]; 189 try { 190 $conn->multi( Redis::PIPELINE ); 191 foreach ( $batchKeys as $key ) { 192 $conn->get( $key ); 193 } 194 $batchResult = $conn->exec(); 195 if ( $batchResult === false ) { 196 $this->debug( "multi request to $server failed" ); 197 continue; 198 } 199 foreach ( $batchResult as $i => $value ) { 200 if ( $value !== false ) { 201 $result[$batchKeys[$i]] = $this->unserialize( $value ); 202 } 203 } 204 } catch ( RedisException $e ) { 205 $this->handleException( $conn, $e ); 206 } 207 } 208 209 $this->debug( "getMulti for " . count( $keys ) . " keys " . 210 "returned " . count( $result ) . " results" ); 211 return $result; 212 } 213 214 /** 215 * @param array $data 216 * @param int $expiry 217 * @return bool 218 */ 219 public function setMulti( array $data, $expiry = 0 ) { 220 $section = new ProfileSection( __METHOD__ ); 221 222 $batches = array(); 223 $conns = array(); 224 foreach ( $data as $key => $value ) { 225 list( $server, $conn ) = $this->getConnection( $key ); 226 if ( !$conn ) { 227 continue; 228 } 229 $conns[$server] = $conn; 230 $batches[$server][] = $key; 231 } 232 233 $expiry = $this->convertToRelative( $expiry ); 234 $result = true; 235 foreach ( $batches as $server => $batchKeys ) { 236 $conn = $conns[$server]; 237 try { 238 $conn->multi( Redis::PIPELINE ); 239 foreach ( $batchKeys as $key ) { 240 if ( $expiry ) { 241 $conn->setex( $key, $expiry, $this->serialize( $data[$key] ) ); 242 } else { 243 $conn->set( $key, $this->serialize( $data[$key] ) ); 244 } 245 } 246 $batchResult = $conn->exec(); 247 if ( $batchResult === false ) { 248 $this->debug( "setMulti request to $server failed" ); 249 continue; 250 } 251 foreach ( $batchResult as $value ) { 252 if ( $value === false ) { 253 $result = false; 254 } 255 } 256 } catch ( RedisException $e ) { 257 $this->handleException( $server, $conn, $e ); 258 $result = false; 259 } 260 } 261 262 return $result; 263 } 264 265 266 267 public function add( $key, $value, $expiry = 0 ) { 268 $section = new ProfileSection( __METHOD__ ); 269 270 list( $server, $conn ) = $this->getConnection( $key ); 271 if ( !$conn ) { 272 return false; 273 } 274 $expiry = $this->convertToRelative( $expiry ); 275 try { 276 if ( $expiry ) { 277 $conn->multi(); 278 $conn->setnx( $key, $this->serialize( $value ) ); 279 $conn->expire( $key, $expiry ); 280 $result = ( $conn->exec() == array( true, true ) ); 281 } else { 282 $result = $conn->setnx( $key, $this->serialize( $value ) ); 283 } 284 } catch ( RedisException $e ) { 285 $result = false; 286 $this->handleException( $conn, $e ); 287 } 288 289 $this->logRequest( 'add', $key, $server, $result ); 290 return $result; 291 } 292 293 /** 294 * Non-atomic implementation of incr(). 295 * 296 * Probably all callers actually want incr() to atomically initialise 297 * values to zero if they don't exist, as provided by the Redis INCR 298 * command. But we are constrained by the memcached-like interface to 299 * return null in that case. Once the key exists, further increments are 300 * atomic. 301 * @param string $key Key to increase 302 * @param int $value Value to add to $key (Default 1) 303 * @return int|bool New value or false on failure 304 */ 305 public function incr( $key, $value = 1 ) { 306 $section = new ProfileSection( __METHOD__ ); 307 308 list( $server, $conn ) = $this->getConnection( $key ); 309 if ( !$conn ) { 310 return false; 311 } 312 if ( !$conn->exists( $key ) ) { 313 return null; 314 } 315 try { 316 $result = $conn->incrBy( $key, $value ); 317 } catch ( RedisException $e ) { 318 $result = false; 319 $this->handleException( $conn, $e ); 320 } 321 322 $this->logRequest( 'incr', $key, $server, $result ); 323 return $result; 324 } 325 /** 326 * @param mixed $data 327 * @return string 328 */ 329 protected function serialize( $data ) { 330 // Serialize anything but integers so INCR/DECR work 331 // Do not store integer-like strings as integers to avoid type confusion (bug 60563) 332 return is_int( $data ) ? $data : serialize( $data ); 333 } 334 335 /** 336 * @param string $data 337 * @return mixed 338 */ 339 protected function unserialize( $data ) { 340 return ctype_digit( $data ) ? intval( $data ) : unserialize( $data ); 341 } 342 343 /** 344 * Get a Redis object with a connection suitable for fetching the specified key 345 * @param string $key 346 * @return array (server, RedisConnRef) or (false, false) 347 */ 348 protected function getConnection( $key ) { 349 if ( count( $this->servers ) === 1 ) { 350 $candidates = $this->servers; 351 } else { 352 $candidates = $this->servers; 353 ArrayUtils::consistentHashSort( $candidates, $key, '/' ); 354 if ( !$this->automaticFailover ) { 355 $candidates = array_slice( $candidates, 0, 1 ); 356 } 357 } 358 359 foreach ( $candidates as $server ) { 360 $conn = $this->redisPool->getConnection( $server ); 361 if ( $conn ) { 362 return array( $server, $conn ); 363 } 364 } 365 $this->setLastError( BagOStuff::ERR_UNREACHABLE ); 366 return array( false, false ); 367 } 368 369 /** 370 * Log a fatal error 371 * @param string $msg 372 */ 373 protected function logError( $msg ) { 374 wfDebugLog( 'redis', "Redis error: $msg" ); 375 } 376 377 /** 378 * The redis extension throws an exception in response to various read, write 379 * and protocol errors. Sometimes it also closes the connection, sometimes 380 * not. The safest response for us is to explicitly destroy the connection 381 * object and let it be reopened during the next request. 382 * @param RedisConnRef $conn 383 * @param Exception $e 384 */ 385 protected function handleException( RedisConnRef $conn, $e ) { 386 $this->setLastError( BagOStuff::ERR_UNEXPECTED ); 387 $this->redisPool->handleError( $conn, $e ); 388 } 389 390 /** 391 * Send information about a single request to the debug log 392 * @param string $method 393 * @param string $key 394 * @param string $server 395 * @param bool $result 396 */ 397 public function logRequest( $method, $key, $server, $result ) { 398 $this->debug( "$method $key on $server: " . 399 ( $result === false ? "failure" : "success" ) ); 400 } 401 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |