MDL-67020 Cache: Make local caching work for coursemodinfo

This commit is contained in:
Mark Johnson 2022-08-11 09:13:41 +01:00
parent 045ee091da
commit ea01fb2427
5 changed files with 39 additions and 53 deletions

View File

@ -251,6 +251,7 @@ $definitions = array(
'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'canuselocalstore' => true,
'requirelockingbeforewrite' => true
),
// This is the session user selections cache.
// It's a special cache that is used to record user selections that should persist for the lifetime of the session.

View File

@ -475,7 +475,8 @@ class course_modinfo {
// partial rebuild logic sometimes sets the $coursemodinfo->cacherev to -1 which is an
// indicator that it needs rebuilding.
if ($coursemodinfo === false || ($course->cacherev > $coursemodinfo->cacherev)) {
$lock = self::get_course_cache_lock($course->id);
$cachekey = $course->id;
$lock = $cachecoursemodinfo->acquire_lock($cachekey);
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).
@ -484,7 +485,7 @@ class course_modinfo {
$coursemodinfo = self::inner_build_course_cache($course, $lock);
}
} finally {
$lock->release();
$cachecoursemodinfo->release_lock($cachekey);
}
}
@ -631,34 +632,6 @@ class course_modinfo {
return $compressedsections;
}
/**
* Gets a lock for rebuilding the cache of a single course.
*
* Caller must release the returned lock.
*
* This is used to ensure that the cache rebuild doesn't happen multiple times in parallel.
* This function will wait up to 1 minute for the lock to be obtained. If the lock cannot
* be obtained, it throws an exception.
*
* @param int $courseid Course id
* @return \core\lock\lock Lock (must be released!)
* @throws moodle_exception If the lock cannot be obtained
*/
protected static function get_course_cache_lock($courseid) {
// Get database lock to ensure this doesn't happen multiple times in parallel. Wait a
// reasonable time for the lock to be released, so we can give a suitable error message.
// In case the system crashes while building the course cache, the lock will automatically
// expire after a (slightly longer) period.
$lockfactory = \core\lock\lock_config::get_lock_factory('core_modinfo');
$lock = $lockfactory->get_lock('build_course_cache_' . $courseid,
self::COURSE_CACHE_LOCK_WAIT, self::COURSE_CACHE_LOCK_EXPIRY);
if (!$lock) {
throw new moodle_exception('locktimeout', '', '', null,
'core_modinfo/build_course_cache_' . $courseid);
}
return $lock;
}
/**
* Builds and stores in MUC object containing information about course
* modules and sections together with cached fields from table course.
@ -676,11 +649,13 @@ class course_modinfo {
throw new coding_exception('Object $course is missing required property \id\'');
}
$lock = self::get_course_cache_lock($course->id);
$cachecoursemodinfo = cache::make('core', 'coursemodinfo');
$cachekey = $course->id;
$cachecoursemodinfo->acquire_lock($cachekey);
try {
return self::inner_build_course_cache($course, $lock, $partialrebuild);
return self::inner_build_course_cache($course, $partialrebuild);
} finally {
$lock->release();
$cachecoursemodinfo->release_lock($cachekey);
}
}
@ -688,20 +663,23 @@ class course_modinfo {
* Called to build course cache when there is already a lock obtained.
*
* @param stdClass $course object from DB table course
* @param \core\lock\lock $lock Lock object - not actually used, just there to indicate you have a lock
* @param bool $partialrebuild Indicate if it's partial course cache rebuild or not
* @return stdClass Course object that has been stored in MUC
*/
protected static function inner_build_course_cache(\stdClass $course, \core\lock\lock $lock,
bool $partialrebuild = false): \stdClass {
protected static function inner_build_course_cache(\stdClass $course, bool $partialrebuild = false): \stdClass {
global $DB, $CFG;
require_once("{$CFG->dirroot}/course/lib.php");
$cachekey = $course->id;
$cachecoursemodinfo = cache::make('core', 'coursemodinfo');
if (!$cachecoursemodinfo->check_lock_state($cachekey)) {
throw new coding_exception('You must acquire a lock on the course ID before calling inner_build_course_cache');
}
// 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.
$coursemodinfo = new stdClass();
$coursemodinfo->modinfo = self::get_array_of_activities($course, $partialrebuild);
@ -710,8 +688,7 @@ class course_modinfo {
$coursemodinfo->$key = $course->$key;
}
// Set the accumulated activities and sections information in cache, together with cacherev.
$cachecoursemodinfo = cache::make('core', 'coursemodinfo');
$cachecoursemodinfo->set_versioned($course->id, $course->cacherev, $coursemodinfo);
$cachecoursemodinfo->set_versioned($cachekey, $course->cacherev, $coursemodinfo);
return $coursemodinfo;
}
@ -724,19 +701,20 @@ class course_modinfo {
public static function purge_course_section_cache_by_id(int $courseid, int $sectionid): void {
$course = get_course($courseid);
$cache = cache::make('core', 'coursemodinfo');
$cache->acquire_lock($course->id);
$coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
$cachekey = $course->id;
$cache->acquire_lock($cachekey);
$coursemodinfo = $cache->get_versioned($cachekey, $course->cacherev);
if ($coursemodinfo !== false) {
foreach ($coursemodinfo->sectioncache as $sectionno => $sectioncache) {
if ($sectioncache->id == $sectionid) {
$coursemodinfo->cacherev = -1;
unset($coursemodinfo->sectioncache[$sectionno]);
$cache->set_versioned($course->id, $course->cacherev, $coursemodinfo);
$cache->set_versioned($cachekey, $course->cacherev, $coursemodinfo);
break;
}
}
}
$cache->release_lock($course->id);
$cache->release_lock($cachekey);
}
/**
@ -748,14 +726,15 @@ class course_modinfo {
public static function purge_course_section_cache_by_number(int $courseid, int $sectionno): void {
$course = get_course($courseid);
$cache = cache::make('core', 'coursemodinfo');
$cache->acquire_lock($course->id);
$coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
$cachekey = $course->id;
$cache->acquire_lock($cachekey);
$coursemodinfo = $cache->get_versioned($cachekey, $course->cacherev);
if ($coursemodinfo !== false && array_key_exists($sectionno, $coursemodinfo->sectioncache)) {
$coursemodinfo->cacherev = -1;
unset($coursemodinfo->sectioncache[$sectionno]);
$cache->set_versioned($course->id, $course->cacherev, $coursemodinfo);
$cache->set_versioned($cachekey, $course->cacherev, $coursemodinfo);
}
$cache->release_lock($course->id);
$cache->release_lock($cachekey);
}
/**
@ -767,16 +746,17 @@ class course_modinfo {
public static function purge_course_module_cache(int $courseid, int $cmid): void {
$course = get_course($courseid);
$cache = cache::make('core', 'coursemodinfo');
$cache->acquire_lock($course->id);
$coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
$cachekey = $course->id;
$cache->acquire_lock($cachekey);
$coursemodinfo = $cache->get_versioned($cachekey, $course->cacherev);
$hascache = ($coursemodinfo !== false) && array_key_exists($cmid, $coursemodinfo->modinfo);
if ($hascache) {
$coursemodinfo->cacherev = -1;
unset($coursemodinfo->modinfo[$cmid]);
$cache->set_versioned($course->id, $course->cacherev, $coursemodinfo);
$coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
$cache->set_versioned($cachekey, $course->cacherev, $coursemodinfo);
$coursemodinfo = $cache->get_versioned($cachekey, $course->cacherev);
}
$cache->release_lock($course->id);
$cache->release_lock($cachekey);
}
/**

View File

@ -274,7 +274,9 @@ class modinfolib_test extends advanced_testcase {
$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->acquire_lock($course->id);
$cache->set_versioned($course->id, $cacherev, (object)array_merge((array)$cachedvalue, array('secretfield' => 1)));
$cache->release_lock($course->id);
// 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();

View File

@ -9,6 +9,9 @@ Declaration is as follow:
$deprecatedcapabilities = [
'fake/access:fakecapability' => ['replacement' => '', 'message' => 'This capability should not be used anymore.']
];
* coursemodinfo cache uses the new `requirelockingbeforewrite` option, and rebuilding the cache now uses the cache lock API, rather
than using the core lock factory directly. This allows the locks to be stored locally if the cache is stored locally, and
avoids the risk of delays and timeouts when multiple nodes need to rebuild the cache locally, but are waiting for a central lock.
=== 4.1 ===

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2022101800.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2022101800.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.1dev+ (Build: 20221018)'; // Human-friendly version name