MediaWiki REL1_35
RedisConnectionPool.php
Go to the documentation of this file.
1<?php
24use Psr\Log\LoggerAwareInterface;
25use Psr\Log\LoggerInterface;
26use Psr\Log\NullLogger;
27
41class RedisConnectionPool implements LoggerAwareInterface {
43 protected $connectTimeout;
45 protected $readTimeout;
47 protected $password;
49 protected $persistent;
51 protected $serializer;
53 protected $id;
54
56 protected $idlePoolSize = 0;
57
62 protected $connections = [];
64 protected $downServers = [];
65
67 protected static $instances = [];
68
70 private const SERVER_DOWN_TTL = 30;
71
75 protected $logger;
76
82 protected function __construct( array $options, $id ) {
83 if ( !class_exists( 'Redis' ) ) {
84 throw new RuntimeException(
85 __CLASS__ . ' requires a Redis client library. ' .
86 'See https://www.mediawiki.org/wiki/Redis#Setup' );
87 }
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' );
99 }
100 $this->serializer = Redis::SERIALIZER_IGBINARY;
101 } elseif ( $options['serializer'] === 'none' ) {
102 $this->serializer = Redis::SERIALIZER_NONE;
103 } else {
104 throw new InvalidArgumentException( "Invalid serializer specified." );
105 }
106 $this->id = $id;
107 }
108
109 public function setLogger( LoggerInterface $logger ) {
110 $this->logger = $logger;
111 }
112
117 protected static function applyDefaultConfig( array $options ) {
118 if ( !isset( $options['connectTimeout'] ) ) {
119 $options['connectTimeout'] = 1;
120 }
121 if ( !isset( $options['readTimeout'] ) ) {
122 $options['readTimeout'] = 1;
123 }
124 if ( !isset( $options['persistent'] ) ) {
125 $options['persistent'] = false;
126 }
127 if ( !isset( $options['password'] ) ) {
128 $options['password'] = null;
129 }
130
131 return $options;
132 }
133
149 public static function singleton( array $options ) {
150 $options = self::applyDefaultConfig( $options );
151 // Map the options to a unique hash...
152 ksort( $options ); // normalize to avoid pool fragmentation
153 $id = sha1( serialize( $options ) );
154 // Initialize the object at the hash as needed...
155 if ( !isset( self::$instances[$id] ) ) {
156 self::$instances[$id] = new self( $options, $id );
157 }
158
159 return self::$instances[$id];
160 }
161
166 public static function destroySingletons() {
167 self::$instances = [];
168 }
169
179 public function getConnection( $server, LoggerInterface $logger = null ) {
180 // The above @return also documents 'Redis' for convenience with IDEs.
181 // RedisConnRef uses PHP magic methods, which wouldn't be recognised.
182
184 // Check the listing "dead" servers which have had a connection errors.
185 // Servers are marked dead for a limited period of time, to
186 // avoid excessive overhead from repeated connection timeouts.
187 if ( isset( $this->downServers[$server] ) ) {
188 $now = time();
189 if ( $now > $this->downServers[$server] ) {
190 // Dead time expired
191 unset( $this->downServers[$server] );
192 } else {
193 // Server is dead
194 $logger->debug(
195 'Server "{redis_server}" is marked down for another ' .
196 ( $this->downServers[$server] - $now ) . 'seconds',
197 [ 'redis_server' => $server ]
198 );
199
200 return false;
201 }
202 }
203
204 // Check if a connection is already free for use
205 if ( isset( $this->connections[$server] ) ) {
206 foreach ( $this->connections[$server] as &$connection ) {
207 if ( $connection['free'] ) {
208 $connection['free'] = false;
210
211 return new RedisConnRef(
212 $this, $server, $connection['conn'], $logger
213 );
214 }
215 }
216 }
217
218 if ( !$server ) {
219 throw new InvalidArgumentException(
220 __CLASS__ . ": invalid configured server \"$server\"" );
221 } elseif ( substr( $server, 0, 1 ) === '/' ) {
222 // UNIX domain socket
223 // These are required by the redis extension to start with a slash, but
224 // we still need to set the port to a special value to make it work.
225 $host = $server;
226 $port = 0;
227 } else {
228 // TCP connection
229 if ( preg_match( '/^\[(.+)\]:(\d+)$/', $server, $m ) ) {
230 list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip, port)
231 } elseif ( preg_match( '/^([^:]+):(\d+)$/', $server, $m ) ) {
232 list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip or path, port)
233 } else {
234 list( $host, $port ) = [ $server, 6379 ]; // (ip or path, port)
235 }
236 }
237
238 $conn = new Redis();
239 try {
240 if ( $this->persistent ) {
241 $result = $conn->pconnect( $host, $port, $this->connectTimeout, $this->id );
242 } else {
243 $result = $conn->connect( $host, $port, $this->connectTimeout );
244 }
245 if ( !$result ) {
246 $logger->error(
247 'Could not connect to server "{redis_server}"',
248 [ 'redis_server' => $server ]
249 );
250 // Mark server down for some time to avoid further timeouts
251 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
252
253 return false;
254 }
255 if ( ( $this->password !== null ) && !$conn->auth( $this->password ) ) {
256 $logger->error(
257 'Authentication error connecting to "{redis_server}"',
258 [ 'redis_server' => $server ]
259 );
260 }
261 } catch ( RedisException $e ) {
262 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
263 $logger->error(
264 'Redis exception connecting to "{redis_server}"',
265 [
266 'redis_server' => $server,
267 'exception' => $e,
268 ]
269 );
270
271 return false;
272 }
273
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 ];
277
278 return new RedisConnRef( $this, $server, $conn, $logger );
279 }
280
288 public function freeConnection( $server, Redis $conn ) {
289 $found = false;
290
291 foreach ( $this->connections[$server] as &$connection ) {
292 if ( $connection['conn'] === $conn && !$connection['free'] ) {
293 $connection['free'] = true;
295 break;
296 }
297 }
298
300
301 return $found;
302 }
303
307 protected function closeExcessIdleConections() {
308 if ( $this->idlePoolSize <= count( $this->connections ) ) {
309 return; // nothing to do (no more connections than servers)
310 }
311
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 ) ) {
317 return; // done (no more connections than servers)
318 }
319 }
320 }
321 }
322 }
323
333 public function handleError( RedisConnRef $cref, RedisException $e ) {
334 $server = $cref->getServer();
335 $this->logger->error(
336 'Redis exception on server "{redis_server}"',
337 [
338 'redis_server' => $server,
339 'exception' => $e,
340 ]
341 );
342 foreach ( $this->connections[$server] as $key => $connection ) {
343 if ( $cref->isConnIdentical( $connection['conn'] ) ) {
344 $this->idlePoolSize -= $connection['free'] ? 1 : 0;
345 unset( $this->connections[$server][$key] );
346 break;
347 }
348 }
349 }
350
367 public function reauthenticateConnection( $server, Redis $conn ) {
368 if ( $this->password !== null && !$conn->auth( $this->password ) ) {
369 $this->logger->error(
370 'Authentication error connecting to "{redis_server}"',
371 [ 'redis_server' => $server ]
372 );
373
374 return false;
375 }
376
377 return true;
378 }
379
386 public function resetTimeout( Redis $conn, $timeout = null ) {
387 $conn->setOption( Redis::OPT_READ_TIMEOUT, $timeout ?: $this->readTimeout );
388 }
389
393 public function __destruct() {
394 foreach ( $this->connections as $server => &$serverConnections ) {
395 foreach ( $serverConnections as $key => &$connection ) {
396 try {
398 $conn = $connection['conn'];
399 $conn->close();
400 } catch ( RedisException $e ) {
401 // The destructor can be called on shutdown when random parts of the system
402 // have been destructed already, causing weird errors. Ignore them.
403 }
404 }
405 }
406 }
407}
serialize()
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),...) -var array<string,array{conn:Redis,free:bool}[]>
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)