From 6a28e0641e885f4c522bd293e91de9f83b91343a Mon Sep 17 00:00:00 2001 From: meirzamoodle Date: Wed, 27 Dec 2023 09:28:47 +0700 Subject: [PATCH] MDL-63128 cachestore_redis: Support Redis cluster as cache store Co-authored-by: Daniel Thee Roperto Co-authored-by: Avi Levy --- cache/stores/redis/addinstanceform.php | 9 +- .../stores/redis/lang/en/cachestore_redis.php | 42 ++-- cache/stores/redis/lib.php | 165 +++++++++------ cache/stores/redis/settings.php | 25 ++- .../tests/cachestore_cluster_redis_test.php | 193 ++++++++++++++++++ .../redis/tests/cachestore_redis_test.php | 154 ++++++++++++++ 6 files changed, 506 insertions(+), 82 deletions(-) create mode 100644 cache/stores/redis/tests/cachestore_cluster_redis_test.php create mode 100644 cache/stores/redis/tests/cachestore_redis_test.php diff --git a/cache/stores/redis/addinstanceform.php b/cache/stores/redis/addinstanceform.php index cdd9ed653a3..770100c3765 100644 --- a/cache/stores/redis/addinstanceform.php +++ b/cache/stores/redis/addinstanceform.php @@ -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'); diff --git a/cache/stores/redis/lang/en/cachestore_redis.php b/cache/stores/redis/lang/en/cachestore_redis.php index a12779e799e..7b170e3e732 100644 --- a/cache/stores/redis/lang/en/cachestore_redis.php +++ b/cache/stores/redis/lang/en/cachestore_redis.php @@ -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:
+ 172.23.0.11
+ 172.23.0.12
+ 172.23.0.13
+ Refer to the above examples to write a server. -See Accepting Client Connections and Redis PHP clients for more information. -'; -$string['password'] = 'Password'; -$string['password_help'] = 'This sets the password of the Redis server.'; +See Accepting Client Connections and Redis PHP clients 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:
+ 172.23.0.11
+ 172.23.0.12
+ 172.23.0.13
+ Refer to the above examples to write a server. See Accepting Client Connections and Redis PHP clients 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.'; diff --git a/cache/stores/redis/lib.php b/cache/stores/redis/lib.php index 16a8d25d8f4..07fb02b7b0a 100644 --- a/cache/stores/redis/lib.php +++ b/cache/stores/redis/lib.php @@ -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; diff --git a/cache/stores/redis/settings.php b/cache/stores/redis/settings.php index d7e7dfc7c3f..7e2136e9acf 100644 --- a/cache/stores/redis/settings.php +++ b/cache/stores/redis/settings.php @@ -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'), diff --git a/cache/stores/redis/tests/cachestore_cluster_redis_test.php b/cache/stores/redis/tests/cachestore_cluster_redis_test.php new file mode 100644 index 00000000000..604b122bea5 --- /dev/null +++ b/cache/stores/redis/tests/cachestore_cluster_redis_test.php @@ -0,0 +1,193 @@ +. + +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 + * @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()); + } +} diff --git a/cache/stores/redis/tests/cachestore_redis_test.php b/cache/stores/redis/tests/cachestore_redis_test.php new file mode 100644 index 00000000000..af3fac0a938 --- /dev/null +++ b/cache/stores/redis/tests/cachestore_redis_test.php @@ -0,0 +1,154 @@ +. + +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()); + } +}