mirror of
https://github.com/moodle/moodle.git
synced 2025-04-17 14:35:29 +02:00
Merge branch 'MDL-63128-main' of https://github.com/meirzamoodle/moodle
This commit is contained in:
commit
d1a34c7d23
9
cache/classes/helper.php
vendored
9
cache/classes/helper.php
vendored
@ -873,4 +873,13 @@ class cache_helper {
|
||||
public static function result_found($value): bool {
|
||||
return $value !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the cluster mode is available in PHP.
|
||||
*
|
||||
* @return bool Return true if the PHP supports redis cluster, otherwise false.
|
||||
*/
|
||||
public static function is_cluster_available(): bool {
|
||||
return class_exists('RedisCluster');
|
||||
}
|
||||
}
|
||||
|
9
cache/stores/redis/addinstanceform.php
vendored
9
cache/stores/redis/addinstanceform.php
vendored
@ -24,7 +24,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot.'/cache/forms.php');
|
||||
require_once($CFG->dirroot . '/cache/forms.php');
|
||||
|
||||
/**
|
||||
* Form for adding instance of Redis Cache Store.
|
||||
@ -39,7 +39,12 @@ class cachestore_redis_addinstance_form extends cachestore_addinstance_form {
|
||||
protected function configuration_definition() {
|
||||
$form = $this->_form;
|
||||
|
||||
$form->addElement('text', 'server', get_string('server', 'cachestore_redis'), array('size' => 24));
|
||||
$form->addElement('advcheckbox', 'clustermode', get_string('clustermode', 'cachestore_redis'), '',
|
||||
cache_helper::is_cluster_available() ? '' : 'disabled');
|
||||
$form->addHelpButton('clustermode', 'clustermode', 'cachestore_redis');
|
||||
$form->setType('clustermode', PARAM_BOOL);
|
||||
|
||||
$form->addElement('textarea', 'server', get_string('server', 'cachestore_redis'), ['cols' => 6, 'rows' => 10]);
|
||||
$form->setType('server', PARAM_TEXT);
|
||||
$form->addHelpButton('server', 'server', 'cachestore_redis');
|
||||
$form->addRule('server', get_string('required'), 'required');
|
||||
|
42
cache/stores/redis/lang/en/cachestore_redis.php
vendored
42
cache/stores/redis/lang/en/cachestore_redis.php
vendored
@ -24,13 +24,18 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['ca_file'] = 'CA file path';
|
||||
$string['ca_file_help'] = 'Location of Certificate Authority file on local filesystem';
|
||||
$string['clustermode'] = 'Cluster Mode';
|
||||
$string['clustermode_help'] = 'Enabling it will run the Redis cluster function, allowing your server to serve multiple servers to handle concurrent requests simultaneously.';
|
||||
$string['clustermodeunavailable'] = 'Redis Cluster is currently unavailable. Please ensure that the PHP Redis extension supports Redis Cluster functionality.';
|
||||
$string['compressor_none'] = 'No compression.';
|
||||
$string['compressor_php_gzip'] = 'Use gzip compression.';
|
||||
$string['compressor_php_zstd'] = 'Use Zstandard compression.';
|
||||
$string['encrypt_connection'] = 'Use TLS encryption.';
|
||||
$string['encrypt_connection_help'] = 'Use TLS to connect to Redis. Do not use \'tls://\' in the hostname for Redis, use this option instead.';
|
||||
$string['ca_file'] = 'CA file path';
|
||||
$string['ca_file_help'] = 'Location of Certificate Authority file on local filesystem';
|
||||
$string['password'] = 'Password';
|
||||
$string['password_help'] = 'This sets the password of the Redis server.';
|
||||
$string['pluginname'] = 'Redis';
|
||||
$string['prefix'] = 'Key prefix';
|
||||
$string['prefix_help'] = 'This prefix is used for all key names on the Redis server.
|
||||
@ -41,8 +46,8 @@ $string['privacy:metadata:redis'] = 'The Redis cachestore plugin stores data bri
|
||||
$string['privacy:metadata:redis:data'] = 'The various data stored in the cache';
|
||||
$string['serializer_igbinary'] = 'The igbinary serializer.';
|
||||
$string['serializer_php'] = 'The default PHP serializer.';
|
||||
$string['server'] = 'Server';
|
||||
$string['server_help'] = 'This sets the hostname, IP address or Unix socket path of the Redis server to use.
|
||||
$string['server'] = 'Server(s)';
|
||||
$string['server_help'] = 'Redis server to use for testing.
|
||||
|
||||
Some example values:
|
||||
|
||||
@ -52,12 +57,20 @@ Some example values:
|
||||
* 1.2.3.4:1234 - To connect to a Redis server by IP address with a specific port.
|
||||
* unix:///var/redis.sock - To connect to a Redis server using a Unix socket.
|
||||
* /var/redis.sock - To connect to a Redis server using a Unix socket (alternative format).
|
||||
* If cluster mode is enabled, please specify servers separated by a new line:<br>
|
||||
172.23.0.11<br>
|
||||
172.23.0.12<br>
|
||||
172.23.0.13<br>
|
||||
Refer to the above examples to write a server.
|
||||
|
||||
See <a href="https://redis.io/docs/reference/clients/#accepting-client-connections" target="_new">Accepting Client Connections</a> and <a href="https://redis.io/resources/clients/#php" target="_new">Redis PHP clients</a> for more information.
|
||||
';
|
||||
$string['password'] = 'Password';
|
||||
$string['password_help'] = 'This sets the password of the Redis server.';
|
||||
See <a href="https://redis.io/docs/reference/clients/#accepting-client-connections" target="_new">Accepting Client Connections</a> and <a href="https://redis.io/resources/clients/#php" target="_new">Redis PHP clients</a> for more information.';
|
||||
$string['task_ttl'] = 'Free up memory used by expired entries in Redis caches';
|
||||
$string['test_clustermode'] = 'Cluster Mode';
|
||||
$string['test_clustermode_desc'] = 'Enable Test in Redis cluster mode.';
|
||||
$string['test_password'] = 'Test server password';
|
||||
$string['test_password_desc'] = 'Redis test server password.';
|
||||
$string['test_serializer'] = 'Serializer';
|
||||
$string['test_serializer_desc'] = 'Serializer to use for testing.';
|
||||
$string['test_server'] = 'Test server';
|
||||
$string['test_server_desc'] = 'Redis server to use for testing.
|
||||
|
||||
@ -69,17 +82,18 @@ Some example values:
|
||||
* 1.2.3.4:1234 - To connect to a Redis server by IP address with a specific port.
|
||||
* unix:///var/redis.sock - To connect to a Redis server using a Unix socket.
|
||||
* /var/redis.sock - To connect to a Redis server using a Unix socket (alternative format).
|
||||
* If cluster mode is enabled, please specify servers separated by a new line:<br>
|
||||
172.23.0.11<br>
|
||||
172.23.0.12<br>
|
||||
172.23.0.13<br>
|
||||
Refer to the above examples to write a server.
|
||||
|
||||
See <a href="https://redis.io/docs/reference/clients/#accepting-client-connections" target="_new">Accepting Client Connections</a> and <a href="https://redis.io/resources/clients/#php" target="_new">Redis PHP clients</a> for more information.';
|
||||
$string['test_password'] = 'Test server password';
|
||||
$string['test_password_desc'] = 'Redis test server password.';
|
||||
$string['test_serializer'] = 'Serializer';
|
||||
$string['test_serializer_desc'] = 'Serializer to use for testing.';
|
||||
$string['test_ttl'] = 'Testing TTL';
|
||||
$string['test_ttl_desc'] = 'Run the performance test using a cache that requires TTL (slower sets).';
|
||||
$string['usecompressor'] = 'Use compressor';
|
||||
$string['usecompressor_help'] = 'Specifies the compressor to use after serializing. It is done at Moodle Cache API level, not at php-redis level.';
|
||||
$string['useserializer'] = 'Use serializer';
|
||||
$string['useserializer_help'] = 'Specifies the serializer to use for serializing.
|
||||
The valid serializers are Redis::SERIALIZER_PHP or Redis::SERIALIZER_IGBINARY.
|
||||
The latter is supported only when phpredis is configured with --enable-redis-igbinary option and the igbinary extension is loaded.';
|
||||
$string['usecompressor'] = 'Use compressor';
|
||||
$string['usecompressor_help'] = 'Specifies the compressor to use after serializing. It is done at Moodle Cache API level, not at php-redis level.';
|
||||
|
165
cache/stores/redis/lib.php
vendored
165
cache/stores/redis/lib.php
vendored
@ -94,7 +94,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
/**
|
||||
* Connection to Redis for this store.
|
||||
*
|
||||
* @var Redis
|
||||
* @var Redis|RedisCluster
|
||||
*/
|
||||
protected $redis;
|
||||
|
||||
@ -177,7 +177,10 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
* @param string $name
|
||||
* @param array $configuration
|
||||
*/
|
||||
public function __construct($name, array $configuration = array()) {
|
||||
public function __construct(
|
||||
$name,
|
||||
array $configuration = [],
|
||||
) {
|
||||
$this->name = $name;
|
||||
|
||||
if (!array_key_exists('server', $configuration) || empty($configuration['server'])) {
|
||||
@ -199,75 +202,112 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Redis instance and
|
||||
* connect to the server.
|
||||
* Create a new Redis or RedisCluster instance and connect to the server.
|
||||
*
|
||||
* @param array $configuration The server configuration
|
||||
* @return Redis
|
||||
* @param array $configuration The redis instance configuration.
|
||||
* @return Redis|RedisCluster|null
|
||||
*/
|
||||
protected function new_redis(array $configuration): \Redis {
|
||||
global $CFG;
|
||||
|
||||
$redis = new Redis();
|
||||
|
||||
$server = $configuration['server'];
|
||||
protected function new_redis(array $configuration): Redis|RedisCluster|null {
|
||||
$encrypt = (bool) ($configuration['encryption'] ?? false);
|
||||
$clustermode = (bool) ($configuration['clustermode'] ?? false);
|
||||
$password = !empty($configuration['password']) ? $configuration['password'] : '';
|
||||
$prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
|
||||
// Check if it isn't a Unix socket to set default port.
|
||||
$port = null;
|
||||
$opts = [];
|
||||
// Unix sockets can start with / or with unix://.
|
||||
if ($server[0] === '/' || strpos($server, 'unix://') === 0) {
|
||||
$port = 0;
|
||||
} else {
|
||||
$port = 6379; // No Unix socket so set default port.
|
||||
if (strpos($server, ':')) { // Check for custom port.
|
||||
list($server, $port) = explode(':', $server);
|
||||
}
|
||||
|
||||
// We can encrypt if we aren't unix socket.
|
||||
if ($encrypt) {
|
||||
$server = 'tls://' . $server;
|
||||
if (empty($configuration['cafile'])) {
|
||||
$sslopts = [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
];
|
||||
// Set Redis server(s).
|
||||
$servers = explode("\n", $configuration['server']);
|
||||
$trimmedservers = [];
|
||||
// print_r($configuration);
|
||||
// print_r($servers);
|
||||
foreach ($servers as $server) {
|
||||
$server = strtolower(trim($server));
|
||||
if (!empty($server)) {
|
||||
if ($server[0] === '/' || str_starts_with($server, 'unix://')) {
|
||||
$port = 0;
|
||||
$trimmedservers[] = $server;
|
||||
} else {
|
||||
$sslopts = ['cafile' => $configuration['cafile']];
|
||||
$port = 6379; // No Unix socket so set default port.
|
||||
if (strpos($server, ':')) { // Check for custom port.
|
||||
list($server, $port) = explode(':', $server);
|
||||
}
|
||||
if (!$clustermode && $encrypt) {
|
||||
$server = 'tls://' . $server;
|
||||
}
|
||||
$trimmedservers[] = $server.':'.$port;
|
||||
}
|
||||
|
||||
// We only need the first record for the single redis.
|
||||
if (!$clustermode) {
|
||||
break;
|
||||
}
|
||||
$opts['stream'] = $sslopts;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ($redis->connect($server, $port, 1, null, 100, 1, $opts)) {
|
||||
// TLS/SSL Configuration.
|
||||
$exceptionclass = $clustermode ? 'RedisClusterException' : 'RedisException';
|
||||
$opts = [];
|
||||
if ($encrypt) {
|
||||
$opts = empty($configuration['cafile']) ?
|
||||
['verify_peer' => false, 'verify_peer_name' => false] :
|
||||
['cafile' => $configuration['cafile']];
|
||||
|
||||
// For a single (non-cluster) Redis, the TLS/SSL config must be added to the 'stream' key.
|
||||
if (!$clustermode) {
|
||||
$opts['stream'] = $opts;
|
||||
}
|
||||
}
|
||||
// Connect to redis.
|
||||
$redis = null;
|
||||
// print_r($trimmedservers);
|
||||
// exit;
|
||||
try {
|
||||
// Create a $redis object of a RedisCluster or Redis class.
|
||||
if ($clustermode) {
|
||||
$redis = new RedisCluster(
|
||||
name: null,
|
||||
seeds: $trimmedservers,
|
||||
timeout: 1,
|
||||
read_timeout: 1,
|
||||
persistent: true,
|
||||
auth: $password,
|
||||
context: !empty($opts) ? $opts : null,
|
||||
);
|
||||
} else {
|
||||
// We only need the first record for the single redis.
|
||||
list($server, $port) = explode(':', $trimmedservers[0]);
|
||||
$redis = new Redis();
|
||||
$redis->connect(
|
||||
host: $server,
|
||||
port: $port,
|
||||
timeout: 1,
|
||||
retry_interval: 100,
|
||||
read_timeout: 1,
|
||||
context: $opts,
|
||||
);
|
||||
if (!empty($password)) {
|
||||
$redis->auth($password);
|
||||
}
|
||||
// If using compressor, serialisation will be done at cachestore level, not php-redis.
|
||||
if ($this->compressor == self::COMPRESSOR_NONE) {
|
||||
$redis->setOption(Redis::OPT_SERIALIZER, $this->serializer);
|
||||
}
|
||||
if (!empty($prefix)) {
|
||||
$redis->setOption(Redis::OPT_PREFIX, $prefix);
|
||||
}
|
||||
if ($encrypt && !$redis->ping()) {
|
||||
/*
|
||||
* In case of a TLS connection, if phpredis client does not
|
||||
* communicate immediately with the server the connection hangs.
|
||||
* See https://github.com/phpredis/phpredis/issues/2332 .
|
||||
*/
|
||||
throw new \RedisException("Ping failed");
|
||||
}
|
||||
$this->isready = true;
|
||||
} else {
|
||||
$this->isready = false;
|
||||
}
|
||||
} catch (\RedisException $e) {
|
||||
debugging("redis $server: $e", DEBUG_NORMAL);
|
||||
|
||||
// In case of a TLS connection,
|
||||
// if phpredis client does not communicate immediately with the server the connection hangs.
|
||||
// See https://github.com/phpredis/phpredis/issues/2332.
|
||||
if ($encrypt && !$redis->ping('Ping')) {
|
||||
throw new $exceptionclass("Ping failed");
|
||||
}
|
||||
|
||||
// If using compressor, serialisation will be done at cachestore level, not php-redis.
|
||||
if ($this->compressor === self::COMPRESSOR_NONE) {
|
||||
$redis->setOption(Redis::OPT_SERIALIZER, $this->serializer);
|
||||
}
|
||||
|
||||
// Set the prefix.
|
||||
$prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
|
||||
if (!empty($prefix)) {
|
||||
$redis->setOption(Redis::OPT_PREFIX, $prefix);
|
||||
}
|
||||
$this->isready = true;
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
$server = $clustermode ? implode(',', $trimmedservers) : $trimmedservers[0].':'.$port;
|
||||
debugging("Failed to connect to Redis at {$server}, the error returned was: {$e->getMessage()}");
|
||||
$this->isready = false;
|
||||
}
|
||||
|
||||
@ -277,10 +317,10 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
/**
|
||||
* See if we can ping Redis server
|
||||
*
|
||||
* @param Redis $redis
|
||||
* @param RedisCluster|Redis $redis
|
||||
* @return bool
|
||||
*/
|
||||
protected function ping(Redis $redis) {
|
||||
protected function ping(RedisCluster|Redis $redis): bool {
|
||||
try {
|
||||
if ($redis->ping() === false) {
|
||||
return false;
|
||||
@ -763,7 +803,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
public function store_total_size(): ?int {
|
||||
try {
|
||||
$details = $this->redis->info('MEMORY');
|
||||
} catch (\RedisException $e) {
|
||||
} catch (RedisException $e) {
|
||||
return null;
|
||||
}
|
||||
if (empty($details['used_memory'])) {
|
||||
@ -789,6 +829,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
'compressor' => $data->compressor,
|
||||
'encryption' => $data->encryption,
|
||||
'cafile' => $data->cafile,
|
||||
'clustermode' => $data->clustermode,
|
||||
);
|
||||
}
|
||||
|
||||
@ -816,6 +857,9 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
if (!empty($config['cafile'])) {
|
||||
$data['cafile'] = $config['cafile'];
|
||||
}
|
||||
if (!empty($config['clustermode'])) {
|
||||
$data['clustermode'] = $config['clustermode'];
|
||||
}
|
||||
$editform->set_data($data);
|
||||
}
|
||||
|
||||
@ -847,6 +891,9 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
if (!empty($config->test_cafile)) {
|
||||
$configuration['cafile'] = $config->test_cafile;
|
||||
}
|
||||
if (!empty($config->test_clustermode)) {
|
||||
$configuration['clustermode'] = $config->test_clustermode;
|
||||
}
|
||||
// Make it possible to test TTL performance by hacking a copy of the cache definition.
|
||||
if (!empty($config->test_ttl)) {
|
||||
$definition = clone $definition;
|
||||
|
25
cache/stores/redis/settings.php
vendored
25
cache/stores/redis/settings.php
vendored
@ -25,15 +25,26 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$settings->add(
|
||||
new admin_setting_configtext(
|
||||
'cachestore_redis/test_server',
|
||||
get_string('test_server', 'cachestore_redis'),
|
||||
get_string('test_server_desc', 'cachestore_redis'),
|
||||
'',
|
||||
PARAM_TEXT,
|
||||
16
|
||||
new admin_setting_configcheckbox(
|
||||
name: 'cachestore_redis/test_clustermode',
|
||||
visiblename: get_string('clustermode', 'cachestore_redis'),
|
||||
description: cache_helper::is_cluster_available() ?
|
||||
get_string('clustermode_help', 'cachestore_redis') :
|
||||
get_string('clustermodeunavailable', 'cachestore_redis'),
|
||||
defaultsetting: 0,
|
||||
)
|
||||
);
|
||||
|
||||
$settings->add(
|
||||
new admin_setting_configtextarea(
|
||||
name: 'cachestore_redis/test_server',
|
||||
visiblename: get_string('test_server', 'cachestore_redis'),
|
||||
description: get_string('test_server_desc', 'cachestore_redis'),
|
||||
defaultsetting: '',
|
||||
paramtype: PARAM_TEXT,
|
||||
)
|
||||
);
|
||||
|
||||
$settings->add(new admin_setting_configcheckbox(
|
||||
'cachestore_redis/test_encryption',
|
||||
get_string('encrypt_connection', 'cachestore_redis'),
|
||||
|
193
cache/stores/redis/tests/cachestore_cluster_redis_test.php
vendored
Normal file
193
cache/stores/redis/tests/cachestore_cluster_redis_test.php
vendored
Normal file
@ -0,0 +1,193 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace cachestore_redis;
|
||||
|
||||
use cache_definition;
|
||||
use cache_store;
|
||||
use cachestore_redis;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once(__DIR__ . '/../../../tests/fixtures/stores.php');
|
||||
require_once(__DIR__ . '/../lib.php');
|
||||
|
||||
/**
|
||||
* Redis cluster test.
|
||||
*
|
||||
* If you wish to use these unit tests all you need to do is add the following definition to
|
||||
* your config.php file:
|
||||
*
|
||||
* define('TEST_CACHESTORE_REDIS_SERVERSCLUSTER', 'localhost:7000,localhost:7001');
|
||||
* define('TEST_CACHESTORE_REDIS_ENCRYPTCLUSTER', true);
|
||||
* define('TEST_CACHESTORE_REDIS_AUTHCLUSTER', 'foobared');
|
||||
* define('TEST_CACHESTORE_REDIS_CASCLUSTER', '/cafile/dir/ca.crt');
|
||||
*
|
||||
* @package cachestore_redis
|
||||
* @author Daniel Thee Roperto <daniel.roperto@catalyst-au.net>
|
||||
* @copyright 2017 Catalyst IT Australia {@link http://www.catalyst-au.net}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*
|
||||
* @coversDefaultClass \cachestore_redis
|
||||
*/
|
||||
class cachestore_cluster_redis_test extends \advanced_testcase {
|
||||
/**
|
||||
* Create a cache store for testing the Redis cluster.
|
||||
*
|
||||
* @param string|null $seed The redis cluster servers.
|
||||
* @return cachestore_redis The created cache store instance.
|
||||
*/
|
||||
public function create_store(?string $seed = null): cachestore_redis {
|
||||
global $DB;
|
||||
|
||||
$definition = cache_definition::load_adhoc(
|
||||
mode: cache_store::MODE_APPLICATION,
|
||||
component: 'cachestore_redis',
|
||||
area: 'phpunit_test',
|
||||
);
|
||||
|
||||
$servers = $seed ?? str_replace(",", "\n", TEST_CACHESTORE_REDIS_SERVERSCLUSTER);
|
||||
|
||||
$config = [
|
||||
'server' => $servers,
|
||||
'prefix' => $DB->get_prefix(),
|
||||
'clustermode' => true,
|
||||
];
|
||||
|
||||
if (defined('TEST_CACHESTORE_REDIS_ENCRYPTCLUSTER') && TEST_CACHESTORE_REDIS_ENCRYPTCLUSTER === true) {
|
||||
$config['encryption'] = true;
|
||||
}
|
||||
if (defined('TEST_CACHESTORE_REDIS_AUTHCLUSTER') && TEST_CACHESTORE_REDIS_AUTHCLUSTER) {
|
||||
$config['password'] = TEST_CACHESTORE_REDIS_AUTHCLUSTER;
|
||||
}
|
||||
if (defined('TEST_CACHESTORE_REDIS_CASCLUSTER') && TEST_CACHESTORE_REDIS_CASCLUSTER) {
|
||||
$config['cafile'] = TEST_CACHESTORE_REDIS_CASCLUSTER;
|
||||
}
|
||||
|
||||
$store = new cachestore_redis('TestCluster', $config);
|
||||
$store->initialise($definition);
|
||||
$store->purge();
|
||||
|
||||
return $store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the test environment.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
if (!cachestore_redis::are_requirements_met()) {
|
||||
$this->markTestSkipped('Could not test cachestore_redis with cluster, missing requirements.');
|
||||
} else if (!\cache_helper::is_cluster_available()) {
|
||||
$this->markTestSkipped('Could not test cachestore_redis with cluster, class RedisCluster is not available.');
|
||||
} else if (!defined('TEST_CACHESTORE_REDIS_SERVERSCLUSTER')) {
|
||||
$this->markTestSkipped('Could not test cachestore_redis with cluster, missing configuration. ' .
|
||||
"Example: define('TEST_CACHESTORE_REDIS_SERVERSCLUSTER', " .
|
||||
"'localhost:7000,localhost:7001,localhost:7002');");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the cache store can be created successfully.
|
||||
*
|
||||
* @covers ::is_ready
|
||||
*/
|
||||
public function test_it_can_create(): void {
|
||||
$store = $this->create_store();
|
||||
$this->assertNotNull($store);
|
||||
$this->assertTrue($store->is_ready());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the cache store trims server names correctly.
|
||||
*
|
||||
* @covers ::new_redis
|
||||
*/
|
||||
public function test_it_trims_server_names(): void {
|
||||
// Add a time before and spaces after the first server. Also adds a blank line before second server.
|
||||
$servers = explode(',', TEST_CACHESTORE_REDIS_SERVERSCLUSTER);
|
||||
$servers[0] = "\t" . $servers[0] . " \n";
|
||||
$servers = implode("\n", $servers);
|
||||
|
||||
$store = $this->create_store($servers);
|
||||
|
||||
$this->assertTrue($store->is_ready());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the cache store can successfully set and get a value.
|
||||
*
|
||||
* @covers ::set
|
||||
* @covers ::get
|
||||
*/
|
||||
public function test_it_can_setget(): void {
|
||||
$store = $this->create_store();
|
||||
$store->set('the key', 'the value');
|
||||
$actual = $store->get('the key');
|
||||
|
||||
$this->assertSame('the value', $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the cache store can successfully set and get multiple values.
|
||||
*
|
||||
* @covers ::set_many
|
||||
* @covers ::get_many
|
||||
*/
|
||||
public function test_it_can_setget_many(): void {
|
||||
$store = $this->create_store();
|
||||
|
||||
// Create values.
|
||||
$values = [];
|
||||
$keys = [];
|
||||
$expected = [];
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$key = "getkey_{$i}";
|
||||
$value = "getvalue #{$i}";
|
||||
$keys[] = $key;
|
||||
$values[] = [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
];
|
||||
$expected[$key] = $value;
|
||||
}
|
||||
|
||||
$store->set_many($values);
|
||||
$actual = $store->get_many($keys);
|
||||
$this->assertSame($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the cache store is marked as not ready if it fails to connect.
|
||||
*
|
||||
* @covers ::is_ready
|
||||
*/
|
||||
public function test_it_is_marked_not_ready_if_failed_to_connect(): void {
|
||||
global $DB;
|
||||
|
||||
$config = [
|
||||
'server' => "abc:123",
|
||||
'prefix' => $DB->get_prefix(),
|
||||
'clustermode' => true,
|
||||
];
|
||||
$store = new cachestore_redis('TestCluster', $config);
|
||||
$debugging = $this->getDebuggingMessages();
|
||||
// Failed to connect should show a debugging message.
|
||||
$this->assertCount(1, \phpunit_util::get_debugging_messages() );
|
||||
$this->assertStringContainsString('Couldn\'t map cluster keyspace using any provided seed', $debugging[0]->message);
|
||||
$this->resetDebugging();
|
||||
$this->assertFalse($store->is_ready());
|
||||
}
|
||||
}
|
154
cache/stores/redis/tests/cachestore_redis_test.php
vendored
Normal file
154
cache/stores/redis/tests/cachestore_redis_test.php
vendored
Normal file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace cachestore_redis;
|
||||
|
||||
use cache_definition;
|
||||
use cache_store;
|
||||
use cachestore_redis;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once(__DIR__.'/../../../tests/fixtures/stores.php');
|
||||
require_once(__DIR__.'/../lib.php');
|
||||
|
||||
/**
|
||||
* Redis cache store test.
|
||||
*
|
||||
* If you wish to use these unit tests all you need to do is add the following definition to
|
||||
* your config.php file.
|
||||
*
|
||||
* define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');
|
||||
*
|
||||
* @package cachestore_redis
|
||||
* @copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*
|
||||
* @coversDefaultClass \cachestore_redis
|
||||
*/
|
||||
class cachestore_redis_test extends \cachestore_tests {
|
||||
/** @var cachestore_redis $store Redis Cache Store. */
|
||||
protected $store;
|
||||
|
||||
/**
|
||||
* Returns the class name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_class_name(): string {
|
||||
return 'cachestore_redis';
|
||||
}
|
||||
|
||||
public function setUp(): void {
|
||||
if (!cachestore_redis::are_requirements_met() || !defined('TEST_CACHESTORE_REDIS_TESTSERVERS')) {
|
||||
$this->markTestSkipped('Could not test cachestore_redis. Requirements are not met.');
|
||||
}
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
parent::tearDown();
|
||||
|
||||
if ($this->store instanceof cachestore_redis) {
|
||||
$this->store->purge();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the required cachestore for the tests to run against Redis.
|
||||
*
|
||||
* @return cachestore_redis
|
||||
*/
|
||||
protected function create_cachestore_redis(): cachestore_redis {
|
||||
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_redis', 'phpunit_test');
|
||||
$store = new cachestore_redis('Test', cachestore_redis::unit_test_configuration());
|
||||
$store->initialise($definition);
|
||||
$this->store = $store;
|
||||
$store->purge();
|
||||
return $store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test methods for various operations (set and has) in the cachestore_redis class.
|
||||
*
|
||||
* @covers ::set
|
||||
* @covers ::has
|
||||
*/
|
||||
public function test_has(): void {
|
||||
$store = $this->create_cachestore_redis();
|
||||
|
||||
$this->assertTrue($store->set('foo', 'bar'));
|
||||
$this->assertTrue($store->has('foo'));
|
||||
$this->assertFalse($store->has('bat'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test methods for the 'has_any' operation in the cachestore_redis class.
|
||||
*
|
||||
* @covers ::set
|
||||
* @covers ::has_any
|
||||
*/
|
||||
public function test_has_any(): void {
|
||||
$store = $this->create_cachestore_redis();
|
||||
|
||||
$this->assertTrue($store->set('foo', 'bar'));
|
||||
$this->assertTrue($store->has_any(['bat', 'foo']));
|
||||
$this->assertFalse($store->has_any(['bat', 'baz']));
|
||||
}
|
||||
|
||||
/**
|
||||
* PHPUnit test methods for the 'has_all' operation in the cachestore_redis class.
|
||||
*
|
||||
* @covers ::set
|
||||
* @covers ::has_all
|
||||
*/
|
||||
public function test_has_all(): void {
|
||||
$store = $this->create_cachestore_redis();
|
||||
|
||||
$this->assertTrue($store->set('foo', 'bar'));
|
||||
$this->assertTrue($store->set('bat', 'baz'));
|
||||
$this->assertTrue($store->has_all(['foo', 'bat']));
|
||||
$this->assertFalse($store->has_all(['foo', 'bat', 'this']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test methods for the 'lock' operations in the cachestore_redis class.
|
||||
*
|
||||
* @covers ::acquire_lock
|
||||
* @covers ::check_lock_state
|
||||
* @covers ::release_lock
|
||||
*/
|
||||
public function test_lock(): void {
|
||||
$store = $this->create_cachestore_redis();
|
||||
|
||||
$this->assertTrue($store->acquire_lock('lock', '123'));
|
||||
$this->assertTrue($store->check_lock_state('lock', '123'));
|
||||
$this->assertFalse($store->check_lock_state('lock', '321'));
|
||||
$this->assertNull($store->check_lock_state('notalock', '123'));
|
||||
$this->assertFalse($store->release_lock('lock', '321'));
|
||||
$this->assertTrue($store->release_lock('lock', '123'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method to check if the cachestore_redis instance is ready after connecting.
|
||||
*
|
||||
* @covers ::is_ready
|
||||
*/
|
||||
public function test_it_is_ready_after_connecting(): void {
|
||||
$store = $this->create_cachestore_redis();
|
||||
$this::assertTrue($store->is_ready());
|
||||
}
|
||||
}
|
@ -346,7 +346,9 @@ $CFG->admin = 'admin';
|
||||
//
|
||||
// Redis session handler (requires redis server and redis extension):
|
||||
// $CFG->session_handler_class = '\core\session\redis';
|
||||
// $CFG->session_redis_host = '127.0.0.1';
|
||||
// $CFG->session_redis_host = '127.0.0.1'; or... // If there is only one host, use the single Redis connection.
|
||||
// $CFG->session_redis_host = '127.0.0.1:7000,127.0.0.1:7001'; // If there are multiple hosts (separated by a comma),
|
||||
// // use the Redis cluster connection.
|
||||
// Use TLS to connect to Redis. An array of SSL context options. Usually:
|
||||
// $CFG->session_redis_encrypt = ['cafile' => '/path/to/ca.crt']; or...
|
||||
// $CFG->session_redis_encrypt = ['verify_peer' => false, 'verify_peer_name' => false];
|
||||
|
@ -25,6 +25,7 @@
|
||||
namespace core\session;
|
||||
|
||||
use RedisException;
|
||||
use RedisClusterException;
|
||||
use SessionHandlerInterface;
|
||||
|
||||
/**
|
||||
@ -52,8 +53,8 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
*/
|
||||
const COMPRESSION_ZSTD = 'zstd';
|
||||
|
||||
/** @var string $host save_path string */
|
||||
protected $host = '';
|
||||
/** @var array $host save_path string */
|
||||
protected array $host = [];
|
||||
/** @var int $port The port to connect to */
|
||||
protected $port = 6379;
|
||||
/** @var array $sslopts SSL options, if applicable */
|
||||
@ -83,7 +84,7 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
*/
|
||||
protected $lockexpire;
|
||||
|
||||
/** @var Redis Connection */
|
||||
/** @var Redis|RedisCluster Connection */
|
||||
protected $connection = null;
|
||||
|
||||
/** @var array $locks List of currently held locks by this page. */
|
||||
@ -92,6 +93,12 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
/** @var int $timeout How long sessions live before expiring. */
|
||||
protected $timeout;
|
||||
|
||||
/** @var bool $clustermode Redis in cluster mode. */
|
||||
protected bool $clustermode = false;
|
||||
|
||||
/** @var int Maximum number of retries for cache store operations. */
|
||||
const MAX_RETRIES = 5;
|
||||
|
||||
/**
|
||||
* Create new instance of handler.
|
||||
*/
|
||||
@ -99,7 +106,10 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
global $CFG;
|
||||
|
||||
if (isset($CFG->session_redis_host)) {
|
||||
$this->host = $CFG->session_redis_host;
|
||||
// If there is only one host, use the single Redis connection.
|
||||
// If there are multiple hosts (separated by a comma), use the Redis cluster connection.
|
||||
$this->host = array_filter(array_map('trim', explode(',', $CFG->session_redis_host)));
|
||||
$this->clustermode = count($this->host) > 1;
|
||||
}
|
||||
|
||||
if (isset($CFG->session_redis_port)) {
|
||||
@ -107,7 +117,9 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
}
|
||||
|
||||
if (isset($CFG->session_redis_encrypt) && $CFG->session_redis_encrypt) {
|
||||
$this->host = 'tls://' . $this->host;
|
||||
if (!$this->clustermode) {
|
||||
$this->host[0] = 'tls://' . $this->host[0];
|
||||
}
|
||||
$this->sslopts = $CFG->session_redis_encrypt;
|
||||
}
|
||||
|
||||
@ -197,86 +209,120 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
|
||||
// The session handler requires a version of Redis with the SETEX command (at least 2.0).
|
||||
$version = phpversion('Redis');
|
||||
if (!$version or version_compare($version, '2.0') <= 0) {
|
||||
if (!$version || version_compare($version, '2.0') <= 0) {
|
||||
throw new exception('sessionhandlerproblem', 'error', '', null, 'redis extension version must be at least 2.0');
|
||||
}
|
||||
|
||||
$this->connection = new \Redis();
|
||||
$encrypt = (bool) ($this->sslopts ?? false);
|
||||
// Set Redis server(s).
|
||||
$trimmedservers = [];
|
||||
foreach ($this->host as $host) {
|
||||
$server = strtolower(trim($host));
|
||||
if (!empty($server)) {
|
||||
if ($server[0] === '/' || str_starts_with($server, 'unix://')) {
|
||||
$port = 0;
|
||||
$trimmedservers[] = $server;
|
||||
} else {
|
||||
$port = 6379; // No Unix socket so set default port.
|
||||
if (strpos($server, ':')) { // Check for custom port.
|
||||
list($server, $port) = explode(':', $server);
|
||||
}
|
||||
$trimmedservers[] = $server.':'.$port;
|
||||
}
|
||||
|
||||
$result = session_set_save_handler($this);
|
||||
// We only need the first record for the single redis.
|
||||
if (!$this->clustermode) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$result) {
|
||||
throw new exception('redissessionhandlerproblem', 'error');
|
||||
// TLS/SSL Configuration.
|
||||
$opts = [];
|
||||
if ($encrypt) {
|
||||
if ($this->clustermode) {
|
||||
$opts = $this->sslopts;
|
||||
} else {
|
||||
// For a single (non-cluster) Redis, the TLS/SSL config must be added to the 'stream' key.
|
||||
$opts['stream'] = $this->sslopts;
|
||||
}
|
||||
}
|
||||
|
||||
// MDL-59866: Add retries for connections (up to 5 times) to make sure it goes through.
|
||||
$counter = 1;
|
||||
$maxnumberofretries = 5;
|
||||
$opts = [];
|
||||
if ($this->sslopts) {
|
||||
// Do not set $opts['stream'] = [], breaks connect().
|
||||
$opts['stream'] = $this->sslopts;
|
||||
}
|
||||
|
||||
while ($counter <= $maxnumberofretries) {
|
||||
|
||||
$exceptionclass = $this->clustermode ? 'RedisClusterException' : 'RedisException';
|
||||
while ($counter <= self::MAX_RETRIES) {
|
||||
$this->connection = null;
|
||||
// Make a connection to Redis server(s).
|
||||
try {
|
||||
|
||||
$delay = rand(100, 500);
|
||||
|
||||
// One second timeout was chosen as it is long for connection, but short enough for a user to be patient.
|
||||
if (!$this->connection->connect($this->host, $this->port, 1, null, $delay, 1, $opts)) {
|
||||
throw new RedisException('Unable to connect to host.');
|
||||
}
|
||||
|
||||
if ($this->auth !== '') {
|
||||
if (!$this->connection->auth($this->auth)) {
|
||||
throw new RedisException('Unable to authenticate.');
|
||||
// Create a $redis object of a RedisCluster or Redis class.
|
||||
if ($this->clustermode) {
|
||||
$this->connection = new \RedisCluster(
|
||||
name: null,
|
||||
seeds: $trimmedservers,
|
||||
timeout: 1,
|
||||
read_timeout: 1,
|
||||
persistent: true,
|
||||
auth: $this->auth,
|
||||
context: !empty($opts) ? $opts : null,
|
||||
);
|
||||
} else {
|
||||
$delay = rand(100, 500);
|
||||
list($server, $port) = explode(':', $trimmedservers[0]);
|
||||
$this->connection = new \Redis();
|
||||
$this->connection->connect(
|
||||
host: $server,
|
||||
port: $this->port ?? $port,
|
||||
timeout: 1,
|
||||
retry_interval: $delay,
|
||||
read_timeout: 1,
|
||||
context: $opts,
|
||||
);
|
||||
if ($this->auth !== '' && !$this->connection->auth($this->auth)) {
|
||||
throw new $exceptionclass('Unable to authenticate.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->connection->setOption(\Redis::OPT_SERIALIZER, $this->serializer)) {
|
||||
throw new RedisException('Unable to set Redis PHP Serializer option.');
|
||||
throw new $exceptionclass('Unable to set the Redis PHP Serializer option.');
|
||||
}
|
||||
|
||||
if ($this->prefix !== '') {
|
||||
// Use custom prefix on sessions.
|
||||
if (!$this->connection->setOption(\Redis::OPT_PREFIX, $this->prefix)) {
|
||||
throw new RedisException('Unable to set Redis Prefix option.');
|
||||
throw new $exceptionclass('Unable to set the Redis Prefix option.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->sslopts && !$this->connection->ping()) {
|
||||
/*
|
||||
* In case of a TLS connection, if phpredis client does not
|
||||
* communicate immediately with the server the connection hangs.
|
||||
* See https://github.com/phpredis/phpredis/issues/2332 .
|
||||
*/
|
||||
throw new \RedisException("Ping failed");
|
||||
if ($this->sslopts && !$this->connection->ping('Ping')) {
|
||||
// In case of a TLS connection,
|
||||
// if phpredis client does not communicate immediately with the server the connection hangs.
|
||||
// See https://github.com/phpredis/phpredis/issues/2332.
|
||||
throw new $exceptionclass("Ping failed");
|
||||
}
|
||||
|
||||
if ($this->database !== 0) {
|
||||
if (!$this->connection->select($this->database)) {
|
||||
throw new RedisException('Unable to select Redis database '.$this->database.'.');
|
||||
throw new $exceptionclass('Unable to select the Redis database ' . $this->database . '.');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (RedisException $e) {
|
||||
$logstring = "Failed to connect (try {$counter} out of {$maxnumberofretries}) to redis ";
|
||||
$logstring .= "at {$this->host}:{$this->port}, error returned was: {$e->getMessage()}";
|
||||
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
$redishost = $this->clustermode ? implode(',', $this->host) : $this->host[0].':'.$this->port ?? $port;
|
||||
$logstring = "Failed to connect (try {$counter} out of " . self::MAX_RETRIES . ") to Redis ";
|
||||
$logstring .= "at ". $redishost .", the error returned was: {$e->getMessage()}";
|
||||
debugging($logstring);
|
||||
}
|
||||
|
||||
$counter++;
|
||||
|
||||
// Introduce a random sleep between 100ms and 500ms.
|
||||
usleep(rand(100000, 500000));
|
||||
}
|
||||
|
||||
// We have exhausted our retries, time to give up.
|
||||
if (isset($logstring)) {
|
||||
throw new RedisException($logstring);
|
||||
// We have exhausted our retries; it's time to give up.
|
||||
throw new $exceptionclass($logstring);
|
||||
}
|
||||
|
||||
$result = session_set_save_handler($this);
|
||||
if (!$result) {
|
||||
throw new exception('redissessionhandlerproblem', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@ -305,7 +351,7 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
}
|
||||
unset($this->locks[$id]);
|
||||
}
|
||||
} catch (RedisException $e) {
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
error_log('Failed talking to redis: '.$e->getMessage());
|
||||
return false;
|
||||
}
|
||||
@ -336,7 +382,7 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
return '';
|
||||
}
|
||||
$this->connection->expire($id, $this->timeout);
|
||||
} catch (RedisException $e) {
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
error_log('Failed talking to redis: '.$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
@ -421,7 +467,7 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
$data = $this->compress($data);
|
||||
|
||||
$this->connection->setex($id, $this->timeout, $data);
|
||||
} catch (RedisException $e) {
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
error_log('Failed talking to redis: '.$e->getMessage());
|
||||
return false;
|
||||
}
|
||||
@ -439,7 +485,7 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
try {
|
||||
$this->connection->del($id);
|
||||
$this->unlock_session($id);
|
||||
} catch (RedisException $e) {
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
error_log('Failed talking to redis: '.$e->getMessage());
|
||||
return false;
|
||||
}
|
||||
@ -582,7 +628,7 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
|
||||
try {
|
||||
return !empty($this->connection->exists($sid));
|
||||
} catch (RedisException $e) {
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
107
lib/tests/session_redis_cluster_test.php
Normal file
107
lib/tests/session_redis_cluster_test.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core;
|
||||
|
||||
use core\session\redis as redis_session;
|
||||
use RedisClusterException;
|
||||
|
||||
/**
|
||||
* Unit tests for Redis cluster in the core/session/redis.php.
|
||||
*
|
||||
* NOTE: in order to execute this test you need to set up
|
||||
* Redis cluster server and add configuration a constant
|
||||
* to config.php or phpunit.xml configuration file:
|
||||
*
|
||||
* define('TEST_SESSION_REDIS_HOSTCLUSTER', '127.0.0.1:7000,127.0.0.1:7001,127.0.0.1:7002');
|
||||
* define('TEST_SESSION_REDIS_AUTHCLUSTER', 'foobared');
|
||||
*
|
||||
* define('TEST_SESSION_REDIS_ENCRYPTCLUSTER', ['verify_peer' => false, 'verify_peer_name' => false]);
|
||||
* OR
|
||||
* define('TEST_SESSION_REDIS_ENCRYPTCLUSTER', ['cafile' => '/cafile/dir/ca.crt']);
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2024 Meirza <meirza.arson@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \core\session\redis
|
||||
*/
|
||||
class session_redis_cluster_test extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Set up the test environment.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
global $CFG;
|
||||
|
||||
if (!\cache_helper::is_cluster_available()) {
|
||||
$this->markTestSkipped('Could not test core_session with cluster, class RedisCluster is not available.');
|
||||
} else if (!defined('TEST_SESSION_REDIS_HOSTCLUSTER')) {
|
||||
$this->markTestSkipped('Could not test session_redis_cluster_test with cluster, missing configuration. ' .
|
||||
"Example: define('TEST_SESSION_REDIS_HOSTCLUSTER', " .
|
||||
"'localhost:7000,localhost:7001,localhost:7002');");
|
||||
}
|
||||
$this->resetAfterTest();
|
||||
$CFG->session_redis_host = TEST_SESSION_REDIS_HOSTCLUSTER;
|
||||
if (defined('TEST_SESSION_REDIS_ENCRYPTCLUSTER') && TEST_SESSION_REDIS_ENCRYPTCLUSTER) {
|
||||
$CFG->session_redis_encrypt = TEST_SESSION_REDIS_ENCRYPTCLUSTER;
|
||||
}
|
||||
if (defined('TEST_SESSION_REDIS_AUTHCLUSTER') && TEST_SESSION_REDIS_AUTHCLUSTER) {
|
||||
$CFG->session_redis_auth = TEST_SESSION_REDIS_AUTHCLUSTER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests compression for session read and write operations.
|
||||
*
|
||||
* It covers the behavior of session read and write operations under different compression configurations.
|
||||
*
|
||||
* @covers ::read
|
||||
* @covers ::write
|
||||
*/
|
||||
public function test_read_and_write(): void {
|
||||
$rediscluster = new redis_session();
|
||||
$rediscluster->init();
|
||||
$this->assertTrue($rediscluster->write('sess1', 'DATA'));
|
||||
$this->assertSame('DATA', $rediscluster->read('sess1'));
|
||||
$this->assertTrue($rediscluster->close());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the behavior when connection attempts to Redis cluster are exceeded.
|
||||
*
|
||||
* It sets up the environment to simulate multiple failed connection attempts and
|
||||
* checks if the expected exception message is received.
|
||||
*
|
||||
* @covers ::init
|
||||
*/
|
||||
public function test_exception_when_connection_attempts_exceeded(): void {
|
||||
global $CFG;
|
||||
|
||||
$CFG->session_redis_host = '127.0.0.1:1111111,127.0.0.1:1111112,127.0.0.1:1111113';
|
||||
$actual = '';
|
||||
|
||||
$rediscluster = new redis_session();
|
||||
try {
|
||||
$rediscluster->init();
|
||||
} catch (RedisClusterException $e) {
|
||||
$actual = $e->getMessage();
|
||||
}
|
||||
|
||||
$expected = "Failed to connect (try 5 out of 5) to Redis at";
|
||||
$this->assertDebuggingCalledCount(5);
|
||||
$this->assertStringContainsString($expected, $actual);
|
||||
}
|
||||
}
|
@ -348,7 +348,7 @@ class session_redis_test extends \advanced_testcase {
|
||||
if ($this->encrypted) {
|
||||
$host = "tls://$host";
|
||||
}
|
||||
$expected = "Failed to connect (try 5 out of 5) to redis at $host:111111";
|
||||
$expected = "Failed to connect (try 5 out of 5) to Redis at $host:111111";
|
||||
$this->assertDebuggingCalledCount(5);
|
||||
$this->assertStringContainsString($expected, $actual);
|
||||
}
|
||||
@ -368,6 +368,6 @@ class session_redis_test extends \advanced_testcase {
|
||||
$sess = new \core\session\redis();
|
||||
|
||||
$prop = new \ReflectionProperty(\core\session\redis::class, 'host');
|
||||
$this->assertEquals('tls://' . TEST_SESSION_REDIS_HOST, $prop->getValue($sess));
|
||||
$this->assertEquals('tls://' . TEST_SESSION_REDIS_HOST, $prop->getValue($sess)[0]);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user