MediaWiki REL1_32
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 = $options['logger'] ?? new \Psr\Log\NullLogger();
85 $this->connectTimeout = $options['connectTimeout'];
86 $this->readTimeout = $options['readTimeout'];
87 $this->persistent = $options['persistent'];
88 $this->password = $options['password'];
89 if ( !isset( $options['serializer'] ) || $options['serializer'] === 'php' ) {
90 $this->serializer = Redis::SERIALIZER_PHP;
91 } elseif ( $options['serializer'] === 'igbinary' ) {
92 $this->serializer = Redis::SERIALIZER_IGBINARY;
93 } elseif ( $options['serializer'] === 'none' ) {
94 $this->serializer = Redis::SERIALIZER_NONE;
95 } else {
96 throw new InvalidArgumentException( "Invalid serializer specified." );
97 }
98 $this->id = $id;
99 }
100
105 public function setLogger( LoggerInterface $logger ) {
106 $this->logger = $logger;
107 }
108
113 protected static function applyDefaultConfig( array $options ) {
114 if ( !isset( $options['connectTimeout'] ) ) {
115 $options['connectTimeout'] = 1;
116 }
117 if ( !isset( $options['readTimeout'] ) ) {
118 $options['readTimeout'] = 1;
119 }
120 if ( !isset( $options['persistent'] ) ) {
121 $options['persistent'] = false;
122 }
123 if ( !isset( $options['password'] ) ) {
124 $options['password'] = null;
125 }
126
127 return $options;
128 }
129
145 public static function singleton( array $options ) {
147 // Map the options to a unique hash...
148 ksort( $options ); // normalize to avoid pool fragmentation
149 $id = sha1( serialize( $options ) );
150 // Initialize the object at the hash as needed...
151 if ( !isset( self::$instances[$id] ) ) {
152 self::$instances[$id] = new self( $options, $id );
153 }
154
155 return self::$instances[$id];
156 }
157
162 public static function destroySingletons() {
163 self::$instances = [];
164 }
165
175 public function getConnection( $server, LoggerInterface $logger = null ) {
177 // Check the listing "dead" servers which have had a connection errors.
178 // Servers are marked dead for a limited period of time, to
179 // avoid excessive overhead from repeated connection timeouts.
180 if ( isset( $this->downServers[$server] ) ) {
181 $now = time();
182 if ( $now > $this->downServers[$server] ) {
183 // Dead time expired
184 unset( $this->downServers[$server] );
185 } else {
186 // Server is dead
187 $logger->debug(
188 'Server "{redis_server}" is marked down for another ' .
189 ( $this->downServers[$server] - $now ) . 'seconds',
190 [ 'redis_server' => $server ]
191 );
192
193 return false;
194 }
195 }
196
197 // Check if a connection is already free for use
198 if ( isset( $this->connections[$server] ) ) {
199 foreach ( $this->connections[$server] as &$connection ) {
200 if ( $connection['free'] ) {
201 $connection['free'] = false;
203
204 return new RedisConnRef(
205 $this, $server, $connection['conn'], $logger
206 );
207 }
208 }
209 }
210
211 if ( !$server ) {
212 throw new InvalidArgumentException(
213 __CLASS__ . ": invalid configured server \"$server\"" );
214 } elseif ( substr( $server, 0, 1 ) === '/' ) {
215 // UNIX domain socket
216 // These are required by the redis extension to start with a slash, but
217 // we still need to set the port to a special value to make it work.
218 $host = $server;
219 $port = 0;
220 } else {
221 // TCP connection
222 if ( preg_match( '/^\[(.+)\]:(\d+)$/', $server, $m ) ) {
223 list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip, port)
224 } elseif ( preg_match( '/^([^:]+):(\d+)$/', $server, $m ) ) {
225 list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip or path, port)
226 } else {
227 list( $host, $port ) = [ $server, 6379 ]; // (ip or path, port)
228 }
229 }
230
231 $conn = new Redis();
232 try {
233 if ( $this->persistent ) {
234 $result = $conn->pconnect( $host, $port, $this->connectTimeout, $this->id );
235 } else {
236 $result = $conn->connect( $host, $port, $this->connectTimeout );
237 }
238 if ( !$result ) {
239 $logger->error(
240 'Could not connect to server "{redis_server}"',
241 [ 'redis_server' => $server ]
242 );
243 // Mark server down for some time to avoid further timeouts
244 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
245
246 return false;
247 }
248 if ( $this->password !== null ) {
249 if ( !$conn->auth( $this->password ) ) {
250 $logger->error(
251 'Authentication error connecting to "{redis_server}"',
252 [ 'redis_server' => $server ]
253 );
254 }
255 }
256 } catch ( RedisException $e ) {
257 $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
258 $logger->error(
259 'Redis exception connecting to "{redis_server}"',
260 [
261 'redis_server' => $server,
262 'exception' => $e,
263 ]
264 );
265
266 return false;
267 }
268
269 if ( $conn ) {
270 $conn->setOption( Redis::OPT_READ_TIMEOUT, $this->readTimeout );
271 $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
272 $this->connections[$server][] = [ 'conn' => $conn, 'free' => false ];
273
274 return new RedisConnRef( $this, $server, $conn, $logger );
275 } else {
276 return false;
277 }
278 }
279
287 public function freeConnection( $server, Redis $conn ) {
288 $found = false;
289
290 foreach ( $this->connections[$server] as &$connection ) {
291 if ( $connection['conn'] === $conn && !$connection['free'] ) {
292 $connection['free'] = true;
294 break;
295 }
296 }
297
299
300 return $found;
301 }
302
306 protected function closeExcessIdleConections() {
307 if ( $this->idlePoolSize <= count( $this->connections ) ) {
308 return; // nothing to do (no more connections than servers)
309 }
310
311 foreach ( $this->connections as &$serverConnections ) {
312 foreach ( $serverConnections as $key => &$connection ) {
313 if ( $connection['free'] ) {
314 unset( $serverConnections[$key] );
315 if ( --$this->idlePoolSize <= count( $this->connections ) ) {
316 return; // done (no more connections than servers)
317 }
318 }
319 }
320 }
321 }
322
332 public function handleError( RedisConnRef $cref, RedisException $e ) {
333 $server = $cref->getServer();
334 $this->logger->error(
335 'Redis exception on server "{redis_server}"',
336 [
337 'redis_server' => $server,
338 'exception' => $e,
339 ]
340 );
341 foreach ( $this->connections[$server] as $key => $connection ) {
342 if ( $cref->isConnIdentical( $connection['conn'] ) ) {
343 $this->idlePoolSize -= $connection['free'] ? 1 : 0;
344 unset( $this->connections[$server][$key] );
345 break;
346 }
347 }
348 }
349
366 public function reauthenticateConnection( $server, Redis $conn ) {
367 if ( $this->password !== null ) {
368 if ( !$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
378 return true;
379 }
380
387 public function resetTimeout( Redis $conn, $timeout = null ) {
388 $conn->setOption( Redis::OPT_READ_TIMEOUT, $timeout ?: $this->readTimeout );
389 }
390
394 function __destruct() {
395 foreach ( $this->connections as $server => &$serverConnections ) {
396 foreach ( $serverConnections as $key => &$connection ) {
397 try {
399 $conn = $connection['conn'];
400 $conn->close();
401 } catch ( RedisException $e ) {
402 // The destructor can be called on shutdown when random parts of the system
403 // have been destructed already, causing weird errors. Ignore them.
404 }
405 }
406 }
407 }
408}
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:2050
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:2226
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))