MediaWiki  REL1_23
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 
00073     protected function __construct( array $options ) {
00074         if ( !class_exists( 'Redis' ) ) {
00075             throw new MWException( __CLASS__ . ' requires a Redis client library. ' .
00076                 'See https://www.mediawiki.org/wiki/Redis#Setup' );
00077         }
00078         $this->connectTimeout = $options['connectTimeout'];
00079         $this->persistent = $options['persistent'];
00080         $this->password = $options['password'];
00081         if ( !isset( $options['serializer'] ) || $options['serializer'] === 'php' ) {
00082             $this->serializer = Redis::SERIALIZER_PHP;
00083         } elseif ( $options['serializer'] === 'igbinary' ) {
00084             $this->serializer = Redis::SERIALIZER_IGBINARY;
00085         } elseif ( $options['serializer'] === 'none' ) {
00086             $this->serializer = Redis::SERIALIZER_NONE;
00087         } else {
00088             throw new MWException( "Invalid serializer specified." );
00089         }
00090     }
00091 
00096     protected static function applyDefaultConfig( array $options ) {
00097         if ( !isset( $options['connectTimeout'] ) ) {
00098             $options['connectTimeout'] = 1;
00099         }
00100         if ( !isset( $options['persistent'] ) ) {
00101             $options['persistent'] = false;
00102         }
00103         if ( !isset( $options['password'] ) ) {
00104             $options['password'] = null;
00105         }
00106 
00107         return $options;
00108     }
00109 
00122     public static function singleton( array $options ) {
00123         $options = self::applyDefaultConfig( $options );
00124         // Map the options to a unique hash...
00125         ksort( $options ); // normalize to avoid pool fragmentation
00126         $id = sha1( serialize( $options ) );
00127         // Initialize the object at the hash as needed...
00128         if ( !isset( self::$instances[$id] ) ) {
00129             self::$instances[$id] = new self( $options );
00130             wfDebug( "Creating a new " . __CLASS__ . " instance with id $id.\n" );
00131         }
00132 
00133         return self::$instances[$id];
00134     }
00135 
00144     public function getConnection( $server ) {
00145         // Check the listing "dead" servers which have had a connection errors.
00146         // Servers are marked dead for a limited period of time, to
00147         // avoid excessive overhead from repeated connection timeouts.
00148         if ( isset( $this->downServers[$server] ) ) {
00149             $now = time();
00150             if ( $now > $this->downServers[$server] ) {
00151                 // Dead time expired
00152                 unset( $this->downServers[$server] );
00153             } else {
00154                 // Server is dead
00155                 wfDebug( "server $server is marked down for another " .
00156                     ( $this->downServers[$server] - $now ) . " seconds, can't get connection\n" );
00157 
00158                 return false;
00159             }
00160         }
00161 
00162         // Check if a connection is already free for use
00163         if ( isset( $this->connections[$server] ) ) {
00164             foreach ( $this->connections[$server] as &$connection ) {
00165                 if ( $connection['free'] ) {
00166                     $connection['free'] = false;
00167                     --$this->idlePoolSize;
00168 
00169                     return new RedisConnRef( $this, $server, $connection['conn'] );
00170                 }
00171             }
00172         }
00173 
00174         if ( substr( $server, 0, 1 ) === '/' ) {
00175             // UNIX domain socket
00176             // These are required by the redis extension to start with a slash, but
00177             // we still need to set the port to a special value to make it work.
00178             $host = $server;
00179             $port = 0;
00180         } else {
00181             // TCP connection
00182             $hostPort = IP::splitHostAndPort( $server );
00183             if ( !$hostPort ) {
00184                 throw new MWException( __CLASS__ . ": invalid configured server \"$server\"" );
00185             }
00186             list( $host, $port ) = $hostPort;
00187             if ( $port === false ) {
00188                 $port = 6379;
00189             }
00190         }
00191 
00192         $conn = new Redis();
00193         try {
00194             if ( $this->persistent ) {
00195                 $result = $conn->pconnect( $host, $port, $this->connectTimeout );
00196             } else {
00197                 $result = $conn->connect( $host, $port, $this->connectTimeout );
00198             }
00199             if ( !$result ) {
00200                 wfDebugLog( 'redis', "Could not connect to server $server" );
00201                 // Mark server down for some time to avoid further timeouts
00202                 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
00203 
00204                 return false;
00205             }
00206             if ( $this->password !== null ) {
00207                 if ( !$conn->auth( $this->password ) ) {
00208                     wfDebugLog( 'redis', "Authentication error connecting to $server" );
00209                 }
00210             }
00211         } catch ( RedisException $e ) {
00212             $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
00213             wfDebugLog( 'redis', "Redis exception connecting to $server: " . $e->getMessage() );
00214 
00215             return false;
00216         }
00217 
00218         if ( $conn ) {
00219             $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
00220             $this->connections[$server][] = array( 'conn' => $conn, 'free' => false );
00221 
00222             return new RedisConnRef( $this, $server, $conn );
00223         } else {
00224             return false;
00225         }
00226     }
00227 
00235     public function freeConnection( $server, Redis $conn ) {
00236         $found = false;
00237 
00238         foreach ( $this->connections[$server] as &$connection ) {
00239             if ( $connection['conn'] === $conn && !$connection['free'] ) {
00240                 $connection['free'] = true;
00241                 ++$this->idlePoolSize;
00242                 break;
00243             }
00244         }
00245 
00246         $this->closeExcessIdleConections();
00247 
00248         return $found;
00249     }
00250 
00254     protected function closeExcessIdleConections() {
00255         if ( $this->idlePoolSize <= count( $this->connections ) ) {
00256             return; // nothing to do (no more connections than servers)
00257         }
00258 
00259         foreach ( $this->connections as &$serverConnections ) {
00260             foreach ( $serverConnections as $key => &$connection ) {
00261                 if ( $connection['free'] ) {
00262                     unset( $serverConnections[$key] );
00263                     if ( --$this->idlePoolSize <= count( $this->connections ) ) {
00264                         return; // done (no more connections than servers)
00265                     }
00266                 }
00267             }
00268         }
00269     }
00270 
00282     public function handleException( $server, RedisConnRef $cref, RedisException $e ) {
00283         return $this->handleError( $cref, $e );
00284     }
00285 
00295     public function handleError( RedisConnRef $cref, RedisException $e ) {
00296         $server = $cref->getServer();
00297         wfDebugLog( 'redis', "Redis exception on server $server: " . $e->getMessage() . "\n" );
00298         foreach ( $this->connections[$server] as $key => $connection ) {
00299             if ( $cref->isConnIdentical( $connection['conn'] ) ) {
00300                 $this->idlePoolSize -= $connection['free'] ? 1 : 0;
00301                 unset( $this->connections[$server][$key] );
00302                 break;
00303             }
00304         }
00305     }
00306 
00323     public function reauthenticateConnection( $server, Redis $conn ) {
00324         if ( $this->password !== null ) {
00325             if ( !$conn->auth( $this->password ) ) {
00326                 wfDebugLog( 'redis', "Authentication error connecting to $server" );
00327 
00328                 return false;
00329             }
00330         }
00331 
00332         return true;
00333     }
00334 
00338     function __destruct() {
00339         foreach ( $this->connections as $server => &$serverConnections ) {
00340             foreach ( $serverConnections as $key => &$connection ) {
00341                 $connection['conn']->close();
00342             }
00343         }
00344     }
00345 }
00346 
00355 class RedisConnRef {
00357     protected $pool;
00359     protected $conn;
00360 
00361     protected $server; // string
00362     protected $lastError; // string
00363 
00369     public function __construct( RedisConnectionPool $pool, $server, Redis $conn ) {
00370         $this->pool = $pool;
00371         $this->server = $server;
00372         $this->conn = $conn;
00373     }
00374 
00379     public function getServer() {
00380         return $this->server;
00381     }
00382 
00383     public function getLastError() {
00384         return $this->lastError;
00385     }
00386 
00387     public function clearLastError() {
00388         $this->lastError = null;
00389     }
00390 
00391     public function __call( $name, $arguments ) {
00392         $conn = $this->conn; // convenience
00393 
00394         $conn->clearLastError();
00395         $res = call_user_func_array( array( $conn, $name ), $arguments );
00396         if ( preg_match( '/^ERR operation not permitted\b/', $conn->getLastError() ) ) {
00397             $this->pool->reauthenticateConnection( $this->server, $conn );
00398             $conn->clearLastError();
00399             $res = call_user_func_array( array( $conn, $name ), $arguments );
00400             wfDebugLog( 'redis', "Used automatic re-authentication for method '$name'." );
00401         }
00402 
00403         $this->lastError = $conn->getLastError() ?: $this->lastError;
00404 
00405         return $res;
00406     }
00407 
00415     public function luaEval( $script, array $params, $numKeys ) {
00416         $sha1 = sha1( $script ); // 40 char hex
00417         $conn = $this->conn; // convenience
00418         $server = $this->server; // convenience
00419 
00420         // Try to run the server-side cached copy of the script
00421         $conn->clearLastError();
00422         $res = $conn->evalSha( $sha1, $params, $numKeys );
00423         // If we got a permission error reply that means that (a) we are not in
00424         // multi()/pipeline() and (b) some connection problem likely occurred. If
00425         // the password the client gave was just wrong, an exception should have
00426         // been thrown back in getConnection() previously.
00427         if ( preg_match( '/^ERR operation not permitted\b/', $conn->getLastError() ) ) {
00428             $this->pool->reauthenticateConnection( $server, $conn );
00429             $conn->clearLastError();
00430             $res = $conn->eval( $script, $params, $numKeys );
00431             wfDebugLog( 'redis', "Used automatic re-authentication for Lua script $sha1." );
00432         }
00433         // If the script is not in cache, use eval() to retry and cache it
00434         if ( preg_match( '/^NOSCRIPT/', $conn->getLastError() ) ) {
00435             $conn->clearLastError();
00436             $res = $conn->eval( $script, $params, $numKeys );
00437             wfDebugLog( 'redis', "Used eval() for Lua script $sha1." );
00438         }
00439 
00440         if ( $conn->getLastError() ) { // script bug?
00441             wfDebugLog( 'redis', "Lua script error on server $server: " . $conn->getLastError() );
00442         }
00443 
00444         $this->lastError = $conn->getLastError() ?: $this->lastError;
00445 
00446         return $res;
00447     }
00448 
00453     public function isConnIdentical( Redis $conn ) {
00454         return $this->conn === $conn;
00455     }
00456 
00457     function __destruct() {
00458         $this->pool->freeConnection( $this->server, $this->conn );
00459     }
00460 }