diff --git a/cache/classes/allow_temporary_caches.php b/cache/classes/allow_temporary_caches.php new file mode 100644 index 00000000000..83379c19df8 --- /dev/null +++ b/cache/classes/allow_temporary_caches.php @@ -0,0 +1,80 @@ +. + +namespace core_cache; + +/** + * Create and keep an instance of this class to allow temporary caches when caches are disabled. + * + * This class works together with code in {@see cache_factory_disabled}. + * + * The intention is that temporary cache should be short-lived (not for the entire install process), + * which avoids two problems: first, that we might run out of memory for the caches, and second, + * that some code e.g. install.php/upgrade.php files, is entitled to assume that caching is not + * used and make direct database changes. + * + * @package core_cache + * @copyright 2022 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class allow_temporary_caches { + /** @var int Number of references of this class; if more than 0, temporary caches are allowed */ + protected static $references = 0; + + /** + * Constructs an instance of this class. + * + * Temporary caches will be allowed until this instance goes out of scope. Store this token + * in a local variable, so that the caches have a limited life; do not save it outside your + * function. + * + * If cache is not disabled then normal (non-temporary) caches will be used, and this class + * does nothing. + * + * If an object of this class already exists then creating (or destroying) another one will + * have no effect. + */ + public function __construct() { + self::$references++; + } + + /** + * Destroys an instance of this class. + * + * You do not need to call this manually; PHP will call it automatically when your variable + * goes out of scope. If you do need to remove your token at other times, use unset($token); + * + * If there are no other instances of this object, then all temporary caches will be discarded. + */ + public function __destruct() { + global $CFG; + require_once($CFG->dirroot . '/cache/disabledlib.php'); + + self::$references--; + if (self::$references === 0) { + \cache_factory_disabled::clear_temporary_caches(); + } + } + + /** + * Checks if temp caches are currently allowed. + * + * @return bool True if allowed + */ + public static function is_allowed(): bool { + return self::$references > 0; + } +} diff --git a/cache/disabledlib.php b/cache/disabledlib.php index c7cde7607db..380a4748043 100644 --- a/cache/disabledlib.php +++ b/cache/disabledlib.php @@ -229,6 +229,8 @@ class cache_disabled extends cache { * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cache_factory_disabled extends cache_factory { + /** @var array Array of temporary caches in use. */ + protected static $tempcaches = []; /** * Returns an instance of the cache_factor method. @@ -283,6 +285,22 @@ class cache_factory_disabled extends cache_factory { * @return cache_application|cache_session|cache_request */ public function create_cache_from_definition($component, $area, array $identifiers = array(), $unused = null) { + // Temporary in-memory caches are sometimes allowed when caching is disabled. + if (\core_cache\allow_temporary_caches::is_allowed() && !$identifiers) { + $key = $component . '/' . $area; + if (array_key_exists($key, self::$tempcaches)) { + $cache = self::$tempcaches[$key]; + } else { + $definition = $this->create_definition($component, $area); + // The cachestore_static class returns true to all three 'SUPPORTS_' checks so it + // can be used with all definitions. + $cache = new cachestore_static('TEMP:' . $component . '/' . $area); + $cache->initialise($definition); + self::$tempcaches[$key] = $cache; + } + return $cache; + } + // Regular cache definitions are cached inside create_definition(). This is not the case for disabledlib.php // definitions as they use load_adhoc(). They are built as a new object on each call. // We do not need to clone the definition because we know it's new. @@ -292,6 +310,15 @@ class cache_factory_disabled extends cache_factory { return $cache; } + /** + * Removes all temporary caches. + * + * Don't call this directly - used by {@see \core_cache\allow_temporary_caches}. + */ + public static function clear_temporary_caches(): void { + self::$tempcaches = []; + } + /** * Creates an ad-hoc cache from the given param. * diff --git a/cache/tests/allow_temporary_caches_test.php b/cache/tests/allow_temporary_caches_test.php new file mode 100644 index 00000000000..4fd950b9538 --- /dev/null +++ b/cache/tests/allow_temporary_caches_test.php @@ -0,0 +1,65 @@ +. + +namespace core_cache; + +/** + * Unit tests for {@see allow_temporary_caches}. + * + * @package core_cache + * @category test + * @copyright 2022 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \core_cache\allow_temporary_caches + */ +class allow_temporary_caches_test extends \advanced_testcase { + + /** + * Tests whether temporary caches are allowed. + */ + public function test_is_allowed(): void { + // Not allowed by default. + $this->assertFalse(allow_temporary_caches::is_allowed()); + + // Allowed if we have an instance. + $frog = new allow_temporary_caches(); + $this->assertTrue(allow_temporary_caches::is_allowed()); + + // Or two instances. + $toad = new allow_temporary_caches(); + $this->assertTrue(allow_temporary_caches::is_allowed()); + + // Get rid of the instances. + unset($frog); + $this->assertTrue(allow_temporary_caches::is_allowed()); + + // Not allowed when we get back to no instances. + unset($toad); + $this->assertFalse(allow_temporary_caches::is_allowed()); + + // Check it works to automatically free up the instance when variable goes out of scope. + $this->inner_is_allowed(); + $this->assertFalse(allow_temporary_caches::is_allowed()); + } + + /** + * Function call to demonstrate that you don't need to manually unset the variable. + */ + protected function inner_is_allowed(): void { + $gecko = new allow_temporary_caches(); + $this->assertTrue(allow_temporary_caches::is_allowed()); + } +} diff --git a/lib/accesslib.php b/lib/accesslib.php index b9251d2f87f..11c39b54f05 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -2268,6 +2268,9 @@ function reset_role_capabilities($roleid) { function update_capabilities($component = 'moodle') { global $DB, $OUTPUT; + // Allow temporary caches to be used during install, dramatically boosting performance. + $token = new \core_cache\allow_temporary_caches(); + $storedcaps = array(); $filecaps = load_capability_def($component); diff --git a/lib/adminlib.php b/lib/adminlib.php index 052a0d47f2f..d616ec85a58 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -8852,6 +8852,10 @@ function admin_get_root($reload=false, $requirefulltree=true) { function admin_apply_default_settings($node=null, $unconditional=true, $admindefaultsettings=array(), $settingsoutput=array()) { $counter = 0; + // This function relies heavily on config cache, so we need to enable in-memory caches if it + // is used during install when normal caching is disabled. + $token = new \core_cache\allow_temporary_caches(); + if (is_null($node)) { core_plugin_manager::reset_caches(); $node = admin_get_root(true, true);