MediaWiki  1.34.0
RedisConnectionPool.php
Go to the documentation of this file.
1 <?php
24 use Psr\Log\LoggerAwareInterface;
25 use Psr\Log\LoggerInterface;
26 use Psr\Log\NullLogger;
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 }
RedisConnectionPool\freeConnection
freeConnection( $server, Redis $conn)
Mark a connection to a server as free to return to the pool.
Definition: RedisConnectionPool.php:289
RedisConnectionPool\SERVER_DOWN_TTL
const SERVER_DOWN_TTL
integer; seconds to cache servers as "down".
Definition: RedisConnectionPool.php:67
RedisConnectionPool\$downServers
array $downServers
(server name => UNIX timestamp)
Definition: RedisConnectionPool.php:61
RedisConnectionPool\singleton
static singleton(array $options)
Definition: RedisConnectionPool.php:146
RedisConnectionPool\$connections
array $connections
(server name => ((connection info array),...)
Definition: RedisConnectionPool.php:59
RedisConnectionPool\handleError
handleError(RedisConnRef $cref, RedisException $e)
The redis extension throws an exception in response to various read, write and protocol errors.
Definition: RedisConnectionPool.php:334
RedisConnectionPool\resetTimeout
resetTimeout(Redis $conn, $timeout=null)
Adjust or reset the connection handle read timeout value.
Definition: RedisConnectionPool.php:387
RedisConnectionPool\destroySingletons
static destroySingletons()
Destroy all singleton() instances.
Definition: RedisConnectionPool.php:163
RedisConnectionPool\$logger
LoggerInterface $logger
Definition: RedisConnectionPool.php:72
serialize
serialize()
Definition: ApiMessageTrait.php:138
RedisConnectionPool\__construct
__construct(array $options, $id)
Definition: RedisConnectionPool.php:79
RedisConnRef\isConnIdentical
isConnIdentical(Redis $conn)
Definition: RedisConnRef.php:294
RedisConnectionPool\closeExcessIdleConections
closeExcessIdleConections()
Close any extra idle connections if there are more than the limit.
Definition: RedisConnectionPool.php:308
RedisConnectionPool\$instances
static array $instances
(pool ID => RedisConnectionPool)
Definition: RedisConnectionPool.php:64
RedisConnectionPool\$password
string $password
Plaintext auth password.
Definition: RedisConnectionPool.php:47
RedisConnectionPool\$serializer
int $serializer
Serializer to use (Redis::SERIALIZER_*)
Definition: RedisConnectionPool.php:51
RedisConnectionPool\getConnection
getConnection( $server, LoggerInterface $logger=null)
Get a connection to a redis server.
Definition: RedisConnectionPool.php:176
RedisConnectionPool\applyDefaultConfig
static applyDefaultConfig(array $options)
Definition: RedisConnectionPool.php:114
RedisConnectionPool\$readTimeout
string $readTimeout
Read timeout in seconds.
Definition: RedisConnectionPool.php:45
RedisConnectionPool\setLogger
setLogger(LoggerInterface $logger)
Definition: RedisConnectionPool.php:106
RedisConnectionPool\$idlePoolSize
int $idlePoolSize
Current idle pool size.
Definition: RedisConnectionPool.php:56
RedisConnectionPool\$id
string $id
ID for persistent connections.
Definition: RedisConnectionPool.php:53
RedisConnectionPool
Helper class to manage Redis connections.
Definition: RedisConnectionPool.php:41
RedisConnectionPool\reauthenticateConnection
reauthenticateConnection( $server, Redis $conn)
Re-send an AUTH request to the redis server (useful after disconnects).
Definition: RedisConnectionPool.php:368
RedisConnectionPool\$connectTimeout
string $connectTimeout
Connection timeout in seconds.
Definition: RedisConnectionPool.php:43
RedisConnRef
Helper class to handle automatically marking connectons as reusable (via RAII pattern)
Definition: RedisConnRef.php:31
RedisConnectionPool\$persistent
bool $persistent
Whether connections persist.
Definition: RedisConnectionPool.php:49
RedisConnectionPool\__destruct
__destruct()
Make sure connections are closed for sanity.
Definition: RedisConnectionPool.php:394
RedisConnRef\getServer
getServer()
Definition: RedisConnRef.php:84