MediaWiki  master
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 
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' ) ) {
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 
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;
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  list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip, port)
231  } elseif ( preg_match( '/^([^:]+):(\d+)$/', $server, $m ) ) {
232  list( $host, $port ) = [ $m[1], (int)$m[2] ]; // (ip or path, port)
233  } else {
234  list( $host, $port ) = [ $server, 6379 ]; // (ip or path, port)
235  }
236  }
237 
238  $conn = new Redis();
239  try {
240  if ( $this->persistent ) {
241  $result = $conn->pconnect( $host, $port, $this->connectTimeout, $this->id );
242  } else {
243  $result = $conn->connect( $host, $port, $this->connectTimeout );
244  }
245  if ( !$result ) {
246  $logger->error(
247  'Could not connect to server "{redis_server}"',
248  [ 'redis_server' => $server ]
249  );
250  // Mark server down for some time to avoid further timeouts
251  $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
252 
253  return false;
254  }
255  if ( ( $this->password !== null ) && !$conn->auth( $this->password ) ) {
256  $logger->error(
257  'Authentication error connecting to "{redis_server}"',
258  [ 'redis_server' => $server ]
259  );
260  }
261  } catch ( RedisException $e ) {
262  $this->downServers[$server] = time() + self::SERVER_DOWN_TTL;
263  $logger->error(
264  'Redis exception connecting to "{redis_server}"',
265  [
266  'redis_server' => $server,
267  'exception' => $e,
268  ]
269  );
270 
271  return false;
272  }
273 
274  $conn->setOption( Redis::OPT_READ_TIMEOUT, $this->readTimeout );
275  $conn->setOption( Redis::OPT_SERIALIZER, $this->serializer );
276  $this->connections[$server][] = [ 'conn' => $conn, 'free' => false ];
277 
278  return new RedisConnRef( $this, $server, $conn, $logger );
279  }
280 
288  public function freeConnection( $server, Redis $conn ) {
289  $found = false;
290 
291  foreach ( $this->connections[$server] as &$connection ) {
292  if ( $connection['conn'] === $conn && !$connection['free'] ) {
293  $connection['free'] = true;
295  break;
296  }
297  }
298 
299  $this->closeExcessIdleConections();
300 
301  return $found;
302  }
303 
307  protected function closeExcessIdleConections() {
308  if ( $this->idlePoolSize <= count( $this->connections ) ) {
309  return; // nothing to do (no more connections than servers)
310  }
311 
312  foreach ( $this->connections as &$serverConnections ) {
313  foreach ( $serverConnections as $key => &$connection ) {
314  if ( $connection['free'] ) {
315  unset( $serverConnections[$key] );
316  if ( --$this->idlePoolSize <= count( $this->connections ) ) {
317  return; // done (no more connections than servers)
318  }
319  }
320  }
321  }
322  }
323 
333  public function handleError( RedisConnRef $cref, RedisException $e ) {
334  $server = $cref->getServer();
335  $this->logger->error(
336  'Redis exception on server "{redis_server}"',
337  [
338  'redis_server' => $server,
339  'exception' => $e,
340  ]
341  );
342  foreach ( $this->connections[$server] as $key => $connection ) {
343  if ( $cref->isConnIdentical( $connection['conn'] ) ) {
344  $this->idlePoolSize -= $connection['free'] ? 1 : 0;
345  unset( $this->connections[$server][$key] );
346  break;
347  }
348  }
349  }
350 
367  public function reauthenticateConnection( $server, Redis $conn ) {
368  if ( $this->password !== null && !$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  return true;
378  }
379 
386  public function resetTimeout( Redis $conn, $timeout = null ) {
387  $conn->setOption( Redis::OPT_READ_TIMEOUT, $timeout ?: $this->readTimeout );
388  }
389 
393  public function __destruct() {
394  foreach ( $this->connections as $server => &$serverConnections ) {
395  foreach ( $serverConnections as $key => &$connection ) {
396  try {
398  $conn = $connection['conn'];
399  $conn->close();
400  } catch ( RedisException $e ) {
401  // The destructor can be called on shutdown when random parts of the system
402  // have been destructed already, causing weird errors. Ignore them.
403  }
404  }
405  }
406  }
407 }
RedisConnectionPool\freeConnection
freeConnection( $server, Redis $conn)
Mark a connection to a server as free to return to the pool.
Definition: RedisConnectionPool.php:288
RedisConnectionPool\SERVER_DOWN_TTL
const SERVER_DOWN_TTL
integer; seconds to cache servers as "down".
Definition: RedisConnectionPool.php:70
RedisConnectionPool\$downServers
array $downServers
(server name => UNIX timestamp)
Definition: RedisConnectionPool.php:64
RedisConnectionPool\singleton
static singleton(array $options)
Definition: RedisConnectionPool.php:149
RedisConnectionPool\$connections
array $connections
(server name => ((connection info array),...) -var array<string,array{conn:Redis,free:bool}[]>
Definition: RedisConnectionPool.php:62
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:333
RedisConnectionPool\resetTimeout
resetTimeout(Redis $conn, $timeout=null)
Adjust or reset the connection handle read timeout value.
Definition: RedisConnectionPool.php:386
RedisConnectionPool\destroySingletons
static destroySingletons()
Destroy all singleton() instances.
Definition: RedisConnectionPool.php:166
RedisConnectionPool\$logger
LoggerInterface $logger
Definition: RedisConnectionPool.php:75
serialize
serialize()
Definition: ApiMessageTrait.php:138
RedisConnectionPool\__construct
__construct(array $options, $id)
Definition: RedisConnectionPool.php:82
RedisConnRef\isConnIdentical
isConnIdentical(Redis $conn)
Definition: RedisConnRef.php:293
RedisConnectionPool\closeExcessIdleConections
closeExcessIdleConections()
Close any extra idle connections if there are more than the limit.
Definition: RedisConnectionPool.php:307
RedisConnectionPool\$instances
static array $instances
(pool ID => RedisConnectionPool)
Definition: RedisConnectionPool.php:67
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:179
RedisConnectionPool\applyDefaultConfig
static applyDefaultConfig(array $options)
Definition: RedisConnectionPool.php:117
RedisConnectionPool\$readTimeout
string $readTimeout
Read timeout in seconds.
Definition: RedisConnectionPool.php:45
RedisConnectionPool\setLogger
setLogger(LoggerInterface $logger)
Definition: RedisConnectionPool.php:109
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:367
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:393
RedisConnRef\getServer
getServer()
Definition: RedisConnRef.php:84