1
0

RedisFactory.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Alejandro Varela <epma01@gmail.com>
  6. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  7. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  8. * @author Morris Jobke <hey@morrisjobke.de>
  9. * @author Robin Appelman <robin@icewind.nl>
  10. * @author Robin McCorkell <robin@mccorkell.me.uk>
  11. *
  12. * @license AGPL-3.0
  13. *
  14. * This code is free software: you can redistribute it and/or modify
  15. * it under the terms of the GNU Affero General Public License, version 3,
  16. * as published by the Free Software Foundation.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU Affero General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU Affero General Public License, version 3,
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>
  25. *
  26. */
  27. namespace OC;
  28. use OCP\Diagnostics\IEventLogger;
  29. class RedisFactory {
  30. public const REDIS_MINIMAL_VERSION = '4.0.0';
  31. public const REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION = '5.3.0';
  32. /** @var \Redis|\RedisCluster */
  33. private $instance;
  34. private SystemConfig $config;
  35. private IEventLogger $eventLogger;
  36. /**
  37. * RedisFactory constructor.
  38. *
  39. * @param SystemConfig $config
  40. */
  41. public function __construct(SystemConfig $config, IEventLogger $eventLogger) {
  42. $this->config = $config;
  43. $this->eventLogger = $eventLogger;
  44. }
  45. private function create() {
  46. $isCluster = in_array('redis.cluster', $this->config->getKeys(), true);
  47. $config = $isCluster
  48. ? $this->config->getValue('redis.cluster', [])
  49. : $this->config->getValue('redis', []);
  50. if ($isCluster && !class_exists('RedisCluster')) {
  51. throw new \Exception('Redis Cluster support is not available');
  52. }
  53. $timeout = $config['timeout'] ?? 0.0;
  54. $readTimeout = $config['read_timeout'] ?? 0.0;
  55. $auth = null;
  56. if (isset($config['password']) && (string)$config['password'] !== '') {
  57. if (isset($config['user']) && (string)$config['user'] !== '') {
  58. $auth = [$config['user'], $config['password']];
  59. } else {
  60. $auth = $config['password'];
  61. }
  62. }
  63. // # TLS support
  64. // # https://github.com/phpredis/phpredis/issues/1600
  65. $connectionParameters = $this->getSslContext($config);
  66. $persistent = $this->config->getValue('redis.persistent', true);
  67. // cluster config
  68. if ($isCluster) {
  69. if (!isset($config['seeds'])) {
  70. throw new \Exception('Redis cluster config is missing the "seeds" attribute');
  71. }
  72. // Support for older phpredis versions not supporting connectionParameters
  73. if ($connectionParameters !== null) {
  74. $this->instance = new \RedisCluster(null, $config['seeds'], $timeout, $readTimeout, $persistent, $auth, $connectionParameters);
  75. } else {
  76. $this->instance = new \RedisCluster(null, $config['seeds'], $timeout, $readTimeout, $persistent, $auth);
  77. }
  78. if (isset($config['failover_mode'])) {
  79. $this->instance->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, $config['failover_mode']);
  80. }
  81. } else {
  82. $this->instance = new \Redis();
  83. $host = $config['host'] ?? '127.0.0.1';
  84. $port = $config['port'] ?? ($host[0] !== '/' ? 6379 : null);
  85. $this->eventLogger->start('connect:redis', 'Connect to redis and send AUTH, SELECT');
  86. // Support for older phpredis versions not supporting connectionParameters
  87. if ($connectionParameters !== null) {
  88. // Non-clustered redis requires connection parameters to be wrapped inside `stream`
  89. $connectionParameters = [
  90. 'stream' => $this->getSslContext($config)
  91. ];
  92. if ($persistent) {
  93. /**
  94. * even though the stubs and documentation don't want you to know this,
  95. * pconnect does have the same $connectionParameters argument connect has
  96. *
  97. * https://github.com/phpredis/phpredis/blob/0264de1824b03fb2d0ad515b4d4ec019cd2dae70/redis.c#L710-L730
  98. *
  99. * @psalm-suppress TooManyArguments
  100. */
  101. $this->instance->pconnect($host, $port, $timeout, null, 0, $readTimeout, $connectionParameters);
  102. } else {
  103. $this->instance->connect($host, $port, $timeout, null, 0, $readTimeout, $connectionParameters);
  104. }
  105. } else {
  106. if ($persistent) {
  107. $this->instance->pconnect($host, $port, $timeout, null, 0, $readTimeout);
  108. } else {
  109. $this->instance->connect($host, $port, $timeout, null, 0, $readTimeout);
  110. }
  111. }
  112. // Auth if configured
  113. if ($auth !== null) {
  114. $this->instance->auth($auth);
  115. }
  116. if (isset($config['dbindex'])) {
  117. $this->instance->select($config['dbindex']);
  118. }
  119. $this->eventLogger->end('connect:redis');
  120. }
  121. }
  122. /**
  123. * Get the ssl context config
  124. *
  125. * @param array $config the current config
  126. * @return array|null
  127. * @throws \UnexpectedValueException
  128. */
  129. private function getSslContext($config) {
  130. if (isset($config['ssl_context'])) {
  131. if (!$this->isConnectionParametersSupported()) {
  132. throw new \UnexpectedValueException(\sprintf(
  133. 'php-redis extension must be version %s or higher to support ssl context',
  134. self::REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION
  135. ));
  136. }
  137. return $config['ssl_context'];
  138. }
  139. return null;
  140. }
  141. public function getInstance() {
  142. if (!$this->isAvailable()) {
  143. throw new \Exception('Redis support is not available');
  144. }
  145. if (!$this->instance instanceof \Redis) {
  146. $this->create();
  147. }
  148. return $this->instance;
  149. }
  150. public function isAvailable(): bool {
  151. return \extension_loaded('redis') &&
  152. \version_compare(\phpversion('redis'), self::REDIS_MINIMAL_VERSION, '>=');
  153. }
  154. /**
  155. * Php redis does support configurable extra parameters since version 5.3.0, see: https://github.com/phpredis/phpredis#connect-open.
  156. * We need to check if the current version supports extra connection parameters, otherwise the connect method will throw an exception
  157. *
  158. * @return boolean
  159. */
  160. private function isConnectionParametersSupported(): bool {
  161. return \extension_loaded('redis') &&
  162. \version_compare(\phpversion('redis'), self::REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION, '>=');
  163. }
  164. }