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