Skip to content

Commit

Permalink
Add openlms Redis Cluster cache store
Browse files Browse the repository at this point in the history
  • Loading branch information
rrusso committed Dec 7, 2023
1 parent 42a867a commit ccc7f8b
Show file tree
Hide file tree
Showing 19 changed files with 3,589 additions and 0 deletions.
84 changes: 84 additions & 0 deletions cache/stores/rediscluster/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# RedisCluster Cache Store for Moodle™

A Moodle cache store plugin for [RedisCluster](https://redis.io/topics/cluster-tutorial).

## Requirements

* A Redis Cluster (version 4.0 or better)
* [PhpRedis](https://github.com/phpredis/phpredis) extension (version 4.0 or better)
* php-igbinary if using igbinary as the serialization method (recommended)

## Features

### Cache store

This is the main use of this plugin - providing a way to use a Redis Cluster as a Moodle™ cache.

Configurable options:
* failover mode: how phpredis handles reads/writes, one of:
- none
- error (reads from a slave on error)
- distribute (distributes reads over masters/slaves)
* serializer: igbinary or php
* compression: compresses data stored in redis - (de)compression occuring within phpredis

### Session handler

Moodle™ can be configured to use the Redis Cluster (or a different one) as the session store by setting:

`$CFG->session_handler_class = '\cachestore_rediscluster\session';`

Configuration options can be set with:

```
$CFG->session_rediscluster = [
'server' => '192.168.1.100:6379',
'serversecondary' => '192.168.1.101:6379:,
'prefix' => "mdlsession_{$CFG->dbname}:",
'acquire_lock_timeout' => 60,
'lock_expire' => 600,
'max_waiters' => 10,
];
```

The only required setting in the above array is `server`.

The following options govern session locking:

* acquire_lock_timeout: How long to wait for a lock to be released before giving up
* lock_expire: How long before a lock is released automatically
* max_waiters: How many threads can a session have waiting for a lock

Max waiters lets you define how many how many php threads a single user is allowed to have waiting for a session lock. Requests that don't take a session lock are unaffected.

By default, max_waiters is set to 10. Set it to 0 to use default Moodle™ behaviour.

Scripts with the `NO_SESSION_LOCK` define set to `true` ignore the max waiter behaviour.

### auth_saml2 session handler

If you use the `auth_saml2` plugin, you can configure it to use Redis Cluster for session storage by setting:

`$CFG->auth_saml2_store = '\\cachestore_rediscluster\\auth_saml2_store';`

Configuration options can then be set with:
```
$CFG->auth_saml2_rediscluster = [
// The same options as are available in the standard session handler are supported here.
];
```

## License
Copyright (c) 2021 Open LMS (https://www.openlms.net)

This program 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.

This program 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
this program. If not, see <http://www.gnu.org/licenses/>.
124 changes: 124 additions & 0 deletions cache/stores/rediscluster/addinstanceform.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?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/>.

/**
* RedisCluster Cache Store - Add instance form
*
* @package cachestore_rediscluster
* @copyright 2013 Adam Durana
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

defined('MOODLE_INTERNAL') || die();

require_once($CFG->dirroot.'/cache/forms.php');

/**
* Form for adding instance of RedisCluster Cache Store.
*
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_rediscluster_addinstance_form extends cachestore_addinstance_form {
/**
* Builds the form for creating an instance.
*/
protected function configuration_definition() {
$form = $this->_form;

if (!class_exists('RedisCluster')) {
return;
}

$form->addElement('text', 'server', get_string('server', 'cachestore_rediscluster'));
$form->addHelpButton('server', 'server', 'cachestore_rediscluster');
$form->addRule('server', get_string('required'), 'required');
$form->setType('server', PARAM_TEXT);

$form->addElement('text', 'serversecondary', get_string('serversecondary', 'cachestore_rediscluster'));
$form->addHelpButton('serversecondary', 'serversecondary', 'cachestore_rediscluster');
$form->addRule('serversecondary', get_string('required'), 'required');
$form->setType('serversecondary', PARAM_TEXT);

$form->addElement('text', 'prefix', get_string('prefix', 'cachestore_rediscluster'), array('size' => 16));
$form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
$form->addHelpButton('prefix', 'prefix', 'cachestore_rediscluster');
$form->addRule('prefix', get_string('prefixinvalid', 'cachestore_rediscluster'), 'regex', '#^[a-zA-Z0-9\-_]+$#');

$opts = [
RedisCluster::FAILOVER_NONE => get_string('failovernone', 'cachestore_rediscluster'),
RedisCluster::FAILOVER_ERROR => get_string('failovererror', 'cachestore_rediscluster'),
RedisCluster::FAILOVER_DISTRIBUTE => get_string('failoverdistribute', 'cachestore_rediscluster'),
];
// Experimental setting, only add it if its available.
// https://github.com/phpredis/phpredis/pull/1896 .
if (defined('RedisCluster::FAILOVER_PREFERRED')) {
$opts[RedisCluster::FAILOVER_PREFERRED] = get_string('failoverpreferred', 'cachestore_rediscluster');
}
$form->addElement('select', 'failover', get_string('failover', 'cachestore_rediscluster'), $opts);
$form->addHelpButton('failover', 'failover', 'cachestore_rediscluster');
$form->setDefault('failover', RedisCluster::FAILOVER_NONE);

if (defined('RedisCluster::FAILOVER_PREFERRED')) {
$form->addElement('text', 'preferrednodes', get_string('preferrednodes', 'cachestore_rediscluster'));
$form->addHelpButton('preferrednodes', 'preferrednodes', 'cachestore_rediscluster');
$form->disabledIf('preferrednodes', 'failover', 'neq', RedisCluster::FAILOVER_PREFERRED);
$form->setType('preferrednodes', PARAM_TEXT);
$form->setDefault('preferrednodes', '');
}

$form->addElement('checkbox', 'persist', get_string('persist', 'cachestore_rediscluster'));
$form->addHelpButton('persist', 'persist', 'cachestore_rediscluster');
$form->setDefault('persist', 0);
$form->setType('persist', PARAM_BOOL);

$form->addElement('text', 'timeout', get_string('timeout', 'cachestore_rediscluster'));
$form->addHelpButton('timeout', 'timeout', 'cachestore_rediscluster');
$form->setType('timeout', PARAM_FLOAT);
$form->setDefault('timeout', '3.0');

$form->addElement('text', 'readtimeout', get_string('readtimeout', 'cachestore_rediscluster'));
$form->addHelpButton('readtimeout', 'readtimeout', 'cachestore_rediscluster');
$form->setType('readtimeout', PARAM_FLOAT);
$form->setDefault('readtimeout', '3.0');

$opts = [
Redis::SERIALIZER_PHP => get_string('serializerphp', 'cachestore_rediscluster'),
Redis::SERIALIZER_IGBINARY => get_string('serializerigbinary', 'cachestore_rediscluster'),
];
$form->addElement('select', 'serializer', get_string('serializer', 'cachestore_rediscluster'), $opts);
$form->addHelpButton('serializer', 'serializer', 'cachestore_rediscluster');
$form->setDefault('serializer', Redis::SERIALIZER_PHP);

$opts = [
Redis::COMPRESSION_NONE => get_string('compressionnone', 'cachestore_rediscluster'),
];

if (defined('Redis::COMPRESSION_LZ4')) {
$opts[Redis::COMPRESSION_LZ4] = get_string('compressionlz4', 'cachestore_rediscluster');
}

if (defined('Redis::COMPRESSION_LZF')) {
$opts[Redis::COMPRESSION_LZF] = get_string('compressionlzf', 'cachestore_rediscluster');
}
if (defined('Redis::COMPRESSION_ZSTD')) {
$opts[Redis::COMPRESSION_ZSTD] = get_string('compressionzstd', 'cachestore_rediscluster');
}
$form->addElement('select', 'compression', get_string('compression', 'cachestore_rediscluster'), $opts);
$form->addHelpButton('compression', 'compression', 'cachestore_rediscluster');
$form->setDefault('compression', Redis::COMPRESSION_NONE);

}
}
147 changes: 147 additions & 0 deletions cache/stores/rediscluster/classes/auth_saml2_store.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?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/>.

/**
* RedisCluster store simpleSAMLphp class for auth/saml2.
*
* @package cachestore_rediscluster
* @author Adam Olley
* @copyright Copyright (c) 2021 Open LMS (https://www.openlms.net)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace cachestore_rediscluster;

defined('MOODLE_INTERNAL') || die();

require_once($CFG->dirroot . '/auth/saml2/extlib/simplesamlphp/lib/SimpleSAML/Store.php');

/**
* RedisCluster store simpleSAMLphp class for auth/saml2.
*
* @package cachestore_rediscluster
* @copyright Copyright (c) 2021 Open LMS (https://www.openlms.net)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class auth_saml2_store extends \SimpleSAML\Store {

/**
* The connection config for RedisCluster.
*
* @var string
*/
protected $config;

/**
* The RedisCluster cachestore object.
*
* @var cachestore_rediscluster
*/
protected $connection = null;

/**
* Create new instance of handler.
*/
public function __construct() {
global $CFG;

$this->config = [
'compression' => \Redis::COMPRESSION_NONE,
'failover' => \RedisCluster::FAILOVER_NONE,
'persist' => false,
'preferrednodes' => null,
'prefix' => 'simpleSAMLphp.' . $CFG->dbname . '.',
'readtimeout' => 3.0,
'serializer' => \Redis::SERIALIZER_PHP,
'server' => null,
'serversecondary' => null,
'session' => false,
'timeout' => 3.0,
];

foreach (array_keys($this->config) as $key) {
if (!empty($CFG->auth_saml2_rediscluster[$key])) {
$this->config[$key] = $CFG->auth_saml2_rediscluster[$key];
}
}

if (!$this->init()) {
throw new \coding_exception("Could not configure rediscluster for auth_saml2");
}
}

/**
* Init connection.
*/
protected function init() {
global $CFG;

require_once("{$CFG->dirroot}/cache/stores/rediscluster/lib.php");

if (!extension_loaded('redis') || empty($this->config['server']) || !class_exists('RedisCluster')) {
return false;
}

$this->connection = new \cachestore_rediscluster(null, $this->config);
return true;
}

/**
* @param string $type
* @param string $key
* @param mixed $value
* @param int|null $expire
*/
public function set($type, $key, $value, $expire = null) {
$now = time();
if ($expire !== null && $expire > $now) {
$this->connection->command('setex', $this->make_key($type, $key), $expire - $now, $value);
} else {
$this->connection->command('set', $this->make_key($type, $key), $value);
}
}

/**
* @param string $type
* @param string $key
* @return mixed|null
*/
public function get($type, $key) {
$value = $this->connection->command('get', $this->make_key($type, $key));
if ($value === false) {
$value = null;
}

return $value;
}

/**
* @param string $type
* @param string $key
*/
public function delete($type, $key) {
$this->connection->command('unlink', $this->make_key($type, $key));
}

/**
* @param string $type
* @param string $key
* @return string
*/
protected function make_key($type, $key) {
return $type . '.' . $key;
}
}
Loading

0 comments on commit ccc7f8b

Please sign in to comment.