RedisFactory.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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 = '4.0.0';
  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. $persistent = $this->config->getValue('redis.persistent', true);
  47. // cluster config
  48. if ($isCluster) {
  49. if (!isset($config['seeds'])) {
  50. throw new \Exception('Redis cluster config is missing the "seeds" attribute');
  51. }
  52. // Support for older phpredis versions not supporting connectionParameters
  53. if ($connectionParameters !== null) {
  54. $this->instance = new \RedisCluster(null, $config['seeds'], $timeout, $readTimeout, $persistent, $auth, $connectionParameters);
  55. } else {
  56. $this->instance = new \RedisCluster(null, $config['seeds'], $timeout, $readTimeout, $persistent, $auth);
  57. }
  58. if (isset($config['failover_mode'])) {
  59. $this->instance->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, $config['failover_mode']);
  60. }
  61. } else {
  62. $this->instance = new \Redis();
  63. $host = $config['host'] ?? '127.0.0.1';
  64. $port = $config['port'] ?? ($host[0] !== '/' ? 6379 : null);
  65. $this->eventLogger->start('connect:redis', 'Connect to redis and send AUTH, SELECT');
  66. // Support for older phpredis versions not supporting connectionParameters
  67. if ($connectionParameters !== null) {
  68. // Non-clustered redis requires connection parameters to be wrapped inside `stream`
  69. $connectionParameters = [
  70. 'stream' => $this->getSslContext($config)
  71. ];
  72. if ($persistent) {
  73. /**
  74. * even though the stubs and documentation don't want you to know this,
  75. * pconnect does have the same $connectionParameters argument connect has
  76. *
  77. * https://github.com/phpredis/phpredis/blob/0264de1824b03fb2d0ad515b4d4ec019cd2dae70/redis.c#L710-L730
  78. *
  79. * @psalm-suppress TooManyArguments
  80. */
  81. $this->instance->pconnect($host, $port, $timeout, null, 0, $readTimeout, $connectionParameters);
  82. } else {
  83. $this->instance->connect($host, $port, $timeout, null, 0, $readTimeout, $connectionParameters);
  84. }
  85. } else {
  86. if ($persistent) {
  87. $this->instance->pconnect($host, $port, $timeout, null, 0, $readTimeout);
  88. } else {
  89. $this->instance->connect($host, $port, $timeout, null, 0, $readTimeout);
  90. }
  91. }
  92. // Auth if configured
  93. if ($auth !== null) {
  94. $this->instance->auth($auth);
  95. }
  96. if (isset($config['dbindex'])) {
  97. $this->instance->select($config['dbindex']);
  98. }
  99. $this->eventLogger->end('connect:redis');
  100. }
  101. }
  102. /**
  103. * Get the ssl context config
  104. *
  105. * @param array $config the current config
  106. * @return array|null
  107. * @throws \UnexpectedValueException
  108. */
  109. private function getSslContext($config) {
  110. if (isset($config['ssl_context'])) {
  111. if (!$this->isConnectionParametersSupported()) {
  112. throw new \UnexpectedValueException(\sprintf(
  113. 'php-redis extension must be version %s or higher to support ssl context',
  114. self::REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION
  115. ));
  116. }
  117. return $config['ssl_context'];
  118. }
  119. return null;
  120. }
  121. public function getInstance() {
  122. if (!$this->isAvailable()) {
  123. throw new \Exception('Redis support is not available');
  124. }
  125. if (!$this->instance instanceof \Redis) {
  126. $this->create();
  127. }
  128. return $this->instance;
  129. }
  130. public function isAvailable(): bool {
  131. return \extension_loaded('redis') &&
  132. \version_compare(\phpversion('redis'), self::REDIS_MINIMAL_VERSION, '>=');
  133. }
  134. /**
  135. * Php redis does support configurable extra parameters since version 5.3.0, see: https://github.com/phpredis/phpredis#connect-open.
  136. * We need to check if the current version supports extra connection parameters, otherwise the connect method will throw an exception
  137. *
  138. * @return boolean
  139. */
  140. private function isConnectionParametersSupported(): bool {
  141. return \extension_loaded('redis') &&
  142. \version_compare(\phpversion('redis'), self::REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION, '>=');
  143. }
  144. }