From 84a08e0afeb7de159f2aaadac76863da5fe6bce3 Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Sat, 12 Apr 2025 11:58:13 +0200 Subject: [PATCH 01/13] Manually merged Sentinel support from https://github.com/ml65/prometheus_client_php fork --- README.md | 2 + src/Prometheus/Storage/Redis.php | 41 ++++++++ src/Prometheus/Storage/RedisSentinel.php | 126 +++++++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 src/Prometheus/Storage/RedisSentinel.php diff --git a/README.md b/README.md index 3866cfd4..65bada44 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,8 @@ Change the Redis options (the example shows the defaults): 'timeout' => 0.1, // in seconds 'read_timeout' => '10', // in seconds 'persistent_connections' => false + 'sentinels' => false, // support sentinel . Before requesting to redis, a request is made to the sentinel to get + // the address and port of the master redis server. ] ); ``` diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index b10e3f9c..eee318af 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -30,6 +30,8 @@ class Redis implements Adapter 'persistent_connections' => false, 'password' => null, 'user' => null, + 'sentinels' => null, // sentinels flag + 'master_name' => null, ]; /** @@ -59,9 +61,48 @@ class Redis implements Adapter public function __construct(array $options = []) { $this->options = array_merge(self::$defaultOptions, $options); + // is Sentinels ? + $this->isSentinels($this->options); $this->redis = new \Redis(); } + /** + * Sentinels descoverMaster + * @param array $options + */ + public function isSentinels(array $options = []) + { + if($options['sentinels']) { + + list($hostname, $port) = $this->discoveryMaster($options); + $options['host'] = $hostname; + $options['port'] = $port; + $options['sentinels'] = "false"; + } + return $options; + } + + /** + * Sentinels descoveryMaster + * @param mixed[] $options + * @throws StorageException + */ + public function discoveryMaster(array $options = []) + { + $connection = new Sentinel(); + $connection->hostname = $options['host'] ?? null; + $connection->masterName = $options['master_name']; + if (isset($options['port'])) { + $connection->port = $options['port']; + } + $connection->connectionTimeout = $options['connectionTimeout'] ?? null; + $r = $connection->getMaster(); + if (!$r) { + throw new StorageException('Connection to Redis Sentinel server not established'); + } + return $r; + } + /** * @param \Redis $redis * @return self diff --git a/src/Prometheus/Storage/RedisSentinel.php b/src/Prometheus/Storage/RedisSentinel.php new file mode 100644 index 00000000..35088016 --- /dev/null +++ b/src/Prometheus/Storage/RedisSentinel.php @@ -0,0 +1,126 @@ +_socket !== null) { + return; + } + $connection = $this->hostname . ':' . $this->port; + $this->_socket = @stream_socket_client('tcp://' . $this->hostname . ':' . $this->port, $errorNumber, $errorDescription, $this->connectionTimeout ? $this->connectionTimeout : ini_get("default_socket_timeout"), STREAM_CLIENT_CONNECT); + if ($this->_socket) { + if ($this->connectionTimeout !== null) { + stream_set_timeout($this->_socket, $timeout = (int) $this->connectionTimeout, (int) (($this->connectionTimeout - $timeout) * 1000000)); + } + return true; + } else { + $this->_socket = false; + return false; + } + } + + /** + * Asks sentinel to tell redis master server + * + * @return array|false [host,port] array or false if case of error + **/ + function getMaster () { + if ($this->open()) { + return $this->executeCommand('sentinel', [ + 'get-master-addr-by-name', + $this->masterName + ], $this->_socket); + } else { + return false; + } + } + + /** + * Execute redis command on socket and return parsed response + **/ + function executeCommand ($name, $params, $socket) { + $params = array_merge(explode(' ', $name), $params); + $command = '*' . count($params) . "\r\n"; + foreach ($params as $arg) { + $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n"; + } + + fwrite($socket, $command); + + return $this->parseResponse(implode(' ', $params), $socket); + } + + /** + * + * @param string $command + * @return mixed + * @throws StorageException + */ + function parseResponse ($command, $socket) { + if (($line = fgets($socket)) === false) { + throw new StorageException("Failed to read from socket.\nRedis command was: " . $command); + } + $type = $line[0]; + $line = mb_substr($line, 1, - 2, '8bit'); + switch ($type) { + case '+': // Status reply + if ($line === 'OK' || $line === 'PONG') { + return true; + } else { + return $line; + } + case '-': // Error reply + throw new StorageException("Redis error: " . $line . "\nRedis command was: " . $command); + case ':': // Integer reply + // no cast to int as it is in the range of a signed 64 bit integer + return $line; + case '$': // Bulk replies + if ($line == '-1') { + return null; + } + $length = $line + 2; + $data = ''; + while ($length > 0) { + if (($block = fread($socket, $length)) === false) { + throw new \Exception("Failed to read from socket.\nRedis command was: " . $command); + } + $data .= $block; + $length -= mb_strlen($block, '8bit'); + } + + return mb_substr($data, 0, - 2, '8bit'); + case '*': // Multi-bulk replies + $count = (int) $line; + $data = []; + for ($i = 0; $i < $count; $i ++) { + $data[] = $this->parseResponse($command, $socket); + } + + return $data; + default: + throw new StorageException('Received illegal data from redis: ' . $line . "\nRedis command was: " . $command); + } + } + +} From a50c2eabdc62806e61900946babc1ba4f24112c4 Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Sat, 12 Apr 2025 13:15:43 +0200 Subject: [PATCH 02/13] Code refactor tested connection to sentinel/redis instance --- README.md | 10 +++- src/Prometheus/Storage/Redis.php | 36 +++---------- src/Prometheus/Storage/RedisSentinel.php | 69 ++++++++++++++---------- 3 files changed, 56 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 65bada44..e2316d84 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,14 @@ Change the Redis options (the example shows the defaults): 'timeout' => 0.1, // in seconds 'read_timeout' => '10', // in seconds 'persistent_connections' => false - 'sentinels' => false, // support sentinel . Before requesting to redis, a request is made to the sentinel to get - // the address and port of the master redis server. + 'sentinel' => [ + 'enabled' => false, // support sentinel . Before requesting to redis, a request is made to the sentinel to get + 'host' => '127.0.0.1', // the address of the redis, defualt is the same as redis host if empty + 'port' => 26379, // the port of the master redis server, default 26379 if empty. + 'master' => 'mymaster' //, sentine master name, default mymaster + 'timeout' => 0.1 //, sentinel connection timeout + 'read_timeout' => null // sentinel read timeout + ] ] ); ``` diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index eee318af..0f6d7517 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -62,7 +62,7 @@ public function __construct(array $options = []) { $this->options = array_merge(self::$defaultOptions, $options); // is Sentinels ? - $this->isSentinels($this->options); + $this->options = $this->isSentinel($this->options); $this->redis = new \Redis(); } @@ -70,39 +70,17 @@ public function __construct(array $options = []) * Sentinels descoverMaster * @param array $options */ - public function isSentinels(array $options = []) + public function isSentinel(array $options = []) { - if($options['sentinels']) { - - list($hostname, $port) = $this->discoveryMaster($options); + if($options['sentinel']) { + $sentinel = new RedisSentinel($options['sentinel'],$options['host']); + list($hostname, $port) = $sentinel->getMaster($options); $options['host'] = $hostname; $options['port'] = $port; - $options['sentinels'] = "false"; } return $options; } - /** - * Sentinels descoveryMaster - * @param mixed[] $options - * @throws StorageException - */ - public function discoveryMaster(array $options = []) - { - $connection = new Sentinel(); - $connection->hostname = $options['host'] ?? null; - $connection->masterName = $options['master_name']; - if (isset($options['port'])) { - $connection->port = $options['port']; - } - $connection->connectionTimeout = $options['connectionTimeout'] ?? null; - $r = $connection->getMaster(); - if (!$r) { - throw new StorageException('Connection to Redis Sentinel server not established'); - } - return $r; - } - /** * @param \Redis $redis * @return self @@ -168,7 +146,7 @@ public function wipeStorage(): void <<redis->getLastError()), - 0 + null ); } } catch (\RedisException $e) { diff --git a/src/Prometheus/Storage/RedisSentinel.php b/src/Prometheus/Storage/RedisSentinel.php index 35088016..b2705308 100644 --- a/src/Prometheus/Storage/RedisSentinel.php +++ b/src/Prometheus/Storage/RedisSentinel.php @@ -1,25 +1,36 @@ 'mymaster', + 'host' => null, + 'port' => 26379, + 'timeout' => 0.1, + 'read_timeout' => null, + ]; + + public function __construct(array $options = [], $host = null) + { + $this->options = array_merge(self::$defaultOptions, $options); + if(!isset($this->options['host'])) { + $this->options['host'] = $host; + } + } /** * Connects to redis sentinel **/ @@ -27,16 +38,20 @@ protected function open () { if ($this->_socket !== null) { return; } - $connection = $this->hostname . ':' . $this->port; - $this->_socket = @stream_socket_client('tcp://' . $this->hostname . ':' . $this->port, $errorNumber, $errorDescription, $this->connectionTimeout ? $this->connectionTimeout : ini_get("default_socket_timeout"), STREAM_CLIENT_CONNECT); - if ($this->_socket) { - if ($this->connectionTimeout !== null) { - stream_set_timeout($this->_socket, $timeout = (int) $this->connectionTimeout, (int) (($this->connectionTimeout - $timeout) * 1000000)); - } - return true; - } else { - $this->_socket = false; - return false; + $connection = $this->options['host'] . ':' . $this->options['port']; + $connectionTimeout = $this->options['timeout'] ? $this->options['timeout'] : ini_get("default_socket_timeout"); + $address = 'tcp://' . $this->options['host'] . ':' . $this->options['port']; + $this->_socket = @stream_socket_client($address, $errorNumber, $errorDescription, $connectionTimeout, STREAM_CLIENT_CONNECT); + if(!$this->_socket){ + throw new StorageException(sprintf("Can't connect to Redis server '$address'. %s", $errorDescription), + $errorNumber, + null); + } + + if ($this->options['timeout'] !== null) { + $timeoutSeconds = (int) $this->options['timeout']; + $timeoutMicroseconds = (int) (($this->options['timeout'] - $timeoutSeconds) * 1000000); + stream_set_timeout($this->_socket, $timeoutSeconds, $timeoutMicroseconds); } } @@ -46,14 +61,12 @@ protected function open () { * @return array|false [host,port] array or false if case of error **/ function getMaster () { - if ($this->open()) { - return $this->executeCommand('sentinel', [ - 'get-master-addr-by-name', - $this->masterName - ], $this->_socket); - } else { - return false; - } + $this->open(); + + return $this->executeCommand('sentinel', [ + 'get-master-addr-by-name', + $this->options['master'] + ], $this->_socket); } /** From 951eb46d8fc60da39452339d77500a275fcc931a Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Sat, 12 Apr 2025 13:45:31 +0200 Subject: [PATCH 03/13] Removed old default sentinels options --- src/Prometheus/Storage/Redis.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 0f6d7517..92f6e92a 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -30,8 +30,7 @@ class Redis implements Adapter 'persistent_connections' => false, 'password' => null, 'user' => null, - 'sentinels' => null, // sentinels flag - 'master_name' => null, + 'sentinel' => null, // sentinel options ]; /** From 184754106ea84447f59c4a30ffa0a1c9ad1fb03a Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Sat, 12 Apr 2025 13:50:43 +0200 Subject: [PATCH 04/13] propagated sentinel default options to redis --- README.md | 2 +- src/Prometheus/Storage/Redis.php | 11 +++++++++-- src/Prometheus/Storage/RedisSentinel.php | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e2316d84..73b88485 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Change the Redis options (the example shows the defaults): 'read_timeout' => '10', // in seconds 'persistent_connections' => false 'sentinel' => [ - 'enabled' => false, // support sentinel . Before requesting to redis, a request is made to the sentinel to get + 'enable' => false, // support sentinel . Before requesting to redis, a request is made to the sentinel to get 'host' => '127.0.0.1', // the address of the redis, defualt is the same as redis host if empty 'port' => 26379, // the port of the master redis server, default 26379 if empty. 'master' => 'mymaster' //, sentine master name, default mymaster diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 92f6e92a..d5e3a6d2 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -30,7 +30,14 @@ class Redis implements Adapter 'persistent_connections' => false, 'password' => null, 'user' => null, - 'sentinel' => null, // sentinel options + 'sentinel' => [ // sentinel options + 'enable' => false, + 'host' => null + 'port' => 26379, + 'master' => 'mymaster', + 'timeout' => 0.1, + 'read_timeout' => null, + ], ]; /** @@ -71,7 +78,7 @@ public function __construct(array $options = []) */ public function isSentinel(array $options = []) { - if($options['sentinel']) { + if($options['sentinel'] && $options['sentinel']['enable']){ $sentinel = new RedisSentinel($options['sentinel'],$options['host']); list($hostname, $port) = $sentinel->getMaster($options); $options['host'] = $hostname; diff --git a/src/Prometheus/Storage/RedisSentinel.php b/src/Prometheus/Storage/RedisSentinel.php index b2705308..9d930fdf 100644 --- a/src/Prometheus/Storage/RedisSentinel.php +++ b/src/Prometheus/Storage/RedisSentinel.php @@ -17,9 +17,10 @@ class RedisSentinel protected $_socket; private static $defaultOptions = [ - 'master' => 'mymaster', + 'enable' => false, 'host' => null, 'port' => 26379, + 'master' => 'mymaster', 'timeout' => 0.1, 'read_timeout' => null, ]; From 9cd774bf95d8a3e064a18c273ee5340442ed5fa5 Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Sat, 12 Apr 2025 14:03:36 +0200 Subject: [PATCH 05/13] fix typo --- src/Prometheus/Storage/Redis.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index d5e3a6d2..4f9d9034 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -32,7 +32,7 @@ class Redis implements Adapter 'user' => null, 'sentinel' => [ // sentinel options 'enable' => false, - 'host' => null + 'host' => null, 'port' => 26379, 'master' => 'mymaster', 'timeout' => 0.1, From 6ee5e4da237366823e5d9a09b92f877b0b589b3c Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:56:43 +0200 Subject: [PATCH 06/13] Refactor of RedisSentinel to use phpredis as in https://github.com/Namoshek/laravel-redis-sentinel --- README.md | 21 ++- src/Prometheus/Storage/Redis.php | 124 +++++++++++----- src/Prometheus/Storage/RedisSentinel.php | 140 ------------------ src/Prometheus/Storage/RedisSentinelConnector | 95 ++++++++++++ 4 files changed, 195 insertions(+), 185 deletions(-) delete mode 100644 src/Prometheus/Storage/RedisSentinel.php create mode 100644 src/Prometheus/Storage/RedisSentinelConnector diff --git a/README.md b/README.md index 73b88485..1746a26f 100644 --- a/README.md +++ b/README.md @@ -79,14 +79,19 @@ Change the Redis options (the example shows the defaults): 'timeout' => 0.1, // in seconds 'read_timeout' => '10', // in seconds 'persistent_connections' => false - 'sentinel' => [ - 'enable' => false, // support sentinel . Before requesting to redis, a request is made to the sentinel to get - 'host' => '127.0.0.1', // the address of the redis, defualt is the same as redis host if empty - 'port' => 26379, // the port of the master redis server, default 26379 if empty. - 'master' => 'mymaster' //, sentine master name, default mymaster - 'timeout' => 0.1 //, sentinel connection timeout - 'read_timeout' => null // sentinel read timeout - ] + 'sentinel' => [ // sentinel options + 'enable' => false, // if enabled uses sentinel to get the master before connecting to redis + 'host' => '127.0.0.1', // phpredis sentinel address of the redis, default is the same as redis host if empty + 'port' => 26379, // phpredis sentinel port of the primary redis server, default 26379 if empty. + 'service' => 'myprimary', //, phpredis sentinel primary name, default myprimary + 'timeout' => 0, // phpredis sentinel connection timeout + 'persistent' => null, // phpredis sentinel persistence parameter + 'retry_interval' => 0, // phpredis sentinel retry interval + 'read_timeout' => 0, // phpredis sentinel read timeout + 'username' => '', // phpredis sentinel auth username + 'password' => '', // phpredis sentinel auth password + 'ssl' => null, + ] ] ); ``` diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 4f9d9034..a7adcad7 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -31,15 +31,36 @@ class Redis implements Adapter 'password' => null, 'user' => null, 'sentinel' => [ // sentinel options - 'enable' => false, - 'host' => null, - 'port' => 26379, - 'master' => 'mymaster', - 'timeout' => 0.1, - 'read_timeout' => null, - ], + 'enable' => false, // if enabled uses sentinel to get the master before connecting to redis + 'host' => '127.0.0.1', // phpredis sentinel address of the redis, default is the same as redis host if empty + 'port' => 26379, // phpredis sentinel port of the primary redis server, default 26379 if empty. + 'service' => 'myprimary', //, phpredis sentinel primary name, default myprimary + 'timeout' => 0, // phpredis sentinel connection timeout + 'persistent' => null, // phpredis sentinel persistence parameter + 'retry_interval' => 0, // phpredis sentinel retry interval + 'read_timeout' => 0, // phpredis sentinel read timeout + 'username' => '', // phpredis sentinel auth username + 'password' => '', // phpredis sentinel auth password + 'ssl' => null, + ] ]; + // The following array contains all exception message parts which are interpreted as a connection loss or + // another unavailability of Redis. + private const ERROR_MESSAGES_INDICATING_UNAVAILABILITY = [ + 'connection closed', + 'connection refused', + 'connection lost', + 'failed while reconnecting', + 'is loading the dataset in memory', + 'php_network_getaddresses', + 'read error on connection', + 'socket', + 'went away', + 'loading', + 'readonly', + "can't write against a read only replica", + ]; /** * @var string */ @@ -67,8 +88,6 @@ class Redis implements Adapter public function __construct(array $options = []) { $this->options = array_merge(self::$defaultOptions, $options); - // is Sentinels ? - $this->options = $this->isSentinel($this->options); $this->redis = new \Redis(); } @@ -78,11 +97,12 @@ public function __construct(array $options = []) */ public function isSentinel(array $options = []) { - if($options['sentinel'] && $options['sentinel']['enable']){ - $sentinel = new RedisSentinel($options['sentinel'],$options['host']); - list($hostname, $port) = $sentinel->getMaster($options); - $options['host'] = $hostname; - $options['port'] = $port; + if ($options['sentinel'] && $options['sentinel']['enable']) { + $sentinel = new RedisSentinelConnector(); + $options['sentinel']['host'] = $options['sentinel']['host'] ?? $options['host']; + $master = $sentinel->getMaster($options['sentinel']); + $options['host'] = $master['ip']; + $options['port'] = $master['port']; } return $options; } @@ -122,8 +142,8 @@ public static function setPrefix(string $prefix): void } /** - * @deprecated use replacement method wipeStorage from Adapter interface * @throws StorageException + * @deprecated use replacement method wipeStorage from Adapter interface */ public function flushRedis(): void { @@ -212,6 +232,27 @@ function (array $metric): MetricFamilySamples { ); } + /** + * Inspects the given exception and reconnects the client if the reported error indicates that the server + * went away or is in readonly mode, which may happen in case of a Redis Sentinel failover. + */ + private function reconnectIfRedisIsUnavailableOrReadonly(RedisException $exception): bool + { + // We convert the exception message to lower-case in order to perform case-insensitive comparison. + $exceptionMessage = strtolower($exception->getMessage()); + + // Because we also match only partial exception messages, we cannot use in_array() at this point. + foreach (self::ERROR_MESSAGES_INDICATING_UNAVAILABILITY as $errorMessage) { + if (str_contains($exceptionMessage, $errorMessage)) { + // Here we reconnect through Redis Sentinel if we lost connection to the server or if another unavailability occurred. + // We may actually reconnect to the same, broken server. But after a failover occured, we should be ok. + // It may take a moment until the Sentinel returns the new master, so this may be triggered multiple times. + return true; + } + } + return false; + } + /** * @throws StorageException */ @@ -221,7 +262,24 @@ private function ensureOpenConnection(): void return; } - $this->connectToServer(); + while (true) { + try { + $this->options = $this->isSentinel($this->options); + $this->connectToServer(); + break; + } catch (\RedisException $e) { + $retry = $this->reconnectIfRedisIsUnavailableOrReadonly($e); + if (!$retry) { + throw new StorageException( + sprintf("Can't connect to Redis server. %s", $e->getMessage()), + $e->getCode(), + $e + ); + } + } + } + + $authParams = []; if (isset($this->options['user']) && $this->options['user'] !== '') { @@ -250,28 +308,20 @@ private function ensureOpenConnection(): void */ private function connectToServer(): void { - try { - $connection_successful = false; - if ($this->options['persistent_connections'] !== false) { - $connection_successful = $this->redis->pconnect( - $this->options['host'], - (int) $this->options['port'], - (float) $this->options['timeout'] - ); - } else { - $connection_successful = $this->redis->connect($this->options['host'], (int) $this->options['port'], (float) $this->options['timeout']); - } - if (!$connection_successful) { - throw new StorageException( - sprintf("Can't connect to Redis server. %s", $this->redis->getLastError()), - null - ); - } - } catch (\RedisException $e) { + $connection_successful = false; + if ($this->options['persistent_connections'] !== false) { + $connection_successful = $this->redis->pconnect( + $this->options['host'], + (int) $this->options['port'], + (float) $this->options['timeout'] + ); + } else { + $connection_successful = $this->redis->connect($this->options['host'], (int) $this->options['port'], (float) $this->options['timeout']); + } + if (!$connection_successful) { throw new StorageException( - sprintf("Can't connect to Redis server. %s", $e->getMessage()), - $e->getCode(), - $e + sprintf("Can't connect to Redis server. %s", $this->redis->getLastError()), + null ); } } diff --git a/src/Prometheus/Storage/RedisSentinel.php b/src/Prometheus/Storage/RedisSentinel.php deleted file mode 100644 index 9d930fdf..00000000 --- a/src/Prometheus/Storage/RedisSentinel.php +++ /dev/null @@ -1,140 +0,0 @@ - false, - 'host' => null, - 'port' => 26379, - 'master' => 'mymaster', - 'timeout' => 0.1, - 'read_timeout' => null, - ]; - - public function __construct(array $options = [], $host = null) - { - $this->options = array_merge(self::$defaultOptions, $options); - if(!isset($this->options['host'])) { - $this->options['host'] = $host; - } - } - /** - * Connects to redis sentinel - **/ - protected function open () { - if ($this->_socket !== null) { - return; - } - $connection = $this->options['host'] . ':' . $this->options['port']; - $connectionTimeout = $this->options['timeout'] ? $this->options['timeout'] : ini_get("default_socket_timeout"); - $address = 'tcp://' . $this->options['host'] . ':' . $this->options['port']; - $this->_socket = @stream_socket_client($address, $errorNumber, $errorDescription, $connectionTimeout, STREAM_CLIENT_CONNECT); - if(!$this->_socket){ - throw new StorageException(sprintf("Can't connect to Redis server '$address'. %s", $errorDescription), - $errorNumber, - null); - } - - if ($this->options['timeout'] !== null) { - $timeoutSeconds = (int) $this->options['timeout']; - $timeoutMicroseconds = (int) (($this->options['timeout'] - $timeoutSeconds) * 1000000); - stream_set_timeout($this->_socket, $timeoutSeconds, $timeoutMicroseconds); - } - } - - /** - * Asks sentinel to tell redis master server - * - * @return array|false [host,port] array or false if case of error - **/ - function getMaster () { - $this->open(); - - return $this->executeCommand('sentinel', [ - 'get-master-addr-by-name', - $this->options['master'] - ], $this->_socket); - } - - /** - * Execute redis command on socket and return parsed response - **/ - function executeCommand ($name, $params, $socket) { - $params = array_merge(explode(' ', $name), $params); - $command = '*' . count($params) . "\r\n"; - foreach ($params as $arg) { - $command .= '$' . mb_strlen($arg, '8bit') . "\r\n" . $arg . "\r\n"; - } - - fwrite($socket, $command); - - return $this->parseResponse(implode(' ', $params), $socket); - } - - /** - * - * @param string $command - * @return mixed - * @throws StorageException - */ - function parseResponse ($command, $socket) { - if (($line = fgets($socket)) === false) { - throw new StorageException("Failed to read from socket.\nRedis command was: " . $command); - } - $type = $line[0]; - $line = mb_substr($line, 1, - 2, '8bit'); - switch ($type) { - case '+': // Status reply - if ($line === 'OK' || $line === 'PONG') { - return true; - } else { - return $line; - } - case '-': // Error reply - throw new StorageException("Redis error: " . $line . "\nRedis command was: " . $command); - case ':': // Integer reply - // no cast to int as it is in the range of a signed 64 bit integer - return $line; - case '$': // Bulk replies - if ($line == '-1') { - return null; - } - $length = $line + 2; - $data = ''; - while ($length > 0) { - if (($block = fread($socket, $length)) === false) { - throw new \Exception("Failed to read from socket.\nRedis command was: " . $command); - } - $data .= $block; - $length -= mb_strlen($block, '8bit'); - } - - return mb_substr($data, 0, - 2, '8bit'); - case '*': // Multi-bulk replies - $count = (int) $line; - $data = []; - for ($i = 0; $i < $count; $i ++) { - $data[] = $this->parseResponse($command, $socket); - } - - return $data; - default: - throw new StorageException('Received illegal data from redis: ' . $line . "\nRedis command was: " . $command); - } - } - -} diff --git a/src/Prometheus/Storage/RedisSentinelConnector b/src/Prometheus/Storage/RedisSentinelConnector new file mode 100644 index 00000000..3485ed0a --- /dev/null +++ b/src/Prometheus/Storage/RedisSentinelConnector @@ -0,0 +1,95 @@ +connectToSentinel($config); + + $master = $sentinel->master($service); + + if (! $this->isValidMaster($master)) { + throw new StorageException(sprintf("No master found for service '%s'.", $service)); + } + + return $master; + } + + /** + * Check whether master is valid or not. + */ + protected function isValidMaster(mixed $master): bool + { + return is_array($master) && isset($master['ip']) && isset($master['port']); + } + + /** + * Connect to the configured Redis Sentinel instance. + * + * @throws StorageException + */ + private function connectToSentinel(array $config): RedisSentinel + { + $host = $config['host'] ?? ''; + $port = $config['port'] ?? 26379; + $timeout = $config['timeout'] ?? 0.2; + $persistent = $config['persistent'] ?? null; + $retryInterval = $config['retry_interval'] ?? 0; + $readTimeout = $config['read_timeout'] ?? 0; + $username = $config['username'] ?? ''; + $password = $config['password'] ?? ''; + $ssl = $config['ssl'] ?? null; + + if (strlen(trim($host)) === 0) { + throw new StorageException('No host has been specified for the Redis Sentinel connection.'); + } + + $auth = null; + if (strlen(trim($username)) !== 0 && strlen(trim($password)) !== 0) { + $auth = [$username, $password]; + } elseif (strlen(trim($password)) !== 0) { + $auth = $password; + } + + if (version_compare(phpversion('redis'), '6.0', '>=')) { + $options = [ + 'host' => $host, + 'port' => $port, + 'connectTimeout' => $timeout, + 'persistent' => $persistent, + 'retryInterval' => $retryInterval, + 'readTimeout' => $readTimeout, + ]; + + if ($auth !== null) { + $options['auth'] = $auth; + } + + if (version_compare(phpversion('redis'), '6.1', '>=') && $ssl !== null) { + $options['ssl'] = $ssl; + } + + return new RedisSentinel($options); + } + + if ($auth !== null) { + /** @noinspection PhpMethodParametersCountMismatchInspection */ + return new RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout, $auth); + } + + return new RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout); + } +} From da2ef427d7f320685663a1aea0ec9f09bb816ef4 Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Tue, 15 Apr 2025 18:20:45 +0200 Subject: [PATCH 07/13] Update and rename RedisSentinelConnector to RedisSentinelConnector.php --- .../{RedisSentinelConnector => RedisSentinelConnector.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Prometheus/Storage/{RedisSentinelConnector => RedisSentinelConnector.php} (100%) diff --git a/src/Prometheus/Storage/RedisSentinelConnector b/src/Prometheus/Storage/RedisSentinelConnector.php similarity index 100% rename from src/Prometheus/Storage/RedisSentinelConnector rename to src/Prometheus/Storage/RedisSentinelConnector.php From 1f4c1fe72dd65b8bc24f2c319662fefb446e87b5 Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Thu, 17 Apr 2025 11:50:30 +0000 Subject: [PATCH 08/13] added reconnect parameter --- README.md | 1 + src/Prometheus/Storage/Redis.php | 49 ++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 1746a26f..eb4fc026 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ Change the Redis options (the example shows the defaults): 'persistent' => null, // phpredis sentinel persistence parameter 'retry_interval' => 0, // phpredis sentinel retry interval 'read_timeout' => 0, // phpredis sentinel read timeout + 'reconnect' => 0, // retries after losing connection to redis asking for a new primary, if -1 will retry indefinetely 'username' => '', // phpredis sentinel auth username 'password' => '', // phpredis sentinel auth password 'ssl' => null, diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index a7adcad7..825d44fc 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -39,6 +39,7 @@ class Redis implements Adapter 'persistent' => null, // phpredis sentinel persistence parameter 'retry_interval' => 0, // phpredis sentinel retry interval 'read_timeout' => 0, // phpredis sentinel read timeout + 'reconnect' => 0, // retries after losing connection to redis asking for a new primary, if -1 will retry indefinetely 'username' => '', // phpredis sentinel auth username 'password' => '', // phpredis sentinel auth password 'ssl' => null, @@ -95,15 +96,13 @@ public function __construct(array $options = []) * Sentinels descoverMaster * @param array $options */ - public function isSentinel(array $options = []) + public function getSentinelPrimary(array $options = []) { - if ($options['sentinel'] && $options['sentinel']['enable']) { - $sentinel = new RedisSentinelConnector(); - $options['sentinel']['host'] = $options['sentinel']['host'] ?? $options['host']; - $master = $sentinel->getMaster($options['sentinel']); - $options['host'] = $master['ip']; - $options['port'] = $master['port']; - } + $sentinel = new RedisSentinelConnector(); + $options['sentinel']['host'] = $options['sentinel']['host'] ?? $options['host']; + $master = $sentinel->getMaster($options['sentinel']); + $options['host'] = $master['ip']; + $options['port'] = $master['port']; return $options; } @@ -262,22 +261,30 @@ private function ensureOpenConnection(): void return; } - while (true) { - try { - $this->options = $this->isSentinel($this->options); - $this->connectToServer(); - break; - } catch (\RedisException $e) { - $retry = $this->reconnectIfRedisIsUnavailableOrReadonly($e); - if (!$retry) { - throw new StorageException( - sprintf("Can't connect to Redis server. %s", $e->getMessage()), - $e->getCode(), - $e - ); + if($this->options['sentinel'] && $this->options['sentinel']['enable']){ + $reconnect = $this->options['sentinel']['reconnect']; + $retries = 0; + while ($retries<=$reconnect) { + try { + $this->options = $this->getSentinelPrimary($this->options); + $this->connectToServer(); + break; + } catch (\RedisException $e) { + $retry = $this->reconnectIfRedisIsUnavailableOrReadonly($e); + if (!$retry) { + throw new StorageException( + sprintf("Can't connect to Redis server. %s", $e->getMessage()), + $e->getCode(), + $e + ); + } } + $retries++; } + } else { + $this->connectToServer(); } + $authParams = []; From 4f42c317bb90ddfb7e55c412247a3a65c6f3e7be Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Thu, 17 Apr 2025 12:22:40 +0000 Subject: [PATCH 09/13] code cleaning --- .vscode/settings.json | 3 + src/Prometheus/Storage/Redis.php | 23 ++- .../Storage/RedisSentinelConnector.php | 193 +++++++++--------- 3 files changed, 113 insertions(+), 106 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..9fce7cc6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "github-actions.workflows.pinned.workflows": [] +} \ No newline at end of file diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 825d44fc..59435993 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -94,15 +94,18 @@ public function __construct(array $options = []) /** * Sentinels descoverMaster - * @param array $options + * @param mixed[] $options + * @return mixed[] */ - public function getSentinelPrimary(array $options = []) + public function getSentinelPrimary(array $options = []) : array { $sentinel = new RedisSentinelConnector(); $options['sentinel']['host'] = $options['sentinel']['host'] ?? $options['host']; $master = $sentinel->getMaster($options['sentinel']); - $options['host'] = $master['ip']; - $options['port'] = $master['port']; + if (is_array($master)) { + $options['host'] = $master['ip']; + $options['port'] = $master['port']; + } return $options; } @@ -235,7 +238,7 @@ function (array $metric): MetricFamilySamples { * Inspects the given exception and reconnects the client if the reported error indicates that the server * went away or is in readonly mode, which may happen in case of a Redis Sentinel failover. */ - private function reconnectIfRedisIsUnavailableOrReadonly(RedisException $exception): bool + private function reconnectIfRedisIsUnavailableOrReadonly(\RedisException $exception): bool { // We convert the exception message to lower-case in order to perform case-insensitive comparison. $exceptionMessage = strtolower($exception->getMessage()); @@ -261,10 +264,10 @@ private function ensureOpenConnection(): void return; } - if($this->options['sentinel'] && $this->options['sentinel']['enable']){ + if (isset($this->options['sentinel']) && boolval($this->options['sentinel']['enable'])) { $reconnect = $this->options['sentinel']['reconnect']; $retries = 0; - while ($retries<=$reconnect) { + while ($retries <= $reconnect) { try { $this->options = $this->getSentinelPrimary($this->options); $this->connectToServer(); @@ -279,13 +282,11 @@ private function ensureOpenConnection(): void ); } } - $retries++; + $retries++; } } else { $this->connectToServer(); } - - $authParams = []; @@ -328,7 +329,7 @@ private function connectToServer(): void if (!$connection_successful) { throw new StorageException( sprintf("Can't connect to Redis server. %s", $this->redis->getLastError()), - null + 0 ); } } diff --git a/src/Prometheus/Storage/RedisSentinelConnector.php b/src/Prometheus/Storage/RedisSentinelConnector.php index 3485ed0a..60882a64 100644 --- a/src/Prometheus/Storage/RedisSentinelConnector.php +++ b/src/Prometheus/Storage/RedisSentinelConnector.php @@ -1,95 +1,98 @@ -connectToSentinel($config); - - $master = $sentinel->master($service); - - if (! $this->isValidMaster($master)) { - throw new StorageException(sprintf("No master found for service '%s'.", $service)); - } - - return $master; - } - - /** - * Check whether master is valid or not. - */ - protected function isValidMaster(mixed $master): bool - { - return is_array($master) && isset($master['ip']) && isset($master['port']); - } - - /** - * Connect to the configured Redis Sentinel instance. - * - * @throws StorageException - */ - private function connectToSentinel(array $config): RedisSentinel - { - $host = $config['host'] ?? ''; - $port = $config['port'] ?? 26379; - $timeout = $config['timeout'] ?? 0.2; - $persistent = $config['persistent'] ?? null; - $retryInterval = $config['retry_interval'] ?? 0; - $readTimeout = $config['read_timeout'] ?? 0; - $username = $config['username'] ?? ''; - $password = $config['password'] ?? ''; - $ssl = $config['ssl'] ?? null; - - if (strlen(trim($host)) === 0) { - throw new StorageException('No host has been specified for the Redis Sentinel connection.'); - } - - $auth = null; - if (strlen(trim($username)) !== 0 && strlen(trim($password)) !== 0) { - $auth = [$username, $password]; - } elseif (strlen(trim($password)) !== 0) { - $auth = $password; - } - - if (version_compare(phpversion('redis'), '6.0', '>=')) { - $options = [ - 'host' => $host, - 'port' => $port, - 'connectTimeout' => $timeout, - 'persistent' => $persistent, - 'retryInterval' => $retryInterval, - 'readTimeout' => $readTimeout, - ]; - - if ($auth !== null) { - $options['auth'] = $auth; - } - - if (version_compare(phpversion('redis'), '6.1', '>=') && $ssl !== null) { - $options['ssl'] = $ssl; - } - - return new RedisSentinel($options); - } - - if ($auth !== null) { - /** @noinspection PhpMethodParametersCountMismatchInspection */ - return new RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout, $auth); - } - - return new RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout); - } -} +connectToSentinel($config); + + $master = $sentinel->master($service); + + if (! $this->isValidMaster($master)) { + throw new StorageException(sprintf("No master found for service '%s'.", $service)); + } + + return $master; + } + + /** + * Check whether master is valid or not. + * @param mixed[]|bool $master + * @return bool + */ + protected function isValidMaster(array|bool $master): bool + { + return is_array($master) && isset($master['ip']) && isset($master['port']); + } + + /** + * Connect to the configured Redis Sentinel instance. + * @param mixed[] $config + * @return RedisSentinel + * @throws StorageException + */ + private function connectToSentinel(array $config): RedisSentinel + { + $host = $config['host'] ?? ''; + $port = $config['port'] ?? 26379; + $timeout = $config['timeout'] ?? 0.2; + $persistent = $config['persistent'] ?? null; + $retryInterval = $config['retry_interval'] ?? 0; + $readTimeout = $config['read_timeout'] ?? 0; + $username = $config['username'] ?? ''; + $password = $config['password'] ?? ''; + $ssl = $config['ssl'] ?? null; + + if (strlen(trim($host)) === 0) { + throw new StorageException('No host has been specified for the Redis Sentinel connection.'); + } + + $auth = null; + if (strlen(trim($username)) !== 0 && strlen(trim($password)) !== 0) { + $auth = [$username, $password]; + } elseif (strlen(trim($password)) !== 0) { + $auth = $password; + } + + if (version_compare((string)phpversion('redis'), '6.0', '>=')) { + $options = [ + 'host' => $host, + 'port' => $port, + 'connectTimeout' => $timeout, + 'persistent' => $persistent, + 'retryInterval' => $retryInterval, + 'readTimeout' => $readTimeout, + ]; + + if ($auth !== null) { + $options['auth'] = $auth; + } + + if (version_compare((string)phpversion('redis'), '6.1', '>=') && $ssl !== null) { + $options['ssl'] = $ssl; + } + + return new RedisSentinel($options); + } + + if ($auth !== null) { + /** @noinspection PhpMethodParametersCountMismatchInspection */ + return new RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout, $auth); + } + + return new RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout, null); + } +} From 57f2c2e7595d4cc58b290e986e7e499c8e276c75 Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:35:40 +0000 Subject: [PATCH 10/13] adding sentinel tests --- docker-compose.yml | 10 ++ sentinel.conf | 6 + src/Prometheus/Storage/Redis.php | 53 ++++-- src/Prometheus/Storage/RedisSentinel.php | 161 ++++++++++++++++++ .../Storage/RedisSentinelConnector.php | 98 ----------- .../Prometheus/Storage/RedisSentinelTest.php | 94 ++++++++++ tests/bootstrap.php | 1 + 7 files changed, 312 insertions(+), 111 deletions(-) create mode 100644 sentinel.conf create mode 100644 src/Prometheus/Storage/RedisSentinel.php delete mode 100644 src/Prometheus/Storage/RedisSentinelConnector.php create mode 100644 tests/Test/Prometheus/Storage/RedisSentinelTest.php diff --git a/docker-compose.yml b/docker-compose.yml index c6a794b0..b448f69d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,12 +14,21 @@ services: - redis environment: - REDIS_HOST=redis + - REDIS_SENTINEL_HOST=redis-sentinel redis: image: redis ports: - 6379:6379 + redis-sentinel: + image: redis + volumes: + - ./sentinel.conf://etc/sentinel.conf + ports: + - 26379:26379 + command: redis-sentinel /etc/sentinel.conf + phpunit: build: php-fpm/ volumes: @@ -29,3 +38,4 @@ services: - nginx environment: - REDIS_HOST=redis + - REDIS_SENTINEL_HOST=redis-sentinel diff --git a/sentinel.conf b/sentinel.conf new file mode 100644 index 00000000..5f370dfd --- /dev/null +++ b/sentinel.conf @@ -0,0 +1,6 @@ +bind 0.0.0.0 +sentinel monitor myprimary redis 6379 2 +sentinel resolve-hostnames yes +sentinel down-after-milliseconds myprimary 10000 +sentinel failover-timeout myprimary 10000 +sentinel parallel-syncs myprimary 1 \ No newline at end of file diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 59435993..603b8f39 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -77,6 +77,11 @@ class Redis implements Adapter */ private $redis; + /** + * @var RedisSentinel + */ + private $sentinel; + /** * @var boolean */ @@ -88,20 +93,23 @@ class Redis implements Adapter */ public function __construct(array $options = []) { - $this->options = array_merge(self::$defaultOptions, $options); + $this->options = [...self::$defaultOptions, ...$options]; + $this->options['sentinel'] = [...self::$defaultOptions['sentinel'] ?? [], ...$options['sentinel'] ?? []]; $this->redis = new \Redis(); + if(isset($this->options['sentinel']) && boolval($this->options['sentinel']['enable'])){ + $options['sentinel']['host'] = $options['sentinel']['host'] ?? $options['host']; + $this->sentinel = new RedisSentinel($options['sentinel']); + } } /** - * Sentinels descoverMaster - * @param mixed[] $options + * Sentinels discoverMaster * @return mixed[] */ - public function getSentinelPrimary(array $options = []) : array - { - $sentinel = new RedisSentinelConnector(); - $options['sentinel']['host'] = $options['sentinel']['host'] ?? $options['host']; - $master = $sentinel->getMaster($options['sentinel']); + public function getSentinelPrimary(): array + { + $master = $this->sentinel->getMaster(); + if (is_array($master)) { $options['host'] = $master['ip']; $options['port'] = $master['port']; @@ -109,13 +117,25 @@ public function getSentinelPrimary(array $options = []) : array return $options; } + /** + * @return \RedisSentinel + */ + public function getRedisSentinel() : \RedisSentinel { + return $this->sentinel->getRedisSentinel(); + } + /** * @param \Redis $redis + * @param \RedisSentinel $redisSentinel * @return self * @throws StorageException */ - public static function fromExistingConnection(\Redis $redis): self + public static function fromExistingConnection(\Redis $redis, \RedisSentinel $redisSentinel = null): self { + if($redisSentinel) { + RedisSentinel::fromExistingConnection($redisSentinel); + } + if ($redis->isConnected() === false) { throw new StorageException('Connection to Redis server not established'); } @@ -263,13 +283,13 @@ private function ensureOpenConnection(): void if ($this->connectionInitialized === true) { return; } - - if (isset($this->options['sentinel']) && boolval($this->options['sentinel']['enable'])) { + + if ($this->sentinel) { $reconnect = $this->options['sentinel']['reconnect']; $retries = 0; while ($retries <= $reconnect) { try { - $this->options = $this->getSentinelPrimary($this->options); + $this->options = $this->getSentinelPrimary(); $this->connectToServer(); break; } catch (\RedisException $e) { @@ -324,7 +344,14 @@ private function connectToServer(): void (float) $this->options['timeout'] ); } else { - $connection_successful = $this->redis->connect($this->options['host'], (int) $this->options['port'], (float) $this->options['timeout']); + try { + $connection_successful = $this->redis->connect($this->options['host'], (int) $this->options['port'], (float) $this->options['timeout']); + } catch(\RedisException $ex){ + throw new StorageException( + sprintf("Can't connect to Redis server. %s", $ex->getMessage()), + $ex->getCode() + ); + } } if (!$connection_successful) { throw new StorageException( diff --git a/src/Prometheus/Storage/RedisSentinel.php b/src/Prometheus/Storage/RedisSentinel.php new file mode 100644 index 00000000..d761a902 --- /dev/null +++ b/src/Prometheus/Storage/RedisSentinel.php @@ -0,0 +1,161 @@ + false, // if enabled uses sentinel to get the master before connecting to redis + 'host' => '127.0.0.1', // phpredis sentinel address of the redis + 'port' => 26379, // phpredis sentinel port of the primary redis server, default 26379 if empty. + 'service' => 'myprimary', //, phpredis sentinel primary name, default myprimary + 'timeout' => 0, // phpredis sentinel connection timeout + 'persistent' => null, // phpredis sentinel persistence parameter + 'retry_interval' => 0, // phpredis sentinel retry interval + 'read_timeout' => 0, // phpredis sentinel read timeout + 'reconnect' => 0, // retries after losing connection to redis asking for a new primary, if -1 will retry indefinetely + 'username' => '', // phpredis sentinel auth username + 'password' => '', // phpredis sentinel auth password + 'ssl' => null, + ]; + + /** + * @param \RedisSentinel $redisSentinel + * @return self + * @throws StorageException + */ + public static function fromExistingConnection(\RedisSentinel $redisSentinel) : self { + $sentinel = new self(); + $sentinel->sentinel = $redisSentinel; + $sentinel->getMaster(); + return $sentinel; + } + + /** + * Redis constructor. + * @param mixed[] $options + */ + public function __construct(array $options = []) + { + $this->options = [...self::$defaultOptions, ...$options]; + $this->sentinel = $this->connectToSentinel($this->options); + } + + /** + * {@inheritdoc} + * @param mixed[] $config + * @return mixed[]|bool + * @throws StorageException|\RedisException + */ + public function getMaster(): array|bool + { + $service = $this->options['service']; + + try { + if(!$this->sentinel) { + $this->sentinel = $this->connectToSentinel($this->options); + } + + $master = $this->sentinel->master($service); + } catch (\RedisException $e){ + throw new StorageException( + sprintf("Can't connect to RedisSentinel server. %s", $e->getMessage()), + $e->getCode(), + $e + ); + } + + if (! $this->isValidMaster($master)) { + throw new StorageException(sprintf("No master found for service '%s'.", $service)); + } + + return $master; + } + + /** + * Check whether master is valid or not. + * @param mixed[]|bool $master + * @return bool + */ + protected function isValidMaster(array|bool $master): bool + { + return is_array($master) && isset($master['ip']) && isset($master['port']); + } + + /** + * Connect to the configured Redis Sentinel instance. + * @return \RedisSentinel + * @throws StorageException + */ + private function connectToSentinel(): \RedisSentinel + { + $host = $this->options['host'] ?? ''; + $port = $this->options['port'] ?? 26379; + $timeout = $this->options['timeout'] ?? 0.2; + $persistent = $this->options['persistent'] ?? null; + $retryInterval = $this->options['retry_interval'] ?? 0; + $readTimeout = $this->options['read_timeout'] ?? 0; + $username = $this->options['username'] ?? ''; + $password = $this->options['password'] ?? ''; + $ssl = $this->options['ssl'] ?? null; + + if (strlen(trim($host)) === 0) { + throw new StorageException('No host has been specified for the Redis Sentinel connection.'); + } + + $auth = null; + if (strlen(trim($username)) !== 0 && strlen(trim($password)) !== 0) { + $auth = [$username, $password]; + } elseif (strlen(trim($password)) !== 0) { + $auth = $password; + } + + if (version_compare((string)phpversion('redis'), '6.0', '>=')) { + $options = [ + 'host' => $host, + 'port' => $port, + 'connectTimeout' => $timeout, + 'persistent' => $persistent, + 'retryInterval' => $retryInterval, + 'readTimeout' => $readTimeout, + ]; + + if ($auth !== null) { + $options['auth'] = $auth; + } + + if (version_compare((string)phpversion('redis'), '6.1', '>=') && $ssl !== null) { + $options['ssl'] = $ssl; + } + // @phpstan-ignore arguments.count, argument.type + return new \RedisSentinel($options); + } + + if ($auth !== null) { + /** + * @phpstan-ignore arguments.count + **/ + return new \RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout, $auth); + } + return new \RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout); + } + + public function getSentinel() : \RedisSentinel { + return $this->sentinel; + } +} diff --git a/src/Prometheus/Storage/RedisSentinelConnector.php b/src/Prometheus/Storage/RedisSentinelConnector.php deleted file mode 100644 index 60882a64..00000000 --- a/src/Prometheus/Storage/RedisSentinelConnector.php +++ /dev/null @@ -1,98 +0,0 @@ -connectToSentinel($config); - - $master = $sentinel->master($service); - - if (! $this->isValidMaster($master)) { - throw new StorageException(sprintf("No master found for service '%s'.", $service)); - } - - return $master; - } - - /** - * Check whether master is valid or not. - * @param mixed[]|bool $master - * @return bool - */ - protected function isValidMaster(array|bool $master): bool - { - return is_array($master) && isset($master['ip']) && isset($master['port']); - } - - /** - * Connect to the configured Redis Sentinel instance. - * @param mixed[] $config - * @return RedisSentinel - * @throws StorageException - */ - private function connectToSentinel(array $config): RedisSentinel - { - $host = $config['host'] ?? ''; - $port = $config['port'] ?? 26379; - $timeout = $config['timeout'] ?? 0.2; - $persistent = $config['persistent'] ?? null; - $retryInterval = $config['retry_interval'] ?? 0; - $readTimeout = $config['read_timeout'] ?? 0; - $username = $config['username'] ?? ''; - $password = $config['password'] ?? ''; - $ssl = $config['ssl'] ?? null; - - if (strlen(trim($host)) === 0) { - throw new StorageException('No host has been specified for the Redis Sentinel connection.'); - } - - $auth = null; - if (strlen(trim($username)) !== 0 && strlen(trim($password)) !== 0) { - $auth = [$username, $password]; - } elseif (strlen(trim($password)) !== 0) { - $auth = $password; - } - - if (version_compare((string)phpversion('redis'), '6.0', '>=')) { - $options = [ - 'host' => $host, - 'port' => $port, - 'connectTimeout' => $timeout, - 'persistent' => $persistent, - 'retryInterval' => $retryInterval, - 'readTimeout' => $readTimeout, - ]; - - if ($auth !== null) { - $options['auth'] = $auth; - } - - if (version_compare((string)phpversion('redis'), '6.1', '>=') && $ssl !== null) { - $options['ssl'] = $ssl; - } - - return new RedisSentinel($options); - } - - if ($auth !== null) { - /** @noinspection PhpMethodParametersCountMismatchInspection */ - return new RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout, $auth); - } - - return new RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout, null); - } -} diff --git a/tests/Test/Prometheus/Storage/RedisSentinelTest.php b/tests/Test/Prometheus/Storage/RedisSentinelTest.php new file mode 100644 index 00000000..46288f34 --- /dev/null +++ b/tests/Test/Prometheus/Storage/RedisSentinelTest.php @@ -0,0 +1,94 @@ +redisConnection = new \Redis(); + $this->redisConnection->connect(REDIS_HOST); + $this->redisConnection->flushAll(); + } + + /** + * @test + */ + public function itShouldThrowAnExceptionOnConnectionFailureWithRedisSentinelNotEnabled(): void + { + $redis = new Redis(['host' => '/dev/null', 'sentinel' => ['host'=>'/dev/null']]); + + $this->expectException(StorageException::class); + $this->expectExceptionMessage("Can't connect to Redis server"); + + $redis->collect(); + $redis->wipeStorage(); + } + + /** + * @test + */ + public function itShouldThrowAnExceptionOnConnectionFailure(): void + { + $redis = new Redis(['host' => '/dev/null', 'sentinel' => ['host'=>'/dev/null', 'enable' => true]]); + + $this->expectException(StorageException::class); + $this->expectExceptionMessage("Can't connect to RedisSentinel server"); + + $redis->collect(); + $redis->wipeStorage(); + } + + /** + * @test + */ + public function itShouldThrowExceptionWhenInjectedRedisIsNotConnected(): void + { + $connection = new \Redis(); + $sentinel = new \RedisSentinel(); + + self::expectException(StorageException::class); + self::expectExceptionMessageMatches("/Can't connect to RedisSentinel server\\..*/"); + + Redis::fromExistingConnection($connection, $sentinel); + } + + /** + * @test + */ + public function itShouldThrowAnExceptionOnPrimaryFailure(): void + { + $redis = new Redis(['host' => '/dev/null', 'sentinel' => ['host'=>'/dev/null', 'enable' => true, 'service' => 'dummy']]); + + $this->expectException(StorageException::class); + $this->expectExceptionMessage("Can't connect to RedisSentinel server"); + + $redis->collect(); + $redis->wipeStorage(); + } + + /** + * @test + */ + public function itShouldGetMaster(): void + { + $redis = new Redis(['host' => '/dev/null', 'sentinel' => ['host'=>REDIS_SENTINEL_HOST, 'enable' => true, 'service' => 'myprimary']]); + + $redis->collect(); + $redis->wipeStorage(); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 12456d29..3523cbdb 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -17,3 +17,4 @@ $loader->add('Test\\Performance', __DIR__); define('REDIS_HOST', isset($_ENV['REDIS_HOST']) ? $_ENV['REDIS_HOST'] : '127.0.0.1'); +define('REDIS_SENTINEL_HOST', isset($_ENV['REDIS_SENTINEL_HOST']) ? $_ENV['REDIS_SENTINEL_HOST'] : '127.0.0.1'); From f98d3819d9080ca19520d57dea1533c63b92af56 Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:42:20 +0000 Subject: [PATCH 11/13] adding tests --- src/Prometheus/Storage/Redis.php | 14 ++++++-------- .../Test/Prometheus/Storage/RedisSentinelTest.php | 5 ++++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 603b8f39..c27e1693 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -104,17 +104,15 @@ public function __construct(array $options = []) /** * Sentinels discoverMaster - * @return mixed[] */ - public function getSentinelPrimary(): array + public function updateSentinelPrimary() { $master = $this->sentinel->getMaster(); - + if (is_array($master)) { - $options['host'] = $master['ip']; - $options['port'] = $master['port']; + $this->options['host'] = $master['ip']; + $this->options['port'] = $master['port']; } - return $options; } /** @@ -289,7 +287,7 @@ private function ensureOpenConnection(): void $retries = 0; while ($retries <= $reconnect) { try { - $this->options = $this->getSentinelPrimary(); + $this->updateSentinelPrimary(); $this->connectToServer(); break; } catch (\RedisException $e) { @@ -335,7 +333,7 @@ private function ensureOpenConnection(): void * @throws StorageException */ private function connectToServer(): void - { + { $connection_successful = false; if ($this->options['persistent_connections'] !== false) { $connection_successful = $this->redis->pconnect( diff --git a/tests/Test/Prometheus/Storage/RedisSentinelTest.php b/tests/Test/Prometheus/Storage/RedisSentinelTest.php index 46288f34..42a23686 100644 --- a/tests/Test/Prometheus/Storage/RedisSentinelTest.php +++ b/tests/Test/Prometheus/Storage/RedisSentinelTest.php @@ -86,9 +86,12 @@ public function itShouldThrowAnExceptionOnPrimaryFailure(): void */ public function itShouldGetMaster(): void { - $redis = new Redis(['host' => '/dev/null', 'sentinel' => ['host'=>REDIS_SENTINEL_HOST, 'enable' => true, 'service' => 'myprimary']]); + $redis = new Redis(['host' => '/dev/null', + 'sentinel' => ['host'=>REDIS_SENTINEL_HOST, 'enable' => true, 'service' => 'myprimary'] + ]); $redis->collect(); $redis->wipeStorage(); + $this->expectNotToPerformAssertions(); } } From e2a9ed92a90c031de57014419b066a7bffdb1600 Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Thu, 17 Apr 2025 17:12:53 +0000 Subject: [PATCH 12/13] minox fixes --- src/Prometheus/Storage/Redis.php | 24 ++++++++++--------- src/Prometheus/Storage/RedisSentinel.php | 23 ++++++++---------- .../Prometheus/Storage/RedisSentinelTest.php | 13 +++++----- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index c27e1693..05d1143f 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -80,7 +80,7 @@ class Redis implements Adapter /** * @var RedisSentinel */ - private $sentinel; + private $sentinel = null; /** * @var boolean @@ -96,7 +96,7 @@ public function __construct(array $options = []) $this->options = [...self::$defaultOptions, ...$options]; $this->options['sentinel'] = [...self::$defaultOptions['sentinel'] ?? [], ...$options['sentinel'] ?? []]; $this->redis = new \Redis(); - if(isset($this->options['sentinel']) && boolval($this->options['sentinel']['enable'])){ + if (boolval($this->options['sentinel']['enable'])) { $options['sentinel']['host'] = $options['sentinel']['host'] ?? $options['host']; $this->sentinel = new RedisSentinel($options['sentinel']); } @@ -104,9 +104,10 @@ public function __construct(array $options = []) /** * Sentinels discoverMaster + * @return void */ - public function updateSentinelPrimary() - { + public function updateSentinelPrimary(): void + { $master = $this->sentinel->getMaster(); if (is_array($master)) { @@ -118,8 +119,9 @@ public function updateSentinelPrimary() /** * @return \RedisSentinel */ - public function getRedisSentinel() : \RedisSentinel { - return $this->sentinel->getRedisSentinel(); + public function getRedisSentinel(): \RedisSentinel + { + return $this->sentinel->getSentinel(); } /** @@ -130,7 +132,7 @@ public function getRedisSentinel() : \RedisSentinel { */ public static function fromExistingConnection(\Redis $redis, \RedisSentinel $redisSentinel = null): self { - if($redisSentinel) { + if (isset($redisSentinel)) { RedisSentinel::fromExistingConnection($redisSentinel); } @@ -281,8 +283,8 @@ private function ensureOpenConnection(): void if ($this->connectionInitialized === true) { return; } - - if ($this->sentinel) { + + if ($this->sentinel !== null && boolval($this->options['sentinel']['enable'])) { $reconnect = $this->options['sentinel']['reconnect']; $retries = 0; while ($retries <= $reconnect) { @@ -333,7 +335,7 @@ private function ensureOpenConnection(): void * @throws StorageException */ private function connectToServer(): void - { + { $connection_successful = false; if ($this->options['persistent_connections'] !== false) { $connection_successful = $this->redis->pconnect( @@ -344,7 +346,7 @@ private function connectToServer(): void } else { try { $connection_successful = $this->redis->connect($this->options['host'], (int) $this->options['port'], (float) $this->options['timeout']); - } catch(\RedisException $ex){ + } catch (\RedisException $ex) { throw new StorageException( sprintf("Can't connect to Redis server. %s", $ex->getMessage()), $ex->getCode() diff --git a/src/Prometheus/Storage/RedisSentinel.php b/src/Prometheus/Storage/RedisSentinel.php index d761a902..9c70c065 100644 --- a/src/Prometheus/Storage/RedisSentinel.php +++ b/src/Prometheus/Storage/RedisSentinel.php @@ -33,18 +33,19 @@ class RedisSentinel 'password' => '', // phpredis sentinel auth password 'ssl' => null, ]; - + /** * @param \RedisSentinel $redisSentinel * @return self * @throws StorageException */ - public static function fromExistingConnection(\RedisSentinel $redisSentinel) : self { + public static function fromExistingConnection(\RedisSentinel $redisSentinel): self + { $sentinel = new self(); $sentinel->sentinel = $redisSentinel; $sentinel->getMaster(); return $sentinel; - } + } /** * Redis constructor. @@ -53,26 +54,21 @@ public static function fromExistingConnection(\RedisSentinel $redisSentinel) : s public function __construct(array $options = []) { $this->options = [...self::$defaultOptions, ...$options]; - $this->sentinel = $this->connectToSentinel($this->options); + $this->sentinel = $this->initSentinel(); } /** * {@inheritdoc} - * @param mixed[] $config * @return mixed[]|bool * @throws StorageException|\RedisException */ public function getMaster(): array|bool { $service = $this->options['service']; - - try { - if(!$this->sentinel) { - $this->sentinel = $this->connectToSentinel($this->options); - } + try { $master = $this->sentinel->master($service); - } catch (\RedisException $e){ + } catch (\RedisException $e) { throw new StorageException( sprintf("Can't connect to RedisSentinel server. %s", $e->getMessage()), $e->getCode(), @@ -102,7 +98,7 @@ protected function isValidMaster(array|bool $master): bool * @return \RedisSentinel * @throws StorageException */ - private function connectToSentinel(): \RedisSentinel + private function initSentinel(): \RedisSentinel { $host = $this->options['host'] ?? ''; $port = $this->options['port'] ?? 26379; @@ -155,7 +151,8 @@ private function connectToSentinel(): \RedisSentinel return new \RedisSentinel($host, $port, $timeout, $persistent, $retryInterval, $readTimeout); } - public function getSentinel() : \RedisSentinel { + public function getSentinel(): \RedisSentinel + { return $this->sentinel; } } diff --git a/tests/Test/Prometheus/Storage/RedisSentinelTest.php b/tests/Test/Prometheus/Storage/RedisSentinelTest.php index 42a23686..012e3f46 100644 --- a/tests/Test/Prometheus/Storage/RedisSentinelTest.php +++ b/tests/Test/Prometheus/Storage/RedisSentinelTest.php @@ -30,7 +30,7 @@ protected function setUp(): void */ public function itShouldThrowAnExceptionOnConnectionFailureWithRedisSentinelNotEnabled(): void { - $redis = new Redis(['host' => '/dev/null', 'sentinel' => ['host'=>'/dev/null']]); + $redis = new Redis(['host' => '/dev/null', 'sentinel' => ['host' => '/dev/null']]); $this->expectException(StorageException::class); $this->expectExceptionMessage("Can't connect to Redis server"); @@ -44,7 +44,7 @@ public function itShouldThrowAnExceptionOnConnectionFailureWithRedisSentinelNotE */ public function itShouldThrowAnExceptionOnConnectionFailure(): void { - $redis = new Redis(['host' => '/dev/null', 'sentinel' => ['host'=>'/dev/null', 'enable' => true]]); + $redis = new Redis(['host' => '/dev/null', 'sentinel' => ['host' => '/dev/null', 'enable' => true]]); $this->expectException(StorageException::class); $this->expectExceptionMessage("Can't connect to RedisSentinel server"); @@ -59,6 +59,7 @@ public function itShouldThrowAnExceptionOnConnectionFailure(): void public function itShouldThrowExceptionWhenInjectedRedisIsNotConnected(): void { $connection = new \Redis(); + // @phpstan-ignore arguments.count $sentinel = new \RedisSentinel(); self::expectException(StorageException::class); @@ -72,7 +73,7 @@ public function itShouldThrowExceptionWhenInjectedRedisIsNotConnected(): void */ public function itShouldThrowAnExceptionOnPrimaryFailure(): void { - $redis = new Redis(['host' => '/dev/null', 'sentinel' => ['host'=>'/dev/null', 'enable' => true, 'service' => 'dummy']]); + $redis = new Redis(['host' => '/dev/null', 'sentinel' => ['host' => '/dev/null', 'enable' => true, 'service' => 'dummy']]); $this->expectException(StorageException::class); $this->expectExceptionMessage("Can't connect to RedisSentinel server"); @@ -86,10 +87,10 @@ public function itShouldThrowAnExceptionOnPrimaryFailure(): void */ public function itShouldGetMaster(): void { - $redis = new Redis(['host' => '/dev/null', - 'sentinel' => ['host'=>REDIS_SENTINEL_HOST, 'enable' => true, 'service' => 'myprimary'] + $redis = new Redis(['host' => '/dev/null', + 'sentinel' => ['host' => REDIS_SENTINEL_HOST, 'enable' => true, 'service' => 'myprimary'] ]); - + $redis->collect(); $redis->wipeStorage(); $this->expectNotToPerformAssertions(); From 85777487e15c8dd1a0061ea591963cd672964294 Mon Sep 17 00:00:00 2001 From: Jacq <494120+Jacq@users.noreply.github.com> Date: Thu, 17 Apr 2025 17:26:58 +0000 Subject: [PATCH 13/13] fixing tests --- .github/workflows/tests.yml | 22 ++++++++++++++++++- .vscode/settings.json | 5 ++++- examples/flush_adapter.php | 3 ++- sentinel.conf | 1 + src/Prometheus/Storage/Redis.php | 2 +- src/Prometheus/Storage/RedisSentinel.php | 1 + .../Prometheus/Storage/RedisSentinelTest.php | 9 +++++--- 7 files changed, 36 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 78a3122f..3aa31e75 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,10 +30,16 @@ jobs: - 5432:5432 env: - # The hostname used to communicate with the Redis service container + # The hostname used to communicate with the Redis/Sentinel service containers REDIS_HOST: redis + # The hostname for redis sentinel + REDIS_SENTINEL_HOST: redis-sentinel # The default Redis port REDIS_PORT: 6379 + # The default Redis Sentinel port + REDIS_SENTINEL_PORT: 26379 + # The default Redis Sentinel primary + REDIS_SENTINEL_SERVICE: myprimary # MySQL DB_DATABASE: test DB_USER: root @@ -69,6 +75,20 @@ jobs: with: redis-version: ${{ matrix.redis-version }} + - name: Generate Redis Sentinel conf compatible with redis 5 assuming 127.0.0.1 (no resolve hostname) + run: | + REDIS_SENTINEL_IP=127.0.0.1 + cat < sentinel5.conf + port ${{ env.REDIS_SENTINEL_PORT }} + sentinel monitor ${{ env.REDIS_SENTINEL_SERVICE }} $REDIS_SENTINEL_IP ${{ env.REDIS_PORT }} 2 + sentinel down-after-milliseconds ${{ env.REDIS_SENTINEL_SERVICE }} 10000 + sentinel failover-timeout ${{ env.REDIS_SENTINEL_SERVICE }} 180000 + sentinel parallel-syncs ${{ env.REDIS_SENTINEL_SERVICE }} 2 + EOF + + - name: Start Redis Sentinel + run: docker run -d --name ${{env.REDIS_SENTINEL_HOST}} -p 26379:26379 --link redis:redis -v $PWD/sentinel5.conf:/data/sentinel.conf redis:${{ matrix.redis-version }} redis-server sentinel.conf --sentinel + - name: Execute tests (PDO with Sqlite) run: vendor/bin/phpunit diff --git a/.vscode/settings.json b/.vscode/settings.json index 9fce7cc6..a04f8bd1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "github-actions.workflows.pinned.workflows": [] + "github-actions.workflows.pinned.workflows": [], + "githubPullRequests.ignoredPullRequestBranches": [ + "main" + ] } \ No newline at end of file diff --git a/examples/flush_adapter.php b/examples/flush_adapter.php index 1c00eab7..f3defb4e 100644 --- a/examples/flush_adapter.php +++ b/examples/flush_adapter.php @@ -8,8 +8,9 @@ if ($adapterName === 'redis') { define('REDIS_HOST', $_SERVER['REDIS_HOST'] ?? '127.0.0.1'); + define('REDIS_SENTINEL_HOST', $_SERVER['REDIS_SENTINEL_HOST'] ?? '127.0.0.1'); - $adapter = new Prometheus\Storage\Redis(['host' => REDIS_HOST]); + $adapter = new Prometheus\Storage\Redis(['host' => REDIS_HOST, 'sentinel' => ['host' => REDIS_SENTINEL_HOST]]); } elseif ($adapterName === 'apc') { $adapter = new Prometheus\Storage\APC(); } elseif ($adapterName === 'apcng') { diff --git a/sentinel.conf b/sentinel.conf index 5f370dfd..0197f209 100644 --- a/sentinel.conf +++ b/sentinel.conf @@ -1,4 +1,5 @@ bind 0.0.0.0 +port 26379 sentinel monitor myprimary redis 6379 2 sentinel resolve-hostnames yes sentinel down-after-milliseconds myprimary 10000 diff --git a/src/Prometheus/Storage/Redis.php b/src/Prometheus/Storage/Redis.php index 05d1143f..96164cdf 100644 --- a/src/Prometheus/Storage/Redis.php +++ b/src/Prometheus/Storage/Redis.php @@ -130,7 +130,7 @@ public function getRedisSentinel(): \RedisSentinel * @return self * @throws StorageException */ - public static function fromExistingConnection(\Redis $redis, \RedisSentinel $redisSentinel = null): self + public static function fromExistingConnection(\Redis $redis, ?\RedisSentinel $redisSentinel = null): self { if (isset($redisSentinel)) { RedisSentinel::fromExistingConnection($redisSentinel); diff --git a/src/Prometheus/Storage/RedisSentinel.php b/src/Prometheus/Storage/RedisSentinel.php index 9c70c065..b1a957b4 100644 --- a/src/Prometheus/Storage/RedisSentinel.php +++ b/src/Prometheus/Storage/RedisSentinel.php @@ -4,6 +4,7 @@ use Prometheus\Exception\StorageException; +// Redis Sentinel connector based on https://github.com/Namoshek/laravel-redis-sentinel class RedisSentinel { /** diff --git a/tests/Test/Prometheus/Storage/RedisSentinelTest.php b/tests/Test/Prometheus/Storage/RedisSentinelTest.php index 012e3f46..97a1af71 100644 --- a/tests/Test/Prometheus/Storage/RedisSentinelTest.php +++ b/tests/Test/Prometheus/Storage/RedisSentinelTest.php @@ -60,7 +60,10 @@ public function itShouldThrowExceptionWhenInjectedRedisIsNotConnected(): void { $connection = new \Redis(); // @phpstan-ignore arguments.count - $sentinel = new \RedisSentinel(); + + $sentinel = version_compare((string)phpversion('redis'), '6.0', '>=') ? + new \RedisSentinel(['host' => '/dev/null']) : + new \RedisSentinel('/dev/null'); self::expectException(StorageException::class); self::expectExceptionMessageMatches("/Can't connect to RedisSentinel server\\..*/"); @@ -86,8 +89,8 @@ public function itShouldThrowAnExceptionOnPrimaryFailure(): void * @test */ public function itShouldGetMaster(): void - { - $redis = new Redis(['host' => '/dev/null', + { + $redis = new Redis(['host' => REDIS_HOST, 'sentinel' => ['host' => REDIS_SENTINEL_HOST, 'enable' => true, 'service' => 'myprimary'] ]);