RedisFactory.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC;
  8. use OCP\Diagnostics\IEventLogger;
  9. class RedisFactory {
  10. public const REDIS_MINIMAL_VERSION = '3.1.3';
  11. public const REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION = '5.3.0';
  12. /** @var \Redis|\RedisCluster */
  13. private $instance;
  14. private SystemConfig $config;
  15. private IEventLogger $eventLogger;
  16. /**
  17. * RedisFactory constructor.
  18. *
  19. * @param SystemConfig $config
  20. */
  21. public function __construct(SystemConfig $config, IEventLogger $eventLogger) {
  22. $this->config = $config;
  23. $this->eventLogger = $eventLogger;
  24. }
  25. private function create() {
  26. $isCluster = in_array('redis.cluster', $this->config->getKeys(), true);
  27. $config = $isCluster
  28. ? $this->config->getValue('redis.cluster', [])
  29. : $this->config->getValue('redis', []);
  30. if ($isCluster && !class_exists('RedisCluster')) {
  31. throw new \Exception('Redis Cluster support is not available');
  32. }
  33. $timeout = $config['timeout'] ?? 0.0;
  34. $readTimeout = $config['read_timeout'] ?? 0.0;
  35. $auth = null;
  36. if (isset($config['password']) && (string)$config['password'] !== '') {
  37. if (isset($config['user']) && (string)$config['user'] !== '') {
  38. $auth = [$config['user'], $config['password']];
  39. } else {
  40. $auth = $config['password'];
  41. }
  42. }
  43. // # TLS support
  44. // # https://github.com/phpredis/phpredis/issues/1600
  45. $connectionParameters = $this->getSslContext($config);
  46. // cluster config
  47. if ($isCluster) {
  48. if (!isset($config['seeds'])) {
  49. throw new \Exception('Redis cluster config is missing the "seeds" attribute');
  50. }
  51. // Support for older phpredis versions not supporting connectionParameters
  52. if ($connectionParameters !== null) {
  53. $this->instance = new \RedisCluster(null, $config['seeds'], $timeout, $readTimeout, true, $auth, $connectionParameters);
  54. } else {
  55. $this->instance = new \RedisCluster(null, $config['seeds'], $timeout, $readTimeout, true, $auth);
  56. }
  57. if (isset($config['failover_mode'])) {
  58. $this->instance->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, $config['failover_mode']);
  59. }
  60. } else {
  61. $this->instance = new \Redis();
  62. $host = $config['host'] ?? '127.0.0.1';
  63. $port = $config['port'] ?? ($host[0] !== '/' ? 6379 : null);
  64. $this->eventLogger->start('connect:redis', 'Connect to redis and send AUTH, SELECT');
  65. // Support for older phpredis versions not supporting connectionParameters
  66. if ($connectionParameters !== null) {
  67. // Non-clustered redis requires connection parameters to be wrapped inside `stream`
  68. $connectionParameters = [
  69. 'stream' => $this->getSslContext($config)
  70. ];
  71. /**
  72. * even though the stubs and documentation don't want you to know this,
  73. * pconnect does have the same $connectionParameters argument connect has
  74. *
  75. * https://github.com/phpredis/phpredis/blob/0264de1824b03fb2d0ad515b4d4ec019cd2dae70/redis.c#L710-L730
  76. *
  77. * @psalm-suppress TooManyArguments
  78. */
  79. $this->instance->pconnect($host, $port, $timeout, null, 0, $readTimeout, $connectionParameters);
  80. } else {
  81. $this->instance->pconnect($host, $port, $timeout, null, 0, $readTimeout);
  82. }
  83. // Auth if configured
  84. if ($auth !== null) {
  85. $this->instance->auth($auth);
  86. }
  87. if (isset($config['dbindex'])) {
  88. $this->instance->select($config['dbindex']);
  89. }
  90. $this->eventLogger->end('connect:redis');
  91. }
  92. }
  93. /**
  94. * Get the ssl context config
  95. *
  96. * @param array $config the current config
  97. * @return array|null
  98. * @throws \UnexpectedValueException
  99. */
  100. private function getSslContext($config) {
  101. if (isset($config['ssl_context'])) {
  102. if (!$this->isConnectionParametersSupported()) {
  103. throw new \UnexpectedValueException(\sprintf(
  104. 'php-redis extension must be version %s or higher to support ssl context',
  105. self::REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION
  106. ));
  107. }
  108. return $config['ssl_context'];
  109. }
  110. return null;
  111. }
  112. public function getInstance() {
  113. if (!$this->isAvailable()) {
  114. throw new \Exception('Redis support is not available');
  115. }
  116. if (!$this->instance instanceof \Redis) {
  117. $this->create();
  118. }
  119. return $this->instance;
  120. }
  121. public function isAvailable(): bool {
  122. return \extension_loaded('redis') &&
  123. \version_compare(\phpversion('redis'), self::REDIS_MINIMAL_VERSION, '>=');
  124. }
  125. /**
  126. * Php redis does support configurable extra parameters since version 5.3.0, see: https://github.com/phpredis/phpredis#connect-open.
  127. * We need to check if the current version supports extra connection parameters, otherwise the connect method will throw an exception
  128. *
  129. * @return boolean
  130. */
  131. private function isConnectionParametersSupported(): bool {
  132. return \extension_loaded('redis') &&
  133. \version_compare(\phpversion('redis'), self::REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION, '>=');
  134. }
  135. }