MediaWiki  master
RedisConnectionPool.php
Go to the documentation of this file.
1 <?php
27 
41 class 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 
59  protected $connections = [];
61  protected $downServers = [];
62 
64  protected static $instances = [];
65 
67  const SERVER_DOWN_TTL = 30;
68 
72  protected $logger;
73 
79  protected function __construct( array $options, $id ) {
80  if ( !class_exists( 'Redis' ) ) {
81  throw new RuntimeException(
82  __CLASS__ . ' requires a Redis client library. ' .
83  'See https://www.mediawiki.org/wiki/Redis#Setup' );
84  }
85  $this->logger = $options['logger'] ?? new NullLogger();
86  $this->connectTimeout = $options['connectTimeout'];
87  $this->readTimeout = $options['readTimeout'];
88  $this->persistent = $options['persistent'];
89  $this->password = $options['password'];
90  if ( !isset( $options['serializer'] ) || $options['serializer'] === 'php' ) {
91  $this->serializer = Redis::SERIALIZER_PHP;
92  } elseif ( $options['serializer'] === 'igbinary' ) {
93  if ( !defined( 'Redis::SERIALIZER_IGBINARY' ) ) {
94  throw new InvalidArgumentException(
95  __CLASS__ . ': configured serializer "igbinary" not available' );
96  }
97  $this->serializer = Redis::SERIALIZER_IGBINARY;
98  } elseif ( $options['serializer'] === 'none' ) {
99  $this->serializer = Redis::SERIALIZER_NONE;
100  } else {
101  throw new InvalidArgumentException( "Invalid serializer specified." );
102  }
103  $this->id = $id;
104  }
105 
106  public function setLogger( LoggerInterface $logger ) {
107  $this->logger = $logger;
108  }
109 
114  protected static function applyDefaultConfig( array $options ) {
115  if ( !isset( $options['connectTimeout'] ) ) {
116  $options['connectTimeout'] = 1;
117  }
118  if ( !isset( $options['readTimeout'] ) ) {
119  $options['readTimeout'] = 1;
120  }
121  if ( !isset( $options['persistent'] ) ) {
122  $options['persistent'] = false;
123  }
124  if ( !isset( $options['password'] ) ) {
125  $options['password'] = null;
126  }
127 
128  return $options;
129  }
130 
146  public static function singleton( array $options ) {
147  $options = self::applyDefaultConfig( $options );
148  // Map the options to a unique hash...
149  ksort( $options ); // normalize to avoid pool fragmentation
150  $id = sha1( serialize( $options ) );
151  // Initialize the object at the hash as needed...
152  if ( !isset( self::$instances[$id] ) ) {
153  self::$instances[$id] = new self( $options, $id );
154  }
155 
156  return self::$instances[$id];
157  }
158 
163  public static function destroySingletons() {
164  self::$instances = [];
165  }
166 
176  public function getConnection( $server, LoggerInterface $logger = null ) {
177  // The above @return also documents 'Redis' for convenience with IDEs.
178  // RedisConnRef uses PHP magic methods, which wouldn't be recognised.
179 
181  // Check the listing "dead" servers which have had a connection errors.
182  // Servers are marked dead for a limited period of time, to
183  // avoid excessive overhead from repeated connection timeouts.
184  if ( isset( $this->downServers[$server] ) ) {
185  $now = time();
186  if ( $now > $this->downServers[$server] ) {
187  // Dead time expired
188  unset( $this->downServers[$server] );
189  } else {
190  // Server is dead
191  $logger->debug(
192  'Server "{redis_server}" is marked down for another ' .
193  ( $this->downServers[$server] - $now ) . 'seconds',
194  [ 'redis_server' => $server ]
195  );
196 
197  return false;
198  }
199  }
200 
201  // Check if a connection is already free for use
202  if ( isset( $this->connections[$server] ) ) {
203  foreach ( $this->connections[$server] as &$connection ) {
204  if ( $connection['free'] ) {
205  $connection['free'] = false;
207 
208  return new RedisConnRef(
209  $this, $server, $connection['conn'], $logger
210  );
211  }
212  }
213  }
214 
215  if ( !$server ) {
216  throw new InvalidArgumentException(
217  __CLASS__ . ": invalid configured server \"$server\"" );
218  } elseif ( substr( $server, 0, 1 ) === '/' ) {
219  // UNIX domain socket
220  // These are required by the redis extension to start with a slash, but
221  // we still need to set the port to a special value to make it work.
222  $host = $server;
223  $port = 0;
224  } else {
225  // TCP connection
226  if ( preg_match( '/^\[(.+)\]:(\d+)$/', $server, $m ) ) {
227  list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip, port)
228  } elseif ( preg_match( '/^([^:]+):(\d+)$/', $server, $m ) ) {
229  list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip or path, port)
230  } else {
231  list( $host, $port ) = [ $server, 6379 ]; // (ip or path, port)
232  }
233  }
234 
235  $conn = new Redis();
236  try {
237  if ( $this->persistent ) {
238  $result = $conn->pconnect( $host, $port, $this->connectTimeout, $this->id );
239  } else {
240  $result = $conn->connect( $host, $port, $this->connectTimeout );
241  }
242  if ( !$result ) {
243  $logger->error(
244  'Could not connect to server "{redis_server}"',
245  [ 'redis_server' => $server ]
246  );
247  // Mark server down for some time to avoid further timeouts
248  $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
249 
250  return false;
251  }
252  if ( ( $this->password !== null ) && !$conn->auth( $this->password ) ) {
253  $logger->error(
254  'Authentication error connecting to "{redis_server}"',
255  [ 'redis_server' => $server ]
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 
300  $this->closeExcessIdleConections();
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 && !$conn->auth( $this->password ) ) {
370  $this->logger->error(
371  'Authentication error connecting to "{redis_server}"',
372  [ 'redis_server' => $server ]
373  );
374 
375  return false;
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 }
freeConnection( $server, Redis $conn)
Mark a connection to a server as free to return to the pool.
string $id
ID for persistent connections.
serialize()
reauthenticateConnection( $server, Redis $conn)
Re-send an AUTH request to the redis server (useful after disconnects).
bool $persistent
Whether connections persist.
string $password
Plaintext auth password.
array $downServers
(server name => UNIX timestamp)
static destroySingletons()
Destroy all singleton() instances.
string $connectTimeout
Connection timeout in seconds.
static applyDefaultConfig(array $options)
handleError(RedisConnRef $cref, RedisException $e)
The redis extension throws an exception in response to various read, write and protocol errors...
array $connections
(server name => ((connection info array),...)
static singleton(array $options)
__construct(array $options, $id)
int $serializer
Serializer to use (Redis::SERIALIZER_*)
const SERVER_DOWN_TTL
integer; seconds to cache servers as "down".
__destruct()
Make sure connections are closed for sanity.
string $readTimeout
Read timeout in seconds.
resetTimeout(Redis $conn, $timeout=null)
Adjust or reset the connection handle read timeout value.
Helper class to manage Redis connections.
closeExcessIdleConections()
Close any extra idle connections if there are more than the limit.
Helper class to handle automatically marking connectons as reusable (via RAII pattern) ...
isConnIdentical(Redis $conn)
getConnection( $server, LoggerInterface $logger=null)
Get a connection to a redis server.
setLogger(LoggerInterface $logger)
static array $instances
(pool ID => RedisConnectionPool)
int $idlePoolSize
Current idle pool size.