24 use Psr\Log\LoggerAwareInterface;
25 use Psr\Log\LoggerInterface;
26 use Psr\Log\NullLogger;
70 private const SERVER_DOWN_TTL = 30;
83 if ( !class_exists( Redis::class ) ) {
84 throw new RuntimeException(
85 __CLASS__ .
' requires a Redis client library. ' .
86 'See https://www.mediawiki.org/wiki/Redis#Setup' );
88 $this->logger = $options[
'logger'] ??
new NullLogger();
89 $this->connectTimeout = $options[
'connectTimeout'];
90 $this->readTimeout = $options[
'readTimeout'];
91 $this->persistent = $options[
'persistent'];
92 $this->password = $options[
'password'];
93 if ( !isset( $options[
'serializer'] ) || $options[
'serializer'] ===
'php' ) {
94 $this->serializer = Redis::SERIALIZER_PHP;
95 } elseif ( $options[
'serializer'] ===
'igbinary' ) {
96 if ( !defined(
'Redis::SERIALIZER_IGBINARY' ) ) {
97 throw new InvalidArgumentException(
98 __CLASS__ .
': configured serializer "igbinary" not available' );
100 $this->serializer = Redis::SERIALIZER_IGBINARY;
101 } elseif ( $options[
'serializer'] ===
'none' ) {
102 $this->serializer = Redis::SERIALIZER_NONE;
104 throw new InvalidArgumentException(
"Invalid serializer specified." );
118 if ( !isset( $options[
'connectTimeout'] ) ) {
119 $options[
'connectTimeout'] = 1;
121 if ( !isset( $options[
'readTimeout'] ) ) {
122 $options[
'readTimeout'] = 1;
124 if ( !isset( $options[
'persistent'] ) ) {
125 $options[
'persistent'] =
false;
127 if ( !isset( $options[
'password'] ) ) {
128 $options[
'password'] =
null;
153 $id = sha1( serialize( $options ) );
155 if ( !isset( self::$instances[
$id] ) ) {
156 self::$instances[
$id] =
new self( $options,
$id );
159 return self::$instances[
$id];
167 self::$instances = [];
187 if ( isset( $this->downServers[$server] ) ) {
189 if ( $now > $this->downServers[$server] ) {
191 unset( $this->downServers[$server] );
195 'Server "{redis_server}" is marked down for another ' .
196 ( $this->downServers[$server] - $now ) .
'seconds',
197 [
'redis_server' => $server ]
205 if ( isset( $this->connections[$server] ) ) {
206 foreach ( $this->connections[$server] as &$connection ) {
207 if ( $connection[
'free'] ) {
208 $connection[
'free'] =
false;
212 $this, $server, $connection[
'conn'],
$logger
219 throw new InvalidArgumentException(
220 __CLASS__ .
": invalid configured server \"$server\"" );
221 } elseif ( substr( $server, 0, 1 ) ===
'/' ) {
229 if ( preg_match(
'/^\[(.+)\]:(\d+)$/', $server, $m ) ) {
231 [ $host, $port ] = [ $m[1], (int)$m[2] ];
232 } elseif ( preg_match(
'/^((?:[\w]+\:\/\/)?[^:]+):(\d+)$/', $server, $m ) ) {
234 [ $host, $port ] = [ $m[1], (int)$m[2] ];
236 substr( $host, 0, 6 ) ===
'tls://'
237 && version_compare( phpversion(
'redis' ),
'5.0.0' ) < 0
239 throw new RuntimeException(
240 'A newer version of the Redis client library is required to use TLS. ' .
241 'See https://www.mediawiki.org/wiki/Redis#Setup' );
245 [ $host, $port ] = [ $server, 6379 ];
251 if ( $this->persistent ) {
252 $result = $conn->pconnect( $host, $port, $this->connectTimeout, $this->
id );
254 $result = $conn->connect( $host, $port, $this->connectTimeout );
258 'Could not connect to server "{redis_server}"',
259 [
'redis_server' => $server ]
262 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
266 if ( ( $this->password !==
null ) && !$conn->auth( $this->password ) ) {
268 'Authentication error connecting to "{redis_server}"',
269 [
'redis_server' => $server ]
272 }
catch ( RedisException $e ) {
273 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
275 'Redis exception connecting to "{redis_server}"',
277 'redis_server' => $server,
285 $conn->setOption( Redis::OPT_READ_TIMEOUT, $this->readTimeout );
286 $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
287 $this->connections[$server][] = [
'conn' => $conn,
'free' => false ];
302 foreach ( $this->connections[$server] as &$connection ) {
303 if ( $connection[
'conn'] === $conn && !$connection[
'free'] ) {
304 $connection[
'free'] =
true;
319 if ( $this->idlePoolSize <= count( $this->connections ) ) {
323 foreach ( $this->connections as &$serverConnections ) {
324 foreach ( $serverConnections as $key => &$connection ) {
325 if ( $connection[
'free'] ) {
326 unset( $serverConnections[$key] );
327 if ( --$this->idlePoolSize <= count( $this->connections ) ) {
346 $this->logger->error(
347 'Redis exception on server "{redis_server}"',
349 'redis_server' => $server,
353 foreach ( $this->connections[$server] as $key => $connection ) {
355 $this->idlePoolSize -= $connection[
'free'] ? 1 : 0;
356 unset( $this->connections[$server][$key] );
379 if ( $this->password !==
null && !$conn->auth( $this->password ) ) {
380 $this->logger->error(
381 'Authentication error connecting to "{redis_server}"',
382 [
'redis_server' => $server ]
398 $conn->setOption( Redis::OPT_READ_TIMEOUT, $timeout ?: $this->readTimeout );
405 foreach ( $this->connections as &$serverConnections ) {
406 foreach ( $serverConnections as &$connection ) {
409 $conn = $connection[
'conn'];
411 }
catch ( RedisException $e ) {
Helper class to handle automatically marking connections as reusable (via RAII pattern)
isConnIdentical(Redis $conn)
Helper class to manage Redis connections.
static applyDefaultConfig(array $options)
__construct(array $options, $id)
int $serializer
Serializer to use (Redis::SERIALIZER_*)
reauthenticateConnection( $server, Redis $conn)
Re-send an AUTH request to the redis server (useful after disconnects).
array $connections
(server name => ((connection info array),...)
getConnection( $server, LoggerInterface $logger=null)
Get a connection to a redis server.
__destruct()
Make sure connections are closed.
string $id
ID for persistent connections.
closeExcessIdleConections()
Close any extra idle connections if there are more than the limit.
handleError(RedisConnRef $cref, RedisException $e)
The redis extension throws an exception in response to various read, write and protocol errors.
static destroySingletons()
Destroy all singleton() instances.
freeConnection( $server, Redis $conn)
Mark a connection to a server as free to return to the pool.
static array $instances
(pool ID => RedisConnectionPool)
resetTimeout(Redis $conn, $timeout=null)
Adjust or reset the connection handle read timeout value.
string $password
Plaintext auth password.
int $idlePoolSize
Current idle pool size.
bool $persistent
Whether connections persist.
array $downServers
(server name => UNIX timestamp)
string $readTimeout
Read timeout in seconds.
static singleton(array $options)
setLogger(LoggerInterface $logger)
int $connectTimeout
Connection timeout in seconds.