24use Psr\Log\LoggerAwareInterface;
25use Psr\Log\LoggerInterface;
26use Psr\Log\NullLogger;
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;
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 ) ) {
230 list( $host, $port ) = [ $m[1], (int)$m[2] ];
231 } elseif ( preg_match(
'/^([^:]+):(\d+)$/', $server, $m ) ) {
232 list( $host, $port ) = [ $m[1], (int)$m[2] ];
234 list( $host, $port ) = [ $server, 6379 ];
240 if ( $this->persistent ) {
241 $result = $conn->pconnect( $host, $port, $this->connectTimeout, $this->
id );
243 $result = $conn->connect( $host, $port, $this->connectTimeout );
247 'Could not connect to server "{redis_server}"',
248 [
'redis_server' => $server ]
255 if ( ( $this->password !==
null ) && !$conn->auth( $this->password ) ) {
257 'Authentication error connecting to "{redis_server}"',
258 [
'redis_server' => $server ]
261 }
catch ( RedisException $e ) {
264 'Redis exception connecting to "{redis_server}"',
266 'redis_server' => $server,
274 $conn->setOption( Redis::OPT_READ_TIMEOUT, $this->readTimeout );
275 $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
276 $this->connections[$server][] = [
'conn' => $conn,
'free' => false ];
291 foreach ( $this->connections[$server] as &$connection ) {
292 if ( $connection[
'conn'] === $conn && !$connection[
'free'] ) {
293 $connection[
'free'] =
true;
308 if ( $this->idlePoolSize <= count( $this->connections ) ) {
312 foreach ( $this->connections as &$serverConnections ) {
313 foreach ( $serverConnections as $key => &$connection ) {
314 if ( $connection[
'free'] ) {
315 unset( $serverConnections[$key] );
316 if ( --$this->idlePoolSize <= count( $this->connections ) ) {
335 $this->logger->error(
336 'Redis exception on server "{redis_server}"',
338 'redis_server' => $server,
342 foreach ( $this->connections[$server] as $key => $connection ) {
344 $this->idlePoolSize -= $connection[
'free'] ? 1 : 0;
345 unset( $this->connections[$server][$key] );
368 if ( $this->password !==
null && !$conn->auth( $this->password ) ) {
369 $this->logger->error(
370 'Authentication error connecting to "{redis_server}"',
371 [
'redis_server' => $server ]
394 foreach ( $this->connections as $server => &$serverConnections ) {
395 foreach ( $serverConnections as $key => &$connection ) {
398 $conn = $connection[
'conn'];
400 }
catch ( RedisException $e ) {
Helper class to handle automatically marking connectons as reusable (via RAII pattern)
isConnIdentical(Redis $conn)
Helper class to manage Redis connections.
static applyDefaultConfig(array $options)
string $connectTimeout
Connection timeout in seconds.
__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 for sanity.
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.
const SERVER_DOWN_TTL
integer; seconds to cache servers as "down".
array $downServers
(server name => UNIX timestamp)
string $readTimeout
Read timeout in seconds.
static singleton(array $options)
setLogger(LoggerInterface $logger)