diff --git a/config-dist.php b/config-dist.php index e35e19b7649..b8c5d51cb99 100644 --- a/config-dist.php +++ b/config-dist.php @@ -324,6 +324,9 @@ $CFG->admin = 'admin'; // Use the igbinary serializer instead of the php default one. Note that phpredis must be compiled with // igbinary support to make the setting to work. Also, if you change the serializer you have to flush the database! // $CFG->session_redis_serializer_use_igbinary = false; // Optional, default is PHP builtin serializer. +// $CFG->session_redis_compressor = 'none'; // Optional, possible values are: +// // 'gzip' - PHP GZip compression +// // 'zstd' - PHP Zstandard compression // // Please be aware that when selecting Memcached for sessions that it is advised to use a dedicated // memcache server. The memcached extension does not provide isolated environments for individual uses. diff --git a/lib/classes/session/redis.php b/lib/classes/session/redis.php index 61ee51e5aeb..1855238ecc8 100644 --- a/lib/classes/session/redis.php +++ b/lib/classes/session/redis.php @@ -40,6 +40,19 @@ defined('MOODLE_INTERNAL') || die(); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class redis extends handler { + /** + * Compressor: none. + */ + const COMPRESSION_NONE = 'none'; + /** + * Compressor: PHP GZip. + */ + const COMPRESSION_GZIP = 'gzip'; + /** + * Compressor: PHP Zstandard. + */ + const COMPRESSION_ZSTD = 'zstd'; + /** @var string $host save_path string */ protected $host = ''; /** @var int $port The port to connect to */ @@ -56,6 +69,8 @@ class redis extends handler { protected $lockretry = 100; /** @var int $serializer The serializer to use */ protected $serializer = \Redis::SERIALIZER_PHP; + /** @var int $compressor The compressor to use */ + protected $compressor = self::COMPRESSION_NONE; /** @var string $lasthash hash of the session data content */ protected $lasthash = null; @@ -122,6 +137,10 @@ class redis extends handler { if (isset($CFG->session_redis_lock_expire)) { $this->lockexpire = (int)$CFG->session_redis_lock_expire; } + + if (isset($CFG->session_redis_compressor)) { + $this->compressor = $CFG->session_redis_compressor; + } } /** @@ -268,7 +287,8 @@ class redis extends handler { if ($this->requires_write_lock()) { $this->lock_session($id); } - $sessiondata = $this->connection->get($id); + $sessiondata = $this->uncompress($this->connection->get($id)); + if ($sessiondata === false) { if ($this->requires_write_lock()) { $this->unlock_session($id); @@ -285,6 +305,53 @@ class redis extends handler { return $sessiondata; } + /** + * Compresses session data. + * + * @param mixed $value + * @return string + */ + private function compress($value) { + switch ($this->compressor) { + case self::COMPRESSION_NONE: + return $value; + case self::COMPRESSION_GZIP: + return gzencode($value); + case self::COMPRESSION_ZSTD: + return zstd_compress($value); + default: + debugging("Invalid compressor: {$this->compressor}"); + return $value; + } + } + + /** + * Uncompresses session data. + * + * @param string $value + * @return mixed + */ + private function uncompress($value) { + if ($value === false) { + return false; + } + + switch ($this->compressor) { + case self::COMPRESSION_NONE: + break; + case self::COMPRESSION_GZIP: + $value = gzdecode($value); + break; + case self::COMPRESSION_ZSTD: + $value = zstd_uncompress($value); + break; + default: + debugging("Invalid compressor: {$this->compressor}"); + } + + return $value; + } + /** * Write the serialized session data to our session store. * @@ -312,6 +379,8 @@ class redis extends handler { // There can be race conditions on new sessions racing each other but we can // address that in the future. try { + $data = $this->compress($data); + $this->connection->setex($id, $this->timeout, $data); } catch (RedisException $e) { error_log('Failed talking to redis: '.$e->getMessage()); diff --git a/lib/tests/session_redis_test.php b/lib/tests/session_redis_test.php index 028a6b6ee2b..dab879f696c 100644 --- a/lib/tests/session_redis_test.php +++ b/lib/tests/session_redis_test.php @@ -116,6 +116,30 @@ class core_session_redis_testcase extends advanced_testcase { $this->assertSessionNoLocks(); } + public function test_compression_read_and_write_works() { + global $CFG; + + $CFG->session_redis_compressor = \core\session\redis::COMPRESSION_GZIP; + + $sess = new \core\session\redis(); + $sess->init(); + $this->assertTrue($sess->handler_write('sess1', 'DATA')); + $this->assertSame('DATA', $sess->handler_read('sess1')); + $this->assertTrue($sess->handler_close()); + + if (extension_loaded('zstd')) { + $CFG->session_redis_compressor = \core\session\redis::COMPRESSION_ZSTD; + + $sess = new \core\session\redis(); + $sess->init(); + $this->assertTrue($sess->handler_write('sess2', 'DATA')); + $this->assertSame('DATA', $sess->handler_read('sess2')); + $this->assertTrue($sess->handler_close()); + } + + $CFG->session_redis_compressor = \core\session\redis::COMPRESSION_NONE; + } + public function test_session_blocks_with_existing_session() { $sess = new \core\session\redis(); $sess->init();