[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/objectcache/ -> RedisBagOStuff.php (source)

   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  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1