MediaWiki REL1_31
RedisConnectionPool.php
Go to the documentation of this file.
1<?php
24use Psr\Log\LoggerAwareInterface;
25use Psr\Log\LoggerInterface;
26
40class RedisConnectionPool implements LoggerAwareInterface {
42 protected $connectTimeout;
44 protected $readTimeout;
46 protected $password;
48 protected $persistent;
50 protected $serializer;
52 protected $id;
53
55 protected $idlePoolSize = 0;
56
58 protected $connections = [];
60 protected $downServers = [];
61
63 protected static $instances = [];
64
66 const SERVER_DOWN_TTL = 30;
67
71 protected $logger;
72
78 protected function __construct( array $options, $id ) {
79 if ( !class_exists( 'Redis' ) ) {
80 throw new RuntimeException(
81 __CLASS__ . ' requires a Redis client library. ' .
82 'See https://www.mediawiki.org/wiki/Redis#Setup' );
83 }
84 $this->logger = isset( $options['logger'] )
85 ? $options['logger']
86 : new \Psr\Log\NullLogger();
87 $this->connectTimeout = $options['connectTimeout'];
88 $this->readTimeout = $options['readTimeout'];
89 $this->persistent = $options['persistent'];
90 $this->password = $options['password'];
91 if ( !isset( $options['serializer'] ) || $options['serializer'] === 'php' ) {
92 $this->serializer = Redis::SERIALIZER_PHP;
93 } elseif ( $options['serializer'] === 'igbinary' ) {
94 $this->serializer = Redis::SERIALIZER_IGBINARY;
95 } elseif ( $options['serializer'] === 'none' ) {
96 $this->serializer = Redis::SERIALIZER_NONE;
97 } else {
98 throw new InvalidArgumentException( "Invalid serializer specified." );
99 }
100 $this->id = $id;
101 }
102
107 public function setLogger( LoggerInterface $logger ) {
108 $this->logger = $logger;
109 }
110
115 protected static function applyDefaultConfig( array $options ) {
116 if ( !isset( $options['connectTimeout'] ) ) {
117 $options['connectTimeout'] = 1;
118 }
119 if ( !isset( $options['readTimeout'] ) ) {
120 $options['readTimeout'] = 1;
121 }
122 if ( !isset( $options['persistent'] ) ) {
123 $options['persistent'] = false;
124 }
125 if ( !isset( $options['password'] ) ) {
126 $options['password'] = null;
127 }
128
129 return $options;
130 }
131
147 public static function singleton( array $options ) {
149 // Map the options to a unique hash...
150 ksort( $options ); // normalize to avoid pool fragmentation
151 $id = sha1( serialize( $options ) );
152 // Initialize the object at the hash as needed...
153 if ( !isset( self::$instances[$id] ) ) {
154 self::$instances[$id] = new self( $options, $id );
155 }
156
157 return self::$instances[$id];
158 }
159
164 public static function destroySingletons() {
165 self::$instances = [];
166 }
167
177 public function getConnection( $server, LoggerInterface $logger = null ) {
179 // Check the listing "dead" servers which have had a connection errors.
180 // Servers are marked dead for a limited period of time, to
181 // avoid excessive overhead from repeated connection timeouts.
182 if ( isset( $this->downServers[$server] ) ) {
183 $now = time();
184 if ( $now > $this->downServers[$server] ) {
185 // Dead time expired
186 unset( $this->downServers[$server] );
187 } else {
188 // Server is dead
189 $logger->debug(
190 'Server "{redis_server}" is marked down for another ' .
191 ( $this->downServers[$server] - $now ) . 'seconds',
192 [ 'redis_server' => $server ]
193 );
194
195 return false;
196 }
197 }
198
199 // Check if a connection is already free for use
200 if ( isset( $this->connections[$server] ) ) {
201 foreach ( $this->connections[$server] as &$connection ) {
202 if ( $connection['free'] ) {
203 $connection['free'] = false;
205
206 return new RedisConnRef(
207 $this, $server, $connection['conn'], $logger
208 );
209 }
210 }
211 }
212
213 if ( !$server ) {
214 throw new InvalidArgumentException(
215 __CLASS__ . ": invalid configured server \"$server\"" );
216 } elseif ( substr( $server, 0, 1 ) === '/' ) {
217 // UNIX domain socket
218 // These are required by the redis extension to start with a slash, but
219 // we still need to set the port to a special value to make it work.
220 $host = $server;
221 $port = 0;
222 } else {
223 // TCP connection
224 if ( preg_match( '/^\[(.+)\]:(\d+)$/', $server, $m ) ) {
225 list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip, port)
226 } elseif ( preg_match( '/^([^:]+):(\d+)$/', $server, $m ) ) {
227 list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip or path, port)
228 } else {
229 list( $host, $port ) = [ $server, 6379 ]; // (ip or path, port)
230 }
231 }
232
233 $conn = new Redis();
234 try {
235 if ( $this->persistent ) {
236 $result = $conn->pconnect( $host, $port, $this->connectTimeout, $this->id );
237 } else {
238 $result = $conn->connect( $host, $port, $this->connectTimeout );
239 }
240 if ( !$result ) {
241 $logger->error(
242 'Could not connect to server "{redis_server}"',
243 [ 'redis_server' => $server ]
244 );
245 // Mark server down for some time to avoid further timeouts
246 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
247
248 return false;
249 }
250 if ( $this->password !== null ) {
251 if ( !$conn->auth( $this->password ) ) {
252 $logger->error(
253 'Authentication error connecting to "{redis_server}"',
254 [ 'redis_server' => $server ]
255 );
256 }
257 }
258 } catch ( RedisException $e ) {
259 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
260 $logger->error(
261 'Redis exception connecting to "{redis_server}"',
262 [
263 'redis_server' => $server,
264 'exception' => $e,
265 ]
266 );
267
268 return false;
269 }
270
271 if ( $conn ) {
272 $conn->setOption( Redis::OPT_READ_TIMEOUT, $this->readTimeout );
273 $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
274 $this->connections[$server][] = [ 'conn' => $conn, 'free' => false ];
275
276 return new RedisConnRef( $this, $server, $conn, $logger );
277 } else {
278 return false;
279 }
280 }
281
289 public function freeConnection( $server, Redis $conn ) {
290 $found = false;
291
292 foreach ( $this->connections[$server] as &$connection ) {
293 if ( $connection['conn'] === $conn && !$connection['free'] ) {
294 $connection['free'] = true;
296 break;
297 }
298 }
299
301
302 return $found;
303 }
304
308 protected function closeExcessIdleConections() {
309 if ( $this->idlePoolSize <= count( $this->connections ) ) {
310 return; // nothing to do (no more connections than servers)
311 }
312
313 foreach ( $this->connections as &$serverConnections ) {
314 foreach ( $serverConnections as $key => &$connection ) {
315 if ( $connection['free'] ) {
316 unset( $serverConnections[$key] );
317 if ( --$this->idlePoolSize <= count( $this->connections ) ) {
318 return; // done (no more connections than servers)
319 }
320 }
321 }
322 }
323 }
324
334 public function handleError( RedisConnRef $cref, RedisException $e ) {
335 $server = $cref->getServer();
336 $this->logger->error(
337 'Redis exception on server "{redis_server}"',
338 [
339 'redis_server' => $server,
340 'exception' => $e,
341 ]
342 );
343 foreach ( $this->connections[$server] as $key => $connection ) {
344 if ( $cref->isConnIdentical( $connection['conn'] ) ) {
345 $this->idlePoolSize -= $connection['free'] ? 1 : 0;
346 unset( $this->connections[$server][$key] );
347 break;
348 }
349 }
350 }
351
368 public function reauthenticateConnection( $server, Redis $conn ) {
369 if ( $this->password !== null ) {
370 if ( !$conn->auth( $this->password ) ) {
371 $this->logger->error(
372 'Authentication error connecting to "{redis_server}"',
373 [ 'redis_server' => $server ]
374 );
375
376 return false;
377 }
378 }
379
380 return true;
381 }
382
389 public function resetTimeout( Redis $conn, $timeout = null ) {
390 $conn->setOption( Redis::OPT_READ_TIMEOUT, $timeout ?: $this->readTimeout );
391 }
392
396 function __destruct() {
397 foreach ( $this->connections as $server => &$serverConnections ) {
398 foreach ( $serverConnections as $key => &$connection ) {
399 try {
401 $conn = $connection['conn'];
402 $conn->close();
403 } catch ( RedisException $e ) {
404 // The destructor can be called on shutdown when random parts of the system
405 // have been destructed already, causing weird errors. Ignore them.
406 }
407 }
408 }
409 }
410}
serialize()
Helper class to handle automatically marking connectons as reusable (via RAII pattern)
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)
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2001
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
returning false will NOT prevent logging $e
Definition hooks.txt:2176