Merge branch 'MDL-72837-master' of https://github.com/sammarshallou/moodle

This commit is contained in:
Andrew Nicols
2022-02-25 10:59:54 +08:00
14 changed files with 1049 additions and 142 deletions
@@ -210,6 +210,8 @@ class availability_completion_condition_testcase extends advanced_testcase {
'completion' => COMPLETION_TRACKING_AUTOMATIC]);
$DB->set_field('course_modules', 'completiongradeitemnumber', 0,
['id' => $assignrow->cmid]);
// As we manually set the field here, we are going to need to reset the modinfo cache.
rebuild_course_cache($course->id, true);
$assign = new assign(context_module::instance($assignrow->cmid), false, false);
// Get basic details.
+78 -1
View File
@@ -48,6 +48,34 @@ interface cache_loader {
*/
public function get($key, $strictness = IGNORE_MISSING);
/**
* Retrieves the value and actual version for the given key, with at least the required version.
*
* If there is no value for the key, or there is a value but it doesn't have the required
* version, then this function will return false (or throw an exception if you set strictness
* to MUST_EXIST).
*
* This function can be used to make it easier to support localisable caches (where the cache
* could be stored on a local server as well as a shared cache). Specifying the version means
* that it will automatically retrieve the correct version if available, either from the local
* server or [if that has an older version] from the shared server.
*
* If the cached version is newer than specified version, it will be returned regardless. For
* example, if you request version 4, but the locally cached version is 5, it will be returned.
* If you request version 6, and the locally cached version is 5, then the system will look in
* higher-level caches (if any); if there still isn't a version 6 or greater, it will return
* null.
*
* You must use this function if you use set_versioned.
*
* @param string|int $key The key for the data being requested.
* @param int $requiredversion Minimum required version of the data
* @param int $strictness One of IGNORE_MISSING or MUST_EXIST.
* @param mixed $actualversion If specified, will be set to the actual version number retrieved
* @return mixed Data from the cache, or false if the key did not exist or was too old
*/
public function get_versioned($key, int $requiredversion, int $strictness = IGNORE_MISSING, &$actualversion = null);
/**
* Retrieves an array of values for an array of keys.
*
@@ -82,6 +110,29 @@ interface cache_loader {
*/
public function set($key, $data);
/**
* Sets the value for the given key with the given version.
*
* The cache does not store multiple versions - any existing version will be overwritten with
* this one. This function should only be used if there is a known 'current version' (e.g.
* stored in a database table). It only ensures that the cache does not return outdated data.
*
* This function can be used to help implement localisable caches (where the cache could be
* stored on a local server as well as a shared cache). The version will be recorded alongside
* the item and get_versioned will always return the correct version.
*
* The version number must be an integer that always increases. This could be based on the
* current time, or a stored value that increases by 1 each time it changes, etc.
*
* If you use this function you must use get_versioned to retrieve the data.
*
* @param string|int $key The key for the data being set.
* @param int $version Integer for the version of the data
* @param mixed $data The data to set against the key.
* @return bool True on success, false otherwise.
*/
public function set_versioned($key, int $version, $data): bool;
/**
* Sends several key => value pairs to the cache.
*
@@ -436,6 +487,32 @@ interface cache_data_source {
public function load_many_for_cache(array $keys);
}
/**
* Versionable cache data source.
*
* This interface extends the main cache data source interface to add an extra required method if
* the data source is to be used for a versioned cache.
*
* @package core_cache
*/
interface cache_data_source_versionable extends cache_data_source {
/**
* Loads the data for the key provided ready formatted for caching.
*
* If there is no data for that key, or if the data for the required key has an older version
* than the specified $requiredversion, then this returns null.
*
* If there is data then $actualversion should be set to the actual version number retrieved
* (may be the same as $requiredversion or newer).
*
* @param string|int $key The key to load.
* @param int $requiredversion Minimum required version
* @param mixed $actualversion Should be set to the actual version number retrieved
* @return mixed What ever data should be returned, or false if it can't be loaded.
*/
public function load_for_cache_versioned($key, int $requiredversion, &$actualversion);
}
/**
* Cacheable object.
*
@@ -521,4 +598,4 @@ interface cache_lock_interface {
* Things such as unfortunate timeouts etc could cause this situation.
*/
public function __destruct();
}
}
+229 -71
View File
@@ -42,6 +42,11 @@ defined('MOODLE_INTERNAL') || die();
*/
class cache implements cache_loader {
/**
* @var int Constant for cache entries that do not have a version number
*/
const VERSION_NONE = -1;
/**
* We need a timestamp to use within the cache API.
* This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with
@@ -397,13 +402,101 @@ class cache implements cache_loader {
* @throws coding_exception
*/
public function get($key, $strictness = IGNORE_MISSING) {
return $this->get_implementation($key, self::VERSION_NONE, $strictness);
}
/**
* Retrieves the value and actual version for the given key, with at least the required version.
*
* If there is no value for the key, or there is a value but it doesn't have the required
* version, then this function will return null (or throw an exception if you set strictness
* to MUST_EXIST).
*
* This function can be used to make it easier to support localisable caches (where the cache
* could be stored on a local server as well as a shared cache). Specifying the version means
* that it will automatically retrieve the correct version if available, either from the local
* server or [if that has an older version] from the shared server.
*
* If the cached version is newer than specified version, it will be returned regardless. For
* example, if you request version 4, but the locally cached version is 5, it will be returned.
* If you request version 6, and the locally cached version is 5, then the system will look in
* higher-level caches (if any); if there still isn't a version 6 or greater, it will return
* null.
*
* You must use this function if you use set_versioned.
*
* @param string|int $key The key for the data being requested.
* @param int $requiredversion Minimum required version of the data
* @param int $strictness One of IGNORE_MISSING or MUST_EXIST.
* @param mixed $actualversion If specified, will be set to the actual version number retrieved
* @return mixed Data from the cache, or false if the key did not exist or was too old
* @throws \coding_exception If you call get_versioned on a non-versioned cache key
*/
public function get_versioned($key, int $requiredversion, int $strictness = IGNORE_MISSING, &$actualversion = null) {
return $this->get_implementation($key, $requiredversion, $strictness, $actualversion);
}
/**
* Checks returned data to see if it matches the specified version number.
*
* For versioned data, this returns the version_wrapper object (or false). For other
* data, it returns the actual data (or false).
*
* @param mixed $result Result data
* @param int $requiredversion Required version number or VERSION_NONE if there must be no version
* @return bool True if version is current, false if not (or if result is false)
* @throws \coding_exception If unexpected type of data (versioned vs non-versioned) is found
*/
protected static function check_version($result, int $requiredversion): bool {
if ($requiredversion === self::VERSION_NONE) {
if ($result instanceof \core_cache\version_wrapper) {
throw new \coding_exception('Unexpectedly found versioned cache entry');
} else {
// No version checks, so version is always correct.
return true;
}
} else {
// If there's no result, obviously it doesn't meet the required version.
if (!$result) {
return false;
}
if (!($result instanceof \core_cache\version_wrapper)) {
throw new \coding_exception('Unexpectedly found non-versioned cache entry');
}
// If the result doesn't match the required version tag, return false.
if ($result->version < $requiredversion) {
return false;
}
// The version meets the requirement.
return true;
}
}
/**
* Retrieves the value for the given key from the cache.
*
* @param string|int $key The key for the data being requested.
* It can be any structure although using a scalar string or int is recommended in the interests of performance.
* In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
* @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE
* @param int $strictness One of IGNORE_MISSING | MUST_EXIST
* @param mixed $actualversion If specified, will be set to the actual version number retrieved
* @return mixed|false The data from the cache or false if the key did not exist within the cache.
* @throws coding_exception
*/
protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) {
// 1. Get it from the static acceleration array if we can (only when it is enabled and it has already been requested/set).
$usesstaticacceleration = $this->use_static_acceleration();
if ($usesstaticacceleration) {
$result = $this->static_acceleration_get($key);
if ($result !== false) {
return $result;
if ($result && self::check_version($result, $requiredversion)) {
if ($requiredversion === self::VERSION_NONE) {
return $result;
} else {
$actualversion = $result->version;
return $result->data;
}
}
}
@@ -412,18 +505,58 @@ class cache implements cache_loader {
// 3. Get it from the store. Obviously wasn't in the static acceleration array.
$result = $this->store->get($parsedkey);
if ($result) {
// Check the result has at least the required version.
try {
$validversion = self::check_version($result, $requiredversion);
} catch (\coding_exception $e) {
// If we get an exception because there is incorrect data in the cache (not
// versioned when it ought to be), delete it so this exception goes away next time.
// The exception should only happen if there is a code bug (which is why we still
// throw it) but there are unusual scenarios where it might happen and that would
// be annoying if it doesn't fix itself.
$this->store->delete($parsedkey);
throw $e;
}
if (!$validversion) {
// If the result was too old, don't use it.
$result = false;
// Also delete it immediately. This improves performance in the
// case when the cache item is large and there may be multiple clients simultaneously
// requesting it - they won't all have to do a megabyte of IO just in order to find
// that it's out of date.
$this->store->delete($parsedkey);
}
}
if ($result !== false) {
if ($result instanceof cache_ttl_wrapper) {
if ($result->has_expired()) {
// Look to see if there's a TTL wrapper. It might be inside a version wrapper.
if ($requiredversion !== self::VERSION_NONE) {
$ttlconsider = $result->data;
} else {
$ttlconsider = $result;
}
if ($ttlconsider instanceof cache_ttl_wrapper) {
if ($ttlconsider->has_expired()) {
$this->store->delete($parsedkey);
$result = false;
} else if ($requiredversion === self::VERSION_NONE) {
// Use the data inside the TTL wrapper as the result.
$result = $ttlconsider->data;
} else {
$result = $result->data;
// Put the data from the TTL wrapper directly inside the version wrapper.
$result->data = $ttlconsider->data;
}
}
if ($usesstaticacceleration) {
$this->static_acceleration_set($key, $result);
}
// Remove version wrapper if necessary.
if ($requiredversion !== self::VERSION_NONE) {
$actualversion = $result->version;
$result = $result->data;
}
if ($result instanceof cache_cached_object) {
$result = $result->restore_object();
}
@@ -439,9 +572,23 @@ class cache implements cache_loader {
// We must pass the original (unparsed) key to the next loader in the chain.
// The next loader will parse the key as it sees fit. It may be parsed differently
// depending upon the capabilities of the store associated with the loader.
$result = $this->loader->get($key);
if ($requiredversion === self::VERSION_NONE) {
$result = $this->loader->get($key);
} else {
$result = $this->loader->get_versioned($key, $requiredversion, IGNORE_MISSING, $actualversion);
}
} else if ($this->datasource !== false) {
$result = $this->datasource->load_for_cache($key);
if ($requiredversion === self::VERSION_NONE) {
$result = $this->datasource->load_for_cache($key);
} else {
if (!$this->datasource instanceof cache_data_source_versionable) {
throw new \coding_exception('Data source is not versionable');
}
$result = $this->datasource->load_for_cache_versioned($key, $requiredversion, $actualversion);
if ($result && $actualversion < $requiredversion) {
throw new \coding_exception('Data source returned outdated version');
}
}
}
$setaftervalidation = ($result !== false);
} else if ($this->perfdebug) {
@@ -452,9 +599,14 @@ class cache implements cache_loader {
if ($strictness === MUST_EXIST && $result === false) {
throw new coding_exception('Requested key did not exist in any cache stores and could not be loaded.');
}
// 6. Set it to the store if we got it from the loader/datasource.
// 6. Set it to the store if we got it from the loader/datasource. Only set to this direct
// store; parent method will have set it to all stores if needed.
if ($setaftervalidation) {
$this->set($key, $result);
if ($requiredversion === self::VERSION_NONE) {
$this->set_implementation($key, self::VERSION_NONE, $result, false);
} else {
$this->set_implementation($key, $actualversion, $result, false);
}
}
// 7. Make sure we don't pass back anything that could be a reference.
// We don't want people modifying the data in the cache.
@@ -630,10 +782,52 @@ class cache implements cache_loader {
* @return bool True on success, false otherwise.
*/
public function set($key, $data) {
if ($this->loader !== false) {
return $this->set_implementation($key, self::VERSION_NONE, $data);
}
/**
* Sets the value for the given key with the given version.
*
* The cache does not store multiple versions - any existing version will be overwritten with
* this one. This function should only be used if there is a known 'current version' (e.g.
* stored in a database table). It only ensures that the cache does not return outdated data.
*
* This function can be used to help implement localisable caches (where the cache could be
* stored on a local server as well as a shared cache). The version will be recorded alongside
* the item and get_versioned will always return the correct version.
*
* The version number must be an integer that always increases. This could be based on the
* current time, or a stored value that increases by 1 each time it changes, etc.
*
* If you use this function you must use get_versioned to retrieve the data.
*
* @param string|int $key The key for the data being set.
* @param int $version Integer for the version of the data
* @param mixed $data The data to set against the key.
* @return bool True on success, false otherwise.
*/
public function set_versioned($key, int $version, $data): bool {
return $this->set_implementation($key, $version, $data);
}
/**
* Sets the value for the given key, optionally with a version tag.
*
* @param string|int $key The key for the data being set.
* @param int $version Version number for the data or cache::VERSION_NONE if none
* @param mixed $data The data to set against the key.
* @param bool $setparents If true, sets all parent loaders, otherwise only this one
* @return bool True on success, false otherwise.
*/
protected function set_implementation($key, int $version, $data, bool $setparents = true): bool {
if ($this->loader !== false && $setparents) {
// We have a loader available set it there as well.
// We have to let the loader do its own parsing of data as it may be unique.
$this->loader->set($key, $data);
if ($version === self::VERSION_NONE) {
$this->loader->set($key, $data);
} else {
$this->loader->set_versioned($key, $version, $data);
}
}
$usestaticacceleration = $this->use_static_acceleration();
@@ -648,7 +842,12 @@ class cache implements cache_loader {
}
if ($usestaticacceleration) {
$this->static_acceleration_set($key, $data);
// Static acceleration cache should include the cache version wrapper, but not TTL.
if ($version === self::VERSION_NONE) {
$this->static_acceleration_set($key, $data);
} else {
$this->static_acceleration_set($key, new \core_cache\version_wrapper($data, $version));
}
}
if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) {
@@ -656,6 +855,10 @@ class cache implements cache_loader {
}
$parsedkey = $this->parse_key($key);
if ($version !== self::VERSION_NONE) {
$data = new \core_cache\version_wrapper($data, $version);
}
$success = $this->store->set($parsedkey, $data);
if ($this->perfdebug) {
cache_helper::record_cache_set($this->store, $this->definition, 1,
@@ -1505,14 +1708,16 @@ class cache_application extends cache implements cache_loader_with_locking {
* </code>
*
* @param string|int $key The key for the data being requested.
* @param int $version Version number
* @param mixed $data The data to set against the key.
* @param bool $setparents If true, sets all parent loaders, otherwise only this one
* @return bool True on success, false otherwise.
*/
public function set($key, $data) {
protected function set_implementation($key, int $version, $data, bool $setparents = true): bool {
if ($this->requirelockingwrite && !$this->acquire_lock($key)) {
return false;
}
$result = parent::set($key, $data);
$result = parent::set_implementation($key, $version, $data, $setparents);
if ($this->requirelockingwrite && !$this->release_lock($key)) {
debugging('Failed to release cache lock on set operation... this should not happen.', DEBUG_DEVELOPER);
}
@@ -1569,15 +1774,17 @@ class cache_application extends cache implements cache_loader_with_locking {
* Retrieves the value for the given key from the cache.
*
* @param string|int $key The key for the data being requested.
* @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE
* @param int $strictness One of IGNORE_MISSING | MUST_EXIST
* @param mixed &$actualversion If specified, will be set to the actual version number retrieved
* @return mixed|false The data from the cache or false if the key did not exist within the cache.
*/
public function get($key, $strictness = IGNORE_MISSING) {
protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) {
if ($this->requirelockingread && $this->check_lock_state($key) === false) {
// Read locking required and someone else has the read lock.
return false;
}
return parent::get($key, $strictness);
return parent::get_implementation($key, $requiredversion, $strictness, $actualversion);
}
/**
@@ -1830,67 +2037,18 @@ class cache_session extends cache {
* @param string|int $key The key for the data being requested.
* It can be any structure although using a scalar string or int is recommended in the interests of performance.
* In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
* @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE
* @param int $strictness One of IGNORE_MISSING | MUST_EXIST
* @param mixed &$actualversion If specified, will be set to the actual version number retrieved
* @return mixed|false The data from the cache or false if the key did not exist within the cache.
* @throws coding_exception
*/
public function get($key, $strictness = IGNORE_MISSING) {
protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) {
// Check the tracked user.
$this->check_tracked_user();
// 2. Parse the key.
$parsedkey = $this->parse_key($key);
// 3. Get it from the store.
$result = $this->get_store()->get($parsedkey);
if ($result !== false) {
if ($result instanceof cache_ttl_wrapper) {
if ($result->has_expired()) {
$this->get_store()->delete($parsedkey);
$result = false;
} else {
$result = $result->data;
}
}
if ($result instanceof cache_cached_object) {
$result = $result->restore_object();
}
if ($this->perfdebug) {
$readbytes = $this->get_store()->get_last_io_bytes();
}
}
// 4. Load if from the loader/datasource if we don't already have it.
if ($result === false) {
if ($this->perfdebug) {
cache_helper::record_cache_miss($this->get_store(), $this->get_definition());
}
if ($this->get_loader() !== false) {
// We must pass the original (unparsed) key to the next loader in the chain.
// The next loader will parse the key as it sees fit. It may be parsed differently
// depending upon the capabilities of the store associated with the loader.
$result = $this->get_loader()->get($key);
} else if ($this->get_datasource() !== false) {
$result = $this->get_datasource()->load_for_cache($key);
}
// 5. Set it to the store if we got it from the loader/datasource.
if ($result !== false) {
$this->set($key, $result);
}
} else if ($this->perfdebug) {
cache_helper::record_cache_hit($this->get_store(), $this->get_definition(), 1, $readbytes);
}
// 5. Validate strictness.
if ($strictness === MUST_EXIST && $result === false) {
throw new coding_exception('Requested key did not exist in any cache stores and could not be loaded.');
}
// 6. Make sure we don't pass back anything that could be a reference.
// We don't want people modifying the data in the cache.
if (!$this->get_store()->supports_dereferencing_objects() && !is_scalar($result)) {
// If data is an object it will be a reference.
// If data is an array if may contain references.
// We want to break references so that the cache cannot be modified outside of itself.
// Call the function to unreference it (in the best way possible).
$result = $this->unref($result);
}
return $result;
// Use parent code.
return parent::get_implementation($key, $requiredversion, $strictness, $actualversion);
}
/**
+50
View File
@@ -0,0 +1,50 @@
<?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_cache;
/**
* Class wrapping information in the cache that is tagged with a version number.
*
* @package core_cache
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class version_wrapper {
/**
* The data being stored.
* @var mixed
*/
public $data;
/**
* Version number for the data
* @var int
*/
public $version;
/**
* Constructs a version tag wrapper.
*
* @param mixed $data
* @param int $version Version number
*/
public function __construct($data, int $version) {
$this->data = $data;
$this->version = $version;
}
}
+20 -5
View File
@@ -61,14 +61,27 @@ class cache_disabled extends cache {
* Gets a key from the cache.
*
* @param int|string $key
* @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE
* @param int $strictness Unused.
* @param mixed &$actualversion If specified, will be set to the actual version number retrieved
* @return bool
*/
public function get($key, $strictness = IGNORE_MISSING) {
if ($this->get_datasource() !== false) {
return $this->get_datasource()->load_for_cache($key);
protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) {
$datasource = $this->get_datasource();
if ($datasource !== false) {
if ($requiredversion === cache::VERSION_NONE) {
return $datasource->load_for_cache($key);
} else {
if (!$datasource instanceof cache_data_source_versionable) {
throw new \coding_exception('Data source is not versionable');
}
$result = $datasource->load_for_cache_versioned($key, $requiredversion, $actualversion);
if ($result && $actualversion < $requiredversion) {
throw new \coding_exception('Data source returned outdated version');
}
return $result;
}
}
return false;
}
@@ -91,10 +104,12 @@ class cache_disabled extends cache {
* Sets a key value pair in the cache.
*
* @param int|string $key Unused.
* @param int $version Unused.
* @param mixed $data Unused.
* @param bool $setparents Unused.
* @return bool
*/
public function set($key, $data) {
protected function set_implementation($key, int $version, $data, bool $setparents = true): bool {
return false;
}
+1 -1
View File
@@ -225,4 +225,4 @@ class cacheable_object_array extends ArrayObject implements cacheable_object {
$class = __CLASS__;
return new $class($result);
}
}
}
+456 -18
View File
@@ -14,32 +14,46 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* PHPunit tests for the cache API
*
* This file is part of Moodle's cache API, affectionately called MUC.
* It contains the components that are requried in order to use caching.
*
* @package core
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_cache;
defined('MOODLE_INTERNAL') || die();
// Include the necessary evils.
global $CFG;
require_once($CFG->dirroot.'/cache/locallib.php');
require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
use advanced_testcase;
use cache;
use cache_config;
use cache_config_testing;
use cache_factory;
use cache_helper;
use cache_loader;
use cache_phpunit_application;
use cache_phpunit_cache;
use cache_phpunit_dummy_object;
use cache_phpunit_factory;
use cache_phpunit_session;
use cacheable_object_array;
use cache_store;
use coding_exception;
use Exception;
use stdClass;
/**
* PHPunit tests for the cache API
*
* @package core_cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \cache
*/
class core_cache_testcase extends advanced_testcase {
class cache_test extends advanced_testcase {
/**
* Load required libraries and fixtures.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/cache/locallib.php');
require_once($CFG->dirroot . '/cache/tests/fixtures/lib.php');
require_once($CFG->dirroot . '/cache/tests/fixtures/cache_phpunit_dummy_datasource_versionable.php');
}
/**
* Set things back to the default before each test.
@@ -549,6 +563,61 @@ class core_cache_testcase extends advanced_testcase {
$this->assertEquals('c has no value really.', $result['c']);
}
/**
* Tests a definition using a data loader with versioned keys.
*
* @covers ::get_versioned
* @covers ::set_versioned
*/
public function test_definition_data_loader_versioned() {
// Create two definitions, one using a non-versionable data source and the other using
// a versionable one.
$instance = cache_config_testing::instance(true);
$instance->phpunit_add_definition('phpunit/datasourcetest1', array(
'mode' => cache_store::MODE_APPLICATION,
'component' => 'phpunit',
'area' => 'datasourcetest1',
'datasource' => 'cache_phpunit_dummy_datasource',
'datasourcefile' => 'cache/tests/fixtures/lib.php'
));
$instance->phpunit_add_definition('phpunit/datasourcetest2', array(
'mode' => cache_store::MODE_APPLICATION,
'component' => 'phpunit',
'area' => 'datasourcetest2',
'datasource' => 'cache_phpunit_dummy_datasource_versionable',
'datasourcefile' => 'cache/tests/fixtures/lib.php'
));
// The first data source works for normal 'get'.
$cache1 = cache::make('phpunit', 'datasourcetest1');
$this->assertEquals('Frog has no value really.', $cache1->get('Frog'));
// But it doesn't work for get_versioned.
try {
$cache1->get_versioned('zombie', 1);
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Data source is not versionable', $e->getMessage());
}
// The second data source works for get_versioned. Set up the datasource first.
$cache2 = cache::make('phpunit', 'datasourcetest2');
$datasource = \cache_phpunit_dummy_datasource_versionable::get_last_instance();
$datasource->has_value('frog', 3, 'Kermit');
// Check data with no value.
$this->assertFalse($cache2->get_versioned('zombie', 1));
// Check data with value in datastore of required version.
$result = $cache2->get_versioned('frog', 3, IGNORE_MISSING, $actualversion);
$this->assertEquals('Kermit', $result);
$this->assertEquals(3, $actualversion);
// Check when the datastore doesn't have required version.
$this->assertFalse($cache2->get_versioned('frog', 4));
}
/**
* Tests a definition using an overridden loader
*/
@@ -1343,7 +1412,9 @@ class core_cache_testcase extends advanced_testcase {
$this->assertInstanceOf('cache_disabled', $cache);
$this->assertFalse($cache->get('test'));
$this->assertFalse($cache->get_versioned('v', 1));
$this->assertFalse($cache->set('test', 'test'));
$this->assertFalse($cache->set_versioned('v', 1, 'data'));
$this->assertFalse($cache->delete('test'));
$this->assertTrue($cache->purge());
@@ -1352,7 +1423,9 @@ class core_cache_testcase extends advanced_testcase {
$this->assertInstanceOf('cache_disabled', $cache);
$this->assertFalse($cache->get('test'));
$this->assertFalse($cache->get_versioned('v', 1));
$this->assertFalse($cache->set('test', 'test'));
$this->assertFalse($cache->set_versioned('v', 1, 'data'));
$this->assertFalse($cache->delete('test'));
$this->assertTrue($cache->purge());
@@ -1361,7 +1434,9 @@ class core_cache_testcase extends advanced_testcase {
$this->assertInstanceOf('cache_disabled', $cache);
$this->assertFalse($cache->get('test'));
$this->assertFalse($cache->get_versioned('v', 1));
$this->assertFalse($cache->set('test', 'test'));
$this->assertFalse($cache->set_versioned('v', 1, 'data'));
$this->assertFalse($cache->delete('test'));
$this->assertTrue($cache->purge());
@@ -1440,6 +1515,369 @@ class core_cache_testcase extends advanced_testcase {
$this->assertSame(array('two' => 'two', 'three' => 'three'), $cache->get_many(array('two', 'three')));
}
/**
* Data provider to try using a TTL or non-TTL cache.
*
* @return array
*/
public function ttl_or_not(): array {
return [[false], [true]];
}
/**
* Data provider to try using a TTL or non-TTL cache, and static acceleration or not.
*
* @return array
*/
public function ttl_and_static_acceleration_or_not(): array {
return [[false, false], [false, true], [true, false], [true, true]];
}
/**
* Data provider to try using a TTL or non-TTL cache, and simple data on or off.
*
* @return array
*/
public function ttl_and_simple_data_or_not(): array {
// Same values as for ttl and static acceleration (two booleans).
return $this->ttl_and_static_acceleration_or_not();
}
/**
* Shared code to set up a two or three-layer versioned cache for testing.
*
* @param bool $ttl If true, sets TTL in the definition
* @param bool $threelayer If true, uses a 3-layer instead of 2-layer cache
* @param bool $staticacceleration If true, enables static acceleration
* @param bool $simpledata If true, enables simple data
* @return \cache_application Cache
*/
protected function create_versioned_cache(bool $ttl, bool $threelayer = false,
bool $staticacceleration = false, bool $simpledata = false): \cache_application {
$instance = cache_config_testing::instance(true);
$instance->phpunit_add_file_store('a', false);
$instance->phpunit_add_file_store('b', false);
if ($threelayer) {
$instance->phpunit_add_file_store('c', false);
}
$defarray = [
'mode' => cache_store::MODE_APPLICATION,
'component' => 'phpunit',
'area' => 'multi_loader'
];
if ($ttl) {
$defarray['ttl'] = '600';
}
if ($staticacceleration) {
$defarray['staticacceleration'] = true;
$defarray['staticaccelerationsize'] = 10;
}
if ($simpledata) {
$defarray['simpledata'] = true;
}
$instance->phpunit_add_definition('phpunit/multi_loader', $defarray, false);
$instance->phpunit_add_definition_mapping('phpunit/multi_loader', 'a', 1);
$instance->phpunit_add_definition_mapping('phpunit/multi_loader', 'b', 2);
if ($threelayer) {
$instance->phpunit_add_definition_mapping('phpunit/multi_loader', 'c', 3);
}
$multicache = cache::make('phpunit', 'multi_loader');
return $multicache;
}
/**
* Tests basic use of versioned cache.
*
* @dataProvider ttl_and_simple_data_or_not
* @param bool $ttl If true, uses a TTL cache.
* @param bool $simpledata If true, turns on simple data flag
* @covers ::set_versioned
* @covers ::get_versioned
*/
public function test_versioned_cache_basic(bool $ttl, bool $simpledata): void {
$multicache = $this->create_versioned_cache($ttl, false, false, $simpledata);
$this->assertTrue($multicache->set_versioned('game', 1, 'Pooh-sticks'));
$result = $multicache->get_versioned('game', 1, IGNORE_MISSING, $actualversion);
$this->assertEquals('Pooh-sticks', $result);
$this->assertEquals(1, $actualversion);
}
/**
* Tests versioned cache with objects.
*
* @dataProvider ttl_and_static_acceleration_or_not
* @param bool $ttl If true, uses a TTL cache.
* @param bool $staticacceleration If true, enables static acceleration
* @covers ::set_versioned
* @covers ::get_versioned
*/
public function test_versioned_cache_objects(bool $ttl, bool $staticacceleration): void {
$multicache = $this->create_versioned_cache($ttl, false, $staticacceleration);
// Set an object value.
$data = (object)['game' => 'Pooh-sticks'];
$this->assertTrue($multicache->set_versioned('game', 1, $data));
// Get it.
$result = $multicache->get_versioned('game', 1);
$this->assertEquals('Pooh-sticks', $result->game);
// Mess about with the value in the returned object.
$result->game = 'Tag';
// Get it again and confirm the cached object has not been affected.
$result = $multicache->get_versioned('game', 1);
$this->assertEquals('Pooh-sticks', $result->game);
}
/**
* Tests requesting a version that doesn't exist.
*
* @dataProvider ttl_or_not
* @param bool $ttl If true, uses a TTL cache.
* @covers ::set_versioned
* @covers ::get_versioned
*/
public function test_versioned_cache_not_exist(bool $ttl): void {
$multicache = $this->create_versioned_cache($ttl);
$multicache->set_versioned('game', 1, 'Pooh-sticks');
// Exists but with wrong version.
$this->assertFalse($multicache->get_versioned('game', 2));
// Doesn't exist at all.
$this->assertFalse($multicache->get_versioned('frog', 0));
}
/**
* Tests attempts to use get after set_version or get_version after set.
*
* @dataProvider ttl_or_not
* @param bool $ttl If true, uses a TTL cache.
* @covers ::set_versioned
* @covers ::get_versioned
*/
public function test_versioned_cache_incompatible_versioning(bool $ttl): void {
$multicache = $this->create_versioned_cache($ttl);
// What if you use get on a get_version cache?
$multicache->set_versioned('game', 1, 'Pooh-sticks');
try {
$multicache->get('game');
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Unexpectedly found versioned cache entry', $e->getMessage());
}
// Or get_version on a get cache?
$multicache->set('toy', 'Train set');
try {
$multicache->get_versioned('toy', 1);
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Unexpectedly found non-versioned cache entry', $e->getMessage());
}
}
/**
* Versions are only stored once, so if you set a newer version you will always get it even
* if you ask for the lower version number.
*
* @dataProvider ttl_or_not
* @param bool $ttl If true, uses a TTL cache.
* @covers ::set_versioned
* @covers ::get_versioned
*/
public function test_versioned_cache_single_copy(bool $ttl): void {
$multicache = $this->create_versioned_cache($ttl);
$multicache->set_versioned('game', 1, 'Pooh-sticks');
$multicache->set_versioned('game', 2, 'Tag');
$this->assertEquals('Tag', $multicache->get_versioned('game', 1, IGNORE_MISSING, $actualversion));
// The reported version number matches the one returned, not requested.
$this->assertEquals(2, $actualversion);
}
/**
* If the first (local) store has an outdated copy but the second (shared) store has a newer
* one, then it should automatically be retrieved.
*
* @dataProvider ttl_or_not
* @param bool $ttl If true, uses a TTL cache.
* @covers ::set_versioned
* @covers ::get_versioned
*/
public function test_versioned_cache_outdated_local(bool $ttl): void {
$multicache = $this->create_versioned_cache($ttl);
// Set initial value to version 2, 'Tag', in both stores.
$multicache->set_versioned('game', 2, 'Tag');
// Get the two separate cache stores for the multi-level cache.
$factory = cache_factory::instance();
$definition = $factory->create_definition('phpunit', 'multi_loader');
[0 => $storea, 1 => $storeb] = $factory->get_store_instances_in_use($definition);
// Simulate what happens if the shared cache is updated with a new version but the
// local one still has an old version.
$hashgame = cache_helper::hash_key('game', $definition);
$data = 'British Bulldog';
if ($ttl) {
$data = new \cache_ttl_wrapper($data, 600);
}
$storeb->set($hashgame, new \core_cache\version_wrapper($data, 3));
// If we ask for the old one we'll get it straight off from local cache.
$this->assertEquals('Tag', $multicache->get_versioned('game', 2));
// But if we ask for the new one it will still get it via the shared cache.
$this->assertEquals('British Bulldog', $multicache->get_versioned('game', 3));
// Also, now it will have been updated in the local cache as well.
$localvalue = $storea->get($hashgame);
if ($ttl) {
// In case the time has changed slightly since the first set, we can't do an exact
// compare, so check it ignoring the time field.
$this->assertEquals(3, $localvalue->version);
$ttldata = $localvalue->data;
$this->assertInstanceOf('cache_ttl_wrapper', $ttldata);
$this->assertEquals('British Bulldog', $ttldata->data);
} else {
$this->assertEquals(new \core_cache\version_wrapper('British Bulldog', 3), $localvalue);
}
}
/**
* When we request a newer version, older ones are automatically deleted in every level of the
* cache (to save I/O if there are multiple requests, as if there is another request it will
* not have to retrieve the values to find out that they're old).
*
* @dataProvider ttl_or_not
* @param bool $ttl If true, uses a TTL cache.
* @covers ::set_versioned
* @covers ::get_versioned
*/
public function test_versioned_cache_deleting_outdated(bool $ttl): void {
$multicache = $this->create_versioned_cache($ttl);
// Set initial value to version 2, 'Tag', in both stores.
$multicache->set_versioned('game', 2, 'Tag');
// Get the two separate cache stores for the multi-level cache.
$factory = cache_factory::instance();
$definition = $factory->create_definition('phpunit', 'multi_loader');
[0 => $storea, 1 => $storeb] = $factory->get_store_instances_in_use($definition);
// If we request a newer version, then any older version should be deleted in each
// cache level.
$this->assertFalse($multicache->get_versioned('game', 4));
$hashgame = cache_helper::hash_key('game', $definition);
$this->assertFalse($storea->get($hashgame));
$this->assertFalse($storeb->get($hashgame));
}
/**
* Tests a versioned cache when using static cache.
*
* @covers ::set_versioned
* @covers ::get_versioned
*/
public function test_versioned_cache_static(): void {
$staticcache = $this->create_versioned_cache(false, false, true);
// Set a value in the cache, version 1. This will store it in static acceleration.
$staticcache->set_versioned('game', 1, 'Pooh-sticks');
// Get the first cache store (we don't need the second one for this test).
$factory = cache_factory::instance();
$definition = $factory->create_definition('phpunit', 'multi_loader');
[0 => $storea] = $factory->get_store_instances_in_use($definition);
// Hack a newer version into cache store without directly calling set (now the static
// has v1, store has v2). This simulates another client updating the cache.
$hashgame = cache_helper::hash_key('game', $definition);
$storea->set($hashgame, new \core_cache\version_wrapper('Tag', 2));
// Get the key from the cache, v1. This will use static acceleration.
$this->assertEquals('Pooh-sticks', $staticcache->get_versioned('game', 1));
// Now if we ask for a newer version, it should not use the static cached one.
$this->assertEquals('Tag', $staticcache->get_versioned('game', 2));
// This get should have updated static acceleration, so it will be used next time without
// a store request.
$storea->set($hashgame, new \core_cache\version_wrapper('British Bulldog', 3));
$this->assertEquals('Tag', $staticcache->get_versioned('game', 2));
// Requesting the higher version will get rid of static acceleration again.
$this->assertEquals('British Bulldog', $staticcache->get_versioned('game', 3));
// Finally ask for a version that doesn't exist anywhere, just to confirm it returns null.
$this->assertFalse($staticcache->get_versioned('game', 4));
}
/**
* Tests basic use of 3-layer versioned caches.
*
* @covers ::set_versioned
* @covers ::get_versioned
*/
public function test_versioned_cache_3_layers_basic(): void {
$multicache = $this->create_versioned_cache(false, true);
// Basic use of set_versioned and get_versioned.
$multicache->set_versioned('game', 1, 'Pooh-sticks');
$this->assertEquals('Pooh-sticks', $multicache->get_versioned('game', 1));
// What if you ask for a version that doesn't exist?
$this->assertFalse($multicache->get_versioned('game', 2));
// Setting a new version wipes out the old version; if you request it, you get the new one.
$multicache->set_versioned('game', 2, 'Tag');
$this->assertEquals('Tag', $multicache->get_versioned('game', 1));
}
/**
* Tests use of 3-layer versioned caches where the 3 layers currently have different versions.
*
* @covers ::set_versioned
* @covers ::get_versioned
*/
public function test_versioned_cache_3_layers_different_data(): void {
// Set version 2 using normal method.
$multicache = $this->create_versioned_cache(false, true);
$multicache->set_versioned('game', 2, 'Tag');
// Get the three separate cache stores for the multi-level cache.
$factory = cache_factory::instance();
$definition = $factory->create_definition('phpunit', 'multi_loader');
[0 => $storea, 1 => $storeb, 2 => $storec] = $factory->get_store_instances_in_use($definition);
// Set up two other versions so every level has a different version.
$hashgame = cache_helper::hash_key('game', $definition);
$storeb->set($hashgame, new \core_cache\version_wrapper('British Bulldog', 3));
$storec->set($hashgame, new \core_cache\version_wrapper('Hopscotch', 4));
// First request can be satisfied from A; second request requires B...
$this->assertEquals('Tag', $multicache->get_versioned('game', 2));
$this->assertEquals('British Bulldog', $multicache->get_versioned('game', 3));
// And should update the data in A.
$this->assertEquals(new \core_cache\version_wrapper('British Bulldog', 3), $storea->get($hashgame));
$this->assertEquals('British Bulldog', $multicache->get_versioned('game', 1));
// But newer data should still be in C.
$this->assertEquals('Hopscotch', $multicache->get_versioned('game', 4));
// Now it's stored in A and B too.
$this->assertEquals(new \core_cache\version_wrapper('Hopscotch', 4), $storea->get($hashgame));
$this->assertEquals(new \core_cache\version_wrapper('Hopscotch', 4), $storeb->get($hashgame));
}
/**
* Test that multiple application loaders work ok.
*/
@@ -0,0 +1,84 @@
<?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/>.
/**
* A dummy datasource which supports versioning.
*
* @package core_cache
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_phpunit_dummy_datasource_versionable extends cache_phpunit_dummy_datasource
implements cache_data_source_versionable {
/** @var array Data in cache */
protected $data = [];
/** @var cache_phpunit_dummy_datasource_versionable Last created instance */
protected static $lastinstance;
/**
* Returns an instance of this object for use with the cache.
*
* @param cache_definition $definition
* @return cache_phpunit_dummy_datasource New object
*/
public static function get_instance_for_cache(cache_definition $definition):
cache_phpunit_dummy_datasource_versionable {
self::$lastinstance = new cache_phpunit_dummy_datasource_versionable();
return self::$lastinstance;
}
/**
* Gets the last instance that was created.
*
* @return cache_phpunit_dummy_datasource_versionable
*/
public static function get_last_instance(): cache_phpunit_dummy_datasource_versionable {
return self::$lastinstance;
}
/**
* Sets up the datasource so that it has a value for a particular key.
*
* @param string $key Key
* @param int $version Version for key
* @param mixed $data
*/
public function has_value(string $key, int $version, $data): void {
$this->data[$key] = new \core_cache\version_wrapper($data, $version);
}
/**
* Loads versioned data.
*
* @param int|string $key Key
* @param int $requiredversion Minimum version number
* @param mixed $actualversion Should be set to the actual version number retrieved
* @return mixed Data retrieved from cache or false if none
*/
public function load_for_cache_versioned($key, int $requiredversion, &$actualversion) {
if (!array_key_exists($key, $this->data)) {
return false;
}
$value = $this->data[$key];
if ($value->version < $requiredversion) {
return false;
}
$actualversion = $value->version;
return $value->data;
}
}
@@ -0,0 +1,37 @@
<?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/>.
/**
* A subclass of cachestore_file but which doesn't report that it has TTL support.
*
* This is so we can easily test behaviour involving the TTL wrapper objects.
*
* @package core_cache
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_file_with_ttl_wrappers extends cachestore_file {
/**
* Reports the same supported features as the parent, but without SUPPORTS_NATIVE_TTL.
*
* @param array $configuration Configuration
* @return int Supported features
*/
public static function get_supported_features(array $configuration = array()) {
return parent::get_supported_features($configuration) - self::SUPPORTS_NATIVE_TTL;
}
}
+8 -2
View File
@@ -225,9 +225,15 @@ class cache_config_testing extends cache_config_writer {
/**
* Forcefully adds a file store.
*
* You can turn off native TTL support if you want a way to test TTL wrapper objects.
*
* @param string $name
* @param bool $nativettl If false, uses fixture that turns off native TTL support
*/
public function phpunit_add_file_store($name) {
public function phpunit_add_file_store(string $name, bool $nativettl = true): void {
if (!$nativettl) {
require_once(__DIR__ . '/cachestore_file_with_ttl_wrappers.php');
}
$this->configstores[$name] = array(
'name' => $name,
'plugin' => 'file',
@@ -237,7 +243,7 @@ class cache_config_testing extends cache_config_writer {
'features' => 6,
'modes' => 3,
'mappingsonly' => false,
'class' => 'cachestore_file',
'class' => $nativettl ? 'cachestore_file' : 'cachestore_file_with_ttl_wrappers',
'default' => false,
'lock' => 'cachelock_file_default'
);
+4
View File
@@ -7,6 +7,10 @@ Information provided here is intended especially for developers.
* The cache_store class now has functions cache_size_details(), store_total_size(), and
estimate_stored_size(), related to size used by the cache. These can be overridden by a cache
store to provide better information for the new cache usage admin page.
* New functions cache::set_versioned() and cache::get_versioned() can be used to ensure correct
behaviour when using a multi-level cache with early cache levels stored locally. (Used when
rebuilding modinfo.) There is also a new interface cache_data_source_versionable which can
be implemented if you want to make a data source that supports versioning.
=== 3.10 ===
* The function supports_recursion() from the lock_factory interface has been deprecated including the related implementations.
+49 -10
View File
@@ -14,23 +14,61 @@
// 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_course;
use advanced_testcase;
use backup_controller;
use backup;
use blog_entry;
use cache;
use calendar_event;
use coding_exception;
use comment;
use completion_criteria_date;
use completion_completion;
use context_course;
use context_module;
use context_system;
use context_coursecat;
use core_completion_external;
use core_external;
use core_tag_index_builder;
use core_tag_tag;
use course_capability_assignment;
use course_request;
use core_course_category;
use enrol_imsenterprise_testcase;
use external_api;
use grade_item;
use grading_manager;
use moodle_exception;
use moodle_url;
use phpunit_util;
use rating_manager;
use restore_controller;
use stdClass;
use testing_data_generator;
/**
* Course related unit tests
*
* @package core
* @package core_course
* @category phpunit
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class courselib_test extends advanced_testcase {
defined('MOODLE_INTERNAL') || die();
/**
* Load required libraries and fixtures.
*/
public static function setUpBeforeClass(): void {
global $CFG;
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/course/tests/fixtures/course_capability_assignment.php');
require_once($CFG->dirroot . '/enrol/imsenterprise/tests/imsenterprise_test.php');
class core_course_courselib_testcase extends advanced_testcase {
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/course/tests/fixtures/course_capability_assignment.php');
require_once($CFG->dirroot . '/enrol/imsenterprise/tests/imsenterprise_test.php');
}
/**
* Set forum specific test values for calling create_module().
@@ -846,8 +884,9 @@ class core_course_courselib_testcase extends advanced_testcase {
$this->assertEquals($cmids[0] . ',' . $cmids[1], $sequence);
// Check that modinfo cache was reset but not rebuilt (important for performance if calling repeatedly).
$this->assertGreaterThan($coursecacherev, $DB->get_field('course', 'cacherev', array('id' => $course->id)));
$this->assertEmpty(cache::make('core', 'coursemodinfo')->get($course->id));
$newcacherev = $DB->get_field('course', 'cacherev', ['id' => $course->id]);
$this->assertGreaterThan($coursecacherev, $newcacherev);
$this->assertEmpty(cache::make('core', 'coursemodinfo')->get_versioned($course->id, $newcacherev));
// Add one to section that doesn't exist (this might rebuild modinfo).
course_add_cm_to_section($course, $cmids[2], 2);
+10 -15
View File
@@ -475,14 +475,14 @@ class course_modinfo {
$cachecoursemodinfo = cache::make('core', 'coursemodinfo');
// Retrieve modinfo from cache. If not present or cacherev mismatches, call rebuild and retrieve again.
$coursemodinfo = $cachecoursemodinfo->get($course->id);
if ($coursemodinfo === false || ($course->cacherev != $coursemodinfo->cacherev)) {
$coursemodinfo = $cachecoursemodinfo->get_versioned($course->id, $course->cacherev);
if (!$coursemodinfo) {
$lock = self::get_course_cache_lock($course->id);
try {
// Only actually do the build if it's still needed after getting the lock (not if
// somebody else, who might have been holding the lock, built it already).
$coursemodinfo = $cachecoursemodinfo->get($course->id);
if ($coursemodinfo === false || ($course->cacherev != $coursemodinfo->cacherev)) {
$coursemodinfo = $cachecoursemodinfo->get_versioned($course->id, $course->cacherev);
if (!$coursemodinfo) {
$coursemodinfo = self::inner_build_course_cache($course, $lock);
}
} finally {
@@ -681,17 +681,12 @@ class course_modinfo {
global $DB, $CFG;
require_once("{$CFG->dirroot}/course/lib.php");
// Ensure object has all necessary fields.
foreach (self::$cachedfields as $key) {
if (!isset($course->$key)) {
$course = $DB->get_record('course', array('id' => $course->id),
implode(',', array_merge(array('id'), self::$cachedfields)), MUST_EXIST);
break;
}
}
// Always reload the course object from database to ensure we have the latest possible
// value for cacherev.
$course = $DB->get_record('course', ['id' => $course->id],
implode(',', array_merge(['id'], self::$cachedfields)), MUST_EXIST);
// Retrieve all information about activities and sections.
// This may take time on large courses and it is possible that another user modifies the same course during this process.
// Field cacherev stored in both DB and cache will ensure that cached data matches the current course state.
$coursemodinfo = new stdClass();
$coursemodinfo->modinfo = get_array_of_activities($course->id);
$coursemodinfo->sectioncache = self::build_course_section_cache($course);
@@ -700,7 +695,7 @@ class course_modinfo {
}
// Set the accumulated activities and sections information in cache, together with cacherev.
$cachecoursemodinfo = cache::make('core', 'coursemodinfo');
$cachecoursemodinfo->set($course->id, $coursemodinfo);
$cachecoursemodinfo->set_versioned($course->id, $course->cacherev, $coursemodinfo);
return $coursemodinfo;
}
}
+21 -19
View File
@@ -14,23 +14,25 @@
// 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 advanced_testcase;
use cache;
use cm_info;
use coding_exception;
use context_course;
use context_module;
use course_modinfo;
use moodle_exception;
use moodle_url;
use Exception;
/**
* Unit tests for lib/modinfolib.php.
*
* @package core
* @category phpunit
* @copyright 2012 Andrew Davis
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/modinfolib.php');
/**
* Unit tests for modinfolib.php
*
* @copyright 2012 Andrew Davis
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class modinfolib_test extends advanced_testcase {
@@ -258,28 +260,28 @@ class modinfolib_test extends advanced_testcase {
rebuild_course_cache($course->id, true);
$cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
$this->assertGreaterThan($prevcacherev, $cacherev);
$this->assertEmpty($cache->get($course->id));
$this->assertEmpty($cache->get_versioned($course->id, $prevcacherev));
$prevcacherev = $cacherev;
// Build course cache. Cacherev should not change but cache is now not empty. Make sure cacherev is the same everywhere.
$modinfo = get_fast_modinfo($course->id);
$cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
$this->assertEquals($prevcacherev, $cacherev);
$cachedvalue = $cache->get($course->id);
$cachedvalue = $cache->get_versioned($course->id, $cacherev);
$this->assertNotEmpty($cachedvalue);
$this->assertEquals($cacherev, $cachedvalue->cacherev);
$this->assertEquals($cacherev, $modinfo->get_course()->cacherev);
$prevcacherev = $cacherev;
// Little trick to check that cache is not rebuilt druing the next step - substitute the value in MUC and later check that it is still there.
$cache->set($course->id, (object)array_merge((array)$cachedvalue, array('secretfield' => 1)));
$cache->set_versioned($course->id, $cacherev, (object)array_merge((array)$cachedvalue, array('secretfield' => 1)));
// Clear static cache and call get_fast_modinfo() again (pretend we are in another request). Cache should not be rebuilt.
course_modinfo::clear_instance_cache();
$modinfo = get_fast_modinfo($course->id);
$cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
$this->assertEquals($prevcacherev, $cacherev);
$cachedvalue = $cache->get($course->id);
$cachedvalue = $cache->get_versioned($course->id, $cacherev);
$this->assertNotEmpty($cachedvalue);
$this->assertEquals($cacherev, $cachedvalue->cacherev);
$this->assertNotEmpty($cachedvalue->secretfield);
@@ -290,7 +292,7 @@ class modinfolib_test extends advanced_testcase {
rebuild_course_cache($course->id);
$cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
$this->assertGreaterThan($prevcacherev, $cacherev);
$cachedvalue = $cache->get($course->id);
$cachedvalue = $cache->get_versioned($course->id, $cacherev);
$this->assertNotEmpty($cachedvalue);
$this->assertEquals($cacherev, $cachedvalue->cacherev);
$modinfo = get_fast_modinfo($course->id);
@@ -304,7 +306,7 @@ class modinfolib_test extends advanced_testcase {
$modinfo = get_fast_modinfo($course->id);
$cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
$this->assertGreaterThan($prevcacherev, $cacherev);
$cachedvalue = $cache->get($course->id);
$cachedvalue = $cache->get_versioned($course->id, $cacherev);
$this->assertNotEmpty($cachedvalue);
$this->assertEquals($cacherev, $cachedvalue->cacherev);
$this->assertEquals($cacherev, $modinfo->get_course()->cacherev);
@@ -314,10 +316,10 @@ class modinfolib_test extends advanced_testcase {
rebuild_course_cache(0, true);
$cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
$this->assertGreaterThan($prevcacherev, $cacherev);
$this->assertEmpty($cache->get($course->id));
$this->assertEmpty($cache->get_versioned($course->id, $cacherev));
// Rebuild again.
$modinfo = get_fast_modinfo($course->id);
$cachedvalue = $cache->get($course->id);
$cachedvalue = $cache->get_versioned($course->id, $cacherev);
$this->assertNotEmpty($cachedvalue);
$this->assertEquals($cacherev, $cachedvalue->cacherev);
$this->assertEquals($cacherev, $modinfo->get_course()->cacherev);