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