MediaWiki master
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::class ) ) {
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
183 $logger = $logger ?: $this->logger;
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;
209 --$this->idlePoolSize;
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 // (ip, port)
231 [ $host, $port ] = [ $m[1], (int)$m[2] ];
232 } elseif ( preg_match( '/^((?:[\w]+\:\/\/)?[^:]+):(\d+)$/', $server, $m ) ) {
233 // (ip, uri or path, port)
234 [ $host, $port ] = [ $m[1], (int)$m[2] ];
235 if (
236 substr( $host, 0, 6 ) === 'tls://'
237 && version_compare( phpversion( 'redis' ), '5.0.0' ) < 0
238 ) {
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' );
242 }
243 } else {
244 // (ip or path, port)
245 [ $host, $port ] = [ $server, 6379 ];
246 }
247 }
248
249 $conn = new Redis();
250 try {
251 if ( $this->persistent ) {
252 $result = $conn->pconnect( $host, $port, $this->connectTimeout, $this->id );
253 } else {
254 $result = $conn->connect( $host, $port, $this->connectTimeout );
255 }
256 if ( !$result ) {
257 $logger->error(
258 'Could not connect to server "{redis_server}"',
259 [ 'redis_server' => $server ]
260 );
261 // Mark server down for some time to avoid further timeouts
262 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
263
264 return false;
265 }
266 if ( ( $this->password !== null ) && !$conn->auth( $this->password ) ) {
267 $logger->error(
268 'Authentication error connecting to "{redis_server}"',
269 [ 'redis_server' => $server ]
270 );
271 }
272 } catch ( RedisException $e ) {
273 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
274 $logger->error(
275 'Redis exception connecting to "{redis_server}"',
276 [
277 'redis_server' => $server,
278 'exception' => $e,
279 ]
280 );
281
282 return false;
283 }
284
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 ];
288
289 return new RedisConnRef( $this, $server, $conn, $logger );
290 }
291
299 public function freeConnection( $server, Redis $conn ) {
300 $found = false;
301
302 foreach ( $this->connections[$server] as &$connection ) {
303 if ( $connection['conn'] === $conn && !$connection['free'] ) {
304 $connection['free'] = true;
305 ++$this->idlePoolSize;
306 break;
307 }
308 }
309
311
312 return $found;
313 }
314
318 protected function closeExcessIdleConections() {
319 if ( $this->idlePoolSize <= count( $this->connections ) ) {
320 return; // nothing to do (no more connections than servers)
321 }
322
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 ) ) {
328 return; // done (no more connections than servers)
329 }
330 }
331 }
332 }
333 }
334
344 public function handleError( RedisConnRef $cref, RedisException $e ) {
345 $server = $cref->getServer();
346 $this->logger->error(
347 'Redis exception on server "{redis_server}"',
348 [
349 'redis_server' => $server,
350 'exception' => $e,
351 ]
352 );
353 foreach ( $this->connections[$server] as $key => $connection ) {
354 if ( $cref->isConnIdentical( $connection['conn'] ) ) {
355 $this->idlePoolSize -= $connection['free'] ? 1 : 0;
356 unset( $this->connections[$server][$key] );
357 break;
358 }
359 }
360 }
361
378 public function reauthenticateConnection( $server, Redis $conn ) {
379 if ( $this->password !== null && !$conn->auth( $this->password ) ) {
380 $this->logger->error(
381 'Authentication error connecting to "{redis_server}"',
382 [ 'redis_server' => $server ]
383 );
384
385 return false;
386 }
387
388 return true;
389 }
390
397 public function resetTimeout( Redis $conn, $timeout = null ) {
398 $conn->setOption( Redis::OPT_READ_TIMEOUT, $timeout ?: $this->readTimeout );
399 }
400
404 public function __destruct() {
405 foreach ( $this->connections as &$serverConnections ) {
406 foreach ( $serverConnections as &$connection ) {
407 try {
409 $conn = $connection['conn'];
410 $conn->close();
411 } catch ( RedisException $e ) {
412 // The destructor can be called on shutdown when random parts of the system
413 // have been destructed already, causing weird errors. Ignore them.
414 }
415 }
416 }
417 }
418}
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.