MediaWiki  REL1_22
RedisConnectionPool.php
Go to the documentation of this file.
00001 <?php
00038 class RedisConnectionPool {
00046     protected $connectTimeout;
00048     protected $password;
00050     protected $persistent;
00052     protected $serializer;
00056     protected $idlePoolSize = 0;
00057 
00059     protected $connections = array();
00061     protected $downServers = array();
00062 
00064     protected static $instances = array();
00065 
00067     const SERVER_DOWN_TTL = 30;
00068 
00072     protected function __construct( array $options ) {
00073         if ( !class_exists( 'Redis' ) ) {
00074             throw new MWException( __CLASS__ . ' requires a Redis client library. ' .
00075                 'See https://www.mediawiki.org/wiki/Redis#Setup' );
00076         }
00077         $this->connectTimeout = $options['connectTimeout'];
00078         $this->persistent = $options['persistent'];
00079         $this->password = $options['password'];
00080         if ( !isset( $options['serializer'] ) || $options['serializer'] === 'php' ) {
00081             $this->serializer = Redis::SERIALIZER_PHP;
00082         } elseif ( $options['serializer'] === 'igbinary' ) {
00083             $this->serializer = Redis::SERIALIZER_IGBINARY;
00084         } elseif ( $options['serializer'] === 'none' ) {
00085             $this->serializer = Redis::SERIALIZER_NONE;
00086         } else {
00087             throw new MWException( "Invalid serializer specified." );
00088         }
00089     }
00090 
00095     protected static function applyDefaultConfig( array $options ) {
00096         if ( !isset( $options['connectTimeout'] ) ) {
00097             $options['connectTimeout'] = 1;
00098         }
00099         if ( !isset( $options['persistent'] ) ) {
00100             $options['persistent'] = false;
00101         }
00102         if ( !isset( $options['password'] ) ) {
00103             $options['password'] = null;
00104         }
00105         return $options;
00106     }
00107 
00120     public static function singleton( array $options ) {
00121         $options = self::applyDefaultConfig( $options );
00122         // Map the options to a unique hash...
00123         ksort( $options ); // normalize to avoid pool fragmentation
00124         $id = sha1( serialize( $options ) );
00125         // Initialize the object at the hash as needed...
00126         if ( !isset( self::$instances[$id] ) ) {
00127             self::$instances[$id] = new self( $options );
00128             wfDebug( "Creating a new " . __CLASS__ . " instance with id $id." );
00129         }
00130         return self::$instances[$id];
00131     }
00132 
00141     public function getConnection( $server ) {
00142         // Check the listing "dead" servers which have had a connection errors.
00143         // Servers are marked dead for a limited period of time, to
00144         // avoid excessive overhead from repeated connection timeouts.
00145         if ( isset( $this->downServers[$server] ) ) {
00146             $now = time();
00147             if ( $now > $this->downServers[$server] ) {
00148                 // Dead time expired
00149                 unset( $this->downServers[$server] );
00150             } else {
00151                 // Server is dead
00152                 wfDebug( "server $server is marked down for another " .
00153                     ( $this->downServers[$server] - $now ) . " seconds, can't get connection" );
00154                 return false;
00155             }
00156         }
00157 
00158         // Check if a connection is already free for use
00159         if ( isset( $this->connections[$server] ) ) {
00160             foreach ( $this->connections[$server] as &$connection ) {
00161                 if ( $connection['free'] ) {
00162                     $connection['free'] = false;
00163                     --$this->idlePoolSize;
00164                     return new RedisConnRef( $this, $server, $connection['conn'] );
00165                 }
00166             }
00167         }
00168 
00169         if ( substr( $server, 0, 1 ) === '/' ) {
00170             // UNIX domain socket
00171             // These are required by the redis extension to start with a slash, but
00172             // we still need to set the port to a special value to make it work.
00173             $host = $server;
00174             $port = 0;
00175         } else {
00176             // TCP connection
00177             $hostPort = IP::splitHostAndPort( $server );
00178             if ( !$hostPort ) {
00179                 throw new MWException( __CLASS__ . ": invalid configured server \"$server\"" );
00180             }
00181             list( $host, $port ) = $hostPort;
00182             if ( $port === false ) {
00183                 $port = 6379;
00184             }
00185         }
00186 
00187         $conn = new Redis();
00188         try {
00189             if ( $this->persistent ) {
00190                 $result = $conn->pconnect( $host, $port, $this->connectTimeout );
00191             } else {
00192                 $result = $conn->connect( $host, $port, $this->connectTimeout );
00193             }
00194             if ( !$result ) {
00195                 wfDebugLog( 'redis', "Could not connect to server $server" );
00196                 // Mark server down for some time to avoid further timeouts
00197                 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
00198                 return false;
00199             }
00200             if ( $this->password !== null ) {
00201                 if ( !$conn->auth( $this->password ) ) {
00202                     wfDebugLog( 'redis', "Authentication error connecting to $server" );
00203                 }
00204             }
00205         } catch ( RedisException $e ) {
00206             $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
00207             wfDebugLog( 'redis', "Redis exception: " . $e->getMessage() . "\n" );
00208             return false;
00209         }
00210 
00211         if ( $conn ) {
00212             $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
00213             $this->connections[$server][] = array( 'conn' => $conn, 'free' => false );
00214             return new RedisConnRef( $this, $server, $conn );
00215         } else {
00216             return false;
00217         }
00218     }
00219 
00227     public function freeConnection( $server, Redis $conn ) {
00228         $found = false;
00229 
00230         foreach ( $this->connections[$server] as &$connection ) {
00231             if ( $connection['conn'] === $conn && !$connection['free'] ) {
00232                 $connection['free'] = true;
00233                 ++$this->idlePoolSize;
00234                 break;
00235             }
00236         }
00237 
00238         $this->closeExcessIdleConections();
00239 
00240         return $found;
00241     }
00242 
00248     protected function closeExcessIdleConections() {
00249         if ( $this->idlePoolSize <= count( $this->connections ) ) {
00250             return; // nothing to do (no more connections than servers)
00251         }
00252 
00253         foreach ( $this->connections as $server => &$serverConnections ) {
00254             foreach ( $serverConnections as $key => &$connection ) {
00255                 if ( $connection['free'] ) {
00256                     unset( $serverConnections[$key] );
00257                     if ( --$this->idlePoolSize <= count( $this->connections ) ) {
00258                         return; // done (no more connections than servers)
00259                     }
00260                 }
00261             }
00262         }
00263     }
00264 
00276     public function handleException( $server, RedisConnRef $cref, RedisException $e ) {
00277         wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() . "\n" );
00278         foreach ( $this->connections[$server] as $key => $connection ) {
00279             if ( $cref->isConnIdentical( $connection['conn'] ) ) {
00280                 $this->idlePoolSize -= $connection['free'] ? 1 : 0;
00281                 unset( $this->connections[$server][$key] );
00282                 break;
00283             }
00284         }
00285     }
00286 }
00287 
00294 class RedisConnRef {
00296     protected $pool;
00298     protected $conn;
00299 
00300     protected $server; // string
00301 
00307     public function __construct( RedisConnectionPool $pool, $server, Redis $conn ) {
00308         $this->pool = $pool;
00309         $this->server = $server;
00310         $this->conn = $conn;
00311     }
00312 
00313     public function __call( $name, $arguments ) {
00314         return call_user_func_array( array( $this->conn, $name ), $arguments );
00315     }
00316 
00324     public function luaEval( $script, array $params, $numKeys ) {
00325         $sha1 = sha1( $script ); // 40 char hex
00326         $conn = $this->conn; // convenience
00327 
00328         // Try to run the server-side cached copy of the script
00329         $conn->clearLastError();
00330         $res = $conn->evalSha( $sha1, $params, $numKeys );
00331         // If the script is not in cache, use eval() to retry and cache it
00332         if ( preg_match( '/^NOSCRIPT/', $conn->getLastError() ) ) {
00333             $conn->clearLastError();
00334             $res = $conn->eval( $script, $params, $numKeys );
00335             wfDebugLog( 'redis', "Used eval() for Lua script $sha1." );
00336         }
00337 
00338         if ( $conn->getLastError() ) { // script bug?
00339             wfDebugLog( 'redis', "Lua script error: " . $conn->getLastError() );
00340         }
00341 
00342         return $res;
00343     }
00344 
00349     public function isConnIdentical( Redis $conn ) {
00350         return $this->conn === $conn;
00351     }
00352 
00353     function __destruct() {
00354         $this->pool->freeConnection( $this->server, $this->conn );
00355     }
00356 }