From 299cfee5f24ffad9d8d4ab314e3a45acafec985a Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Tue, 27 Aug 2013 16:58:58 +1000 Subject: [PATCH] MDL-41436 Move course content cache to MUC added cache definition and DB field course.cacherev for cache validation --- lang/en/cache.php | 1 + lib/db/caches.php | 6 + lib/db/install.xml | 3 +- lib/db/upgrade.php | 15 ++ lib/modinfolib.php | 421 ++++++++++++++++++++++------------ lib/moodlelib.php | 11 + lib/tests/modinfolib_test.php | 94 ++++++++ version.php | 2 +- 8 files changed, 405 insertions(+), 148 deletions(-) diff --git a/lang/en/cache.php b/lang/en/cache.php index 47403f2f8e8..2ec0c97bc4c 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -42,6 +42,7 @@ $string['cachedef_coursecat'] = 'Course categories lists for particular user'; $string['cachedef_coursecatrecords'] = 'Course categories records'; $string['cachedef_coursecontacts'] = 'List of course contacts'; $string['cachedef_coursecattree'] = 'Course categories tree'; +$string['cachedef_coursemodinfo'] = 'Accumulated information about modules and sections for each course'; $string['cachedef_databasemeta'] = 'Database meta information'; $string['cachedef_eventinvalidation'] = 'Event invalidation'; $string['cachedef_externalbadges'] = 'External badges for particular user'; diff --git a/lib/db/caches.php b/lib/db/caches.php index d03f1c364e4..305fdbca825 100644 --- a/lib/db/caches.php +++ b/lib/db/caches.php @@ -239,4 +239,10 @@ $definitions = array( 'simplekeys' => true, 'ttl' => 3600, ), + // Accumulated information about course modules and sections used to print course view page (user-independed). + // Used in function get_fast_modinfo(), reset in function rebuild_course_cache(). + 'coursemodinfo' => array( + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + ), ); diff --git a/lib/db/install.xml b/lib/db/install.xml index f97f8364d16..e2e4b58d832 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -100,6 +100,7 @@ + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 0fd05c32fe2..cb3f716e0a9 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2386,5 +2386,20 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2013090500.01); } + if ($oldversion < 2013091000.02) { + + // Define field cacherev to be added to course. + $table = new xmldb_table('course'); + $field = new xmldb_field('cacherev', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'completionnotify'); + + // Conditionally launch add field cacherev. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2013091000.02); + } + return true; } diff --git a/lib/modinfolib.php b/lib/modinfolib.php index 402cb7e8862..931748b1e92 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -54,6 +54,13 @@ if (!defined('MAX_MODINFO_CACHE_SIZE')) { * Is an array of grouping id => array of group id => group id. Includes grouping id 0 for 'all groups' */ class course_modinfo { + /** + * List of fields from DB table 'course' that are cached in MUC and are always present in course_modinfo::$course + * @var array + */ + public static $cachedfields = array('shortname', 'fullname', 'format', + 'enablecompletion', 'groupmode', 'groupmodeforce', 'cacherev'); + /** * For convenience we store the course object here as it is needed in other parts of code * @var stdClass @@ -170,7 +177,10 @@ class course_modinfo { } /** - * Returns course object that was used in the first get_fast_modinfo() call. + * Returns course object that was used in the first {@link get_fast_modinfo()} call. + * + * It may not contain all fields from DB table {course} but always has at least the following: + * id,shortname,fullname,format,enablecompletion,groupmode,groupmodeforce,cacherev * * @return stdClass */ @@ -314,24 +324,130 @@ class course_modinfo { return $this->sectioninfo[$sectionnumber]; } + /** + * Static cache for generated course_modinfo instances + * + * @see course_modinfo::instance() + * @see course_modinfo::clear_instance_cache() + * @var course_modinfo[] + */ + protected static $instancecache = array(); + + /** + * Timestamps (microtime) when the course_modinfo instances were last accessed + * + * It is used to remove the least recent accessed instances when static cache is full + * + * @var float[] + */ + protected static $cacheaccessed = array(); + + /** + * Clears the cache used in course_modinfo::instance() + * + * Used in {@link get_fast_modinfo()} when called with argument $reset = true + * and in {@link rebuild_course_cache()} + * + * @param null|int|stdClass $courseorid if specified removes only cached value for this course + */ + public static function clear_instance_cache($courseorid = null) { + if (empty($courseorid)) { + self::$instancecache = array(); + self::$cacheaccessed = array(); + return; + } + if (is_object($courseorid)) { + $courseorid = $courseorid->id; + } + if (isset(self::$instancecache[$courseorid])) { + // Unsetting static variable in PHP is peculiar, it removes the reference, + // but data remain in memory. Prior to unsetting, the varable needs to be + // set to empty to remove its remains from memory. + self::$instancecache[$courseorid] = ''; + unset(self::$instancecache[$courseorid]); + unset(self::$cacheaccessed[$courseorid]); + } + } + + /** + * Returns the instance of course_modinfo for the specified course and specified user + * + * This function uses static cache for the retrieved instances. The cache + * size is limited by MAX_MODINFO_CACHE_SIZE. If instance is not found in + * the static cache or it was created for another user or the cacherev validation + * failed - a new instance is constructed and returned. + * + * Used in {@link get_fast_modinfo()} + * + * @param int|stdClass $courseorid object from DB table 'course' (must have field 'id' + * and recommended to have field 'cacherev') or just a course id + * @param int $userid User id to populate 'availble' and 'uservisible' attributes of modules and sections. + * Set to 0 for current user (default). Set to -1 to avoid calculation of dynamic user-depended data. + * @return course_modinfo + */ + public static function instance($courseorid, $userid = 0) { + global $USER; + if (is_object($courseorid)) { + $course = $courseorid; + } else { + $course = (object)array('id' => $courseorid); + } + if (empty($userid)) { + $userid = $USER->id; + } + + if (!empty(self::$instancecache[$course->id])) { + if (self::$instancecache[$course->id]->userid == $userid && + (!isset($course->cacherev) || + $course->cacherev == self::$instancecache[$course->id]->get_course()->cacherev)) { + // This course's modinfo for the same user was recently retrieved, return cached. + self::$cacheaccessed[$course->id] = microtime(true); + return self::$instancecache[$course->id]; + } else { + // Prevent potential reference problems when switching users. + self::clear_instance_cache($course->id); + } + } + $modinfo = new course_modinfo($course, $userid); + + // We have a limit of MAX_MODINFO_CACHE_SIZE entries to store in static variable. + if (count(self::$instancecache) >= MAX_MODINFO_CACHE_SIZE) { + // Find the course that was the least recently accessed. + asort(self::$cacheaccessed, SORT_NUMERIC); + $courseidtoremove = key(array_reverse(self::$cacheaccessed, true)); + self::clear_instance_cache($courseidtoremove); + } + + // Add modinfo to the static cache. + self::$instancecache[$course->id] = $modinfo; + self::$cacheaccessed[$course->id] = microtime(true); + + return $modinfo; + } + /** * Constructs based on course. * Note: This constructor should not usually be called directly. * Use get_fast_modinfo($course) instead as this maintains a cache. - * @param object $course Moodle course object, which may include modinfo + * @param stdClass $course course object, only property id is required. * @param int $userid User ID + * @throws moodle_exception if course is not found */ public function __construct($course, $userid) { - global $CFG, $DB, $COURSE, $SITE; + global $CFG, $COURSE, $SITE, $DB; - if (!isset($course->modinfo) || !isset($course->sectioncache)) { + if (!isset($course->cacherev)) { + // We require presence of property cacherev to validate the course cache. + // No need to clone the $COURSE or $SITE object here because we clone it below anyway. $course = get_course($course->id, false); } - // Check modinfo field is set. If not, build and load it. - if (empty($course->modinfo) || empty($course->sectioncache)) { - rebuild_course_cache($course->id); - $course = $DB->get_record('course', array('id'=>$course->id), '*', MUST_EXIST); + $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 = self::build_course_cache($course); } // Set initial values @@ -340,35 +456,6 @@ class course_modinfo { $this->cms = array(); $this->instances = array(); $this->groups = null; - $this->course = $course; - - // Load modinfo field into memory as PHP object and check it's valid - $info = unserialize($course->modinfo); - if (!is_array($info)) { - // hmm, something is wrong - lets try to fix it - rebuild_course_cache($course->id); - $course->modinfo = $DB->get_field('course', 'modinfo', array('id'=>$course->id)); - $info = unserialize($course->modinfo); - if (!is_array($info)) { - // If it still fails, abort - debugging('Problem with "modinfo" data for this course'); - return; - } - } - - // Load sectioncache field into memory as PHP object and check it's valid - $sectioncache = unserialize($course->sectioncache); - if (!is_array($sectioncache)) { - // hmm, something is wrong - let's fix it - rebuild_course_cache($course->id); - $course->sectioncache = $DB->get_field('course', 'sectioncache', array('id'=>$course->id)); - $sectioncache = unserialize($course->sectioncache); - if (!is_array($sectioncache)) { - // If it still fails, abort - debugging('Problem with "sectioncache" data for this course'); - return; - } - } // If we haven't already preloaded contexts for the course, do it now // Modules are also cached here as long as it's the first time this course has been preloaded. @@ -380,22 +467,30 @@ class course_modinfo { if ($course->id == $COURSE->id || $course->id == $SITE->id) { // Only verify current course (or frontpage) as pages with many courses may not have module contexts cached. // (Uncached modules will result in a very slow verification). - foreach ($info as $mod) { + foreach ($coursemodinfo->modinfo as $mod) { if (!context_module::instance($mod->cm, IGNORE_MISSING)) { debugging('Course cache integrity check failed: course module with id '. $mod->cm. ' does not have context. Rebuilding cache for course '. $course->id); - rebuild_course_cache($course->id); - $this->course = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST); - $info = unserialize($this->course->modinfo); - $sectioncache = unserialize($this->course->sectioncache); + // Re-request the course record from DB as well, don't use get_course() here. + $course = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST); + $coursemodinfo = self::build_course_cache($course); break; } } } + // Overwrite unset fields in $course object with cached values, store the course object. + $this->course = fullclone($course); + foreach ($coursemodinfo as $key => $value) { + if ($key !== 'modinfo' && $key !== 'sectioncache' && + (!isset($this->course->$key) || $key === 'cacherev')) { + $this->course->$key = $value; + } + } + // Loop through each piece of module data, constructing it static $modexists = array(); - foreach ($info as $mod) { + foreach ($coursemodinfo->modinfo as $mod) { if (empty($mod->name)) { // something is wrong here continue; @@ -428,7 +523,7 @@ class course_modinfo { // Expand section objects $this->sectioninfo = array(); - foreach ($sectioncache as $number => $data) { + foreach ($coursemodinfo->sectioncache as $number => $data) { $this->sectioninfo[$number] = new section_info($data, $number, null, null, $this, null); } @@ -439,26 +534,44 @@ class course_modinfo { * the course cache. (Does not include information that is already cached * in some other way.) * - * Used internally by rebuild_course_cache function; do not use otherwise. + * This function will be removed in 2.7 + * + * @deprecated since 2.6 * @param int $courseid Course ID * @return array Information about sections, indexed by section number (not id) */ public static function build_section_cache($courseid) { global $DB; + debugging('Function course_modinfo::build_section_cache() is deprecated. It can only be used internally to build course cache.'); + $course = $DB->get_record('course', array('id' => $course->id), + array_merge(array('id'), self::$cachedfields), MUST_EXIST); + return self::build_course_section_cache($course); + } + + /** + * Builds a list of information about sections on a course to be stored in + * the course cache. (Does not include information that is already cached + * in some other way.) + * + * @param stdClass $course Course object (must contain fields + * @return array Information about sections, indexed by section number (not id) + */ + protected static function build_course_section_cache($course) { + global $DB; // Get section data - $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section', + $sections = $DB->get_records('course_sections', array('course' => $course->id), 'section', 'section, id, course, name, summary, summaryformat, sequence, visible, ' . 'availablefrom, availableuntil, showavailability, groupingid'); $compressedsections = array(); - $formatoptionsdef = course_get_format($courseid)->section_format_options(); + $formatoptionsdef = course_get_format($course)->section_format_options(); // Remove unnecessary data and add availability foreach ($sections as $number => $section) { // Add cached options from course format to $section object foreach ($formatoptionsdef as $key => $option) { if (!empty($option['cache'])) { - $formatoptions = course_get_format($courseid)->get_format_options($section); + $formatoptions = course_get_format($course)->get_format_options($section); if (!array_key_exists('cachedefault', $option) || $option['cachedefault'] !== $formatoptions[$key]) { $section->$key = $formatoptions[$key]; } @@ -471,6 +584,46 @@ class course_modinfo { return $compressedsections; } + + /** + * Builds and stores in MUC object containing information about course + * modules and sections together with cached fields from table course. + * + * @param stdClass $course object from DB table course. Must have property 'id' + * but preferably should have all cached fields. + * @return stdClass object with all cached keys of the course plus fields modinfo and sectioncache. + * The same object is stored in MUC + * @throws moodle_exception if course is not found (if $course object misses some of the + * necessary fields it is re-requested from database) + */ + public static function build_course_cache($course) { + global $DB, $CFG; + require_once("$CFG->dirroot/course/lib.php"); + if (empty($course->id)) { + throw new coding_exception('Object $course is missing required property \id\''); + } + // Ensure object has all necessary fields. + foreach (self::$cachedfields as $key) { + if (!isset($course->$key)) { + $course = $DB->get_record('course', array('id' => $course->id), + array_merge(array('id'), self::$cachedfields), MUST_EXIST); + break; + } + } + // 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); + foreach (self::$cachedfields as $key) { + $coursemodinfo->$key = $course->$key; + } + // Set the accumulated activities and sections information in cache, together with cacherev. + $cachecoursemodinfo = cache::make('core', 'coursemodinfo'); + $cachecoursemodinfo->set($course->id, $coursemodinfo); + return $coursemodinfo; + } } @@ -1282,7 +1435,16 @@ class cm_info implements IteratorAggregate { } /** - * @return object Moodle course object that was used to construct this data + * Returns course object that was used in the first {@link get_fast_modinfo()} call. + * + * It may not contain all fields from DB table {course} but always has at least the following: + * id,shortname,fullname,format,enablecompletion,groupmode,groupmodeforce,cacherev + * + * If the course object lacks the field you need you can use the global + * function {@link get_course()} that will save extra query if you access + * current course or frontpage course. + * + * @return stdClass */ public function get_course() { return $this->modinfo->get_course(); @@ -1509,9 +1671,6 @@ class cm_info implements IteratorAggregate { $this->showdescription = isset($mod->showdescription) ? $mod->showdescription : 0; $this->state = self::STATE_BASIC; - // Note: These fields from $cm were not present in cm_info in Moodle - // 2.0.2 and prior. They may not be available if course cache hasn't - // been rebuilt since then. $this->section = isset($mod->sectionid) ? $mod->sectionid : 0; $this->module = isset($mod->module) ? $mod->module : 0; $this->added = isset($mod->added) ? $mod->added : 0; @@ -1797,18 +1956,20 @@ class cm_info implements IteratorAggregate { * use get_fast_modinfo($courseid, 0, true) to reset the static cache for particular course * use get_fast_modinfo(0, 0, true) to reset the static cache for all courses * - * @uses MAX_MODINFO_CACHE_SIZE - * @param int|stdClass $courseorid object from DB table 'course' or just a course id + * use rebuild_course_cache($courseid, true) to reset the application AND static cache + * for particular course when it's contents has changed + * + * @param int|stdClass $courseorid object from DB table 'course' (must have field 'id' + * and recommended to have field 'cacherev') or just a course id. Just course id + * is enough when calling get_fast_modinfo() for current course or site or when + * calling for any other course for the second time. * @param int $userid User id to populate 'uservisible' attributes of modules and sections. * Set to 0 for current user (default) * @param bool $resetonly whether we want to get modinfo or just reset the cache * @return course_modinfo|null Module information for course, or null if resetting + * @throws moodle_exception when course is not found (nothing is thrown if resetting) */ function get_fast_modinfo($courseorid, $userid = 0, $resetonly = false) { - global $CFG, $USER; - - static $cache = array(); - // compartibility with syntax prior to 2.4: if ($courseorid === 'reset') { debugging("Using the string 'reset' as the first argument of get_fast_modinfo() is deprecated. Use get_fast_modinfo(0,0,true) instead.", DEBUG_DEVELOPER); @@ -1821,62 +1982,34 @@ function get_fast_modinfo($courseorid, $userid = 0, $resetonly = false) { upgrade_ensure_not_running(); } - if (is_object($courseorid)) { - $course = $courseorid; - } else { - $course = (object)array('id' => $courseorid); - } - // Function is called with $reset = true if ($resetonly) { - if (isset($course->id) && $course->id > 0) { - $cache[$course->id] = false; - } else { - foreach (array_keys($cache) as $key) { - $cache[$key] = false; - } - } + course_modinfo::clear_instance_cache($courseorid); return null; } // Function is called with $reset = false, retrieve modinfo - if (empty($userid)) { - $userid = $USER->id; - } - - if (array_key_exists($course->id, $cache)) { - if ($cache[$course->id] === false) { - // this course has been recently reset, do not rely on modinfo and sectioncache in $course - $course->modinfo = null; - $course->sectioncache = null; - } else if ($cache[$course->id]->userid == $userid) { - // this course's modinfo for the same user was recently retrieved, return cached - return $cache[$course->id]; - } - } - - unset($cache[$course->id]); // prevent potential reference problems when switching users - - $cache[$course->id] = new course_modinfo($course, $userid); - - // Ensure cache does not use too much RAM - if (count($cache) > MAX_MODINFO_CACHE_SIZE) { - reset($cache); - $key = key($cache); - // Unsetting static variable in PHP is percular, it removes the reference, - // but data remain in memory. Prior to unsetting, the varable needs to be - // set to empty to remove its remains from memory. - $cache[$key] = ''; - unset($cache[$key]); - } - - return $cache[$course->id]; + return course_modinfo::instance($courseorid, $userid); } /** - * Rebuilds the cached list of course activities stored in the database - * @param int $courseid - id of course to rebuild, empty means all - * @param boolean $clearonly - only clear the modinfo fields, gets rebuild automatically on the fly + * Rebuilds or resets the cached list of course activities stored in MUC. + * + * rebuild_course_cache() must NEVER be called from lib/db/upgrade.php. + * At the same time course cache may ONLY be cleared using this function in + * upgrade scripts of plugins. + * + * During the bulk operations if it is necessary to reset cache of multiple + * courses it is enough to call {@link increment_revision_number()} for the + * table 'course' and field 'cacherev' specifying affected courses in select. + * + * Cached course information is stored in MUC core/coursemodinfo and is + * validated with the DB field {course}.cacherev + * + * @global moodle_database $DB + * @param int $courseid id of course to rebuild, empty means all + * @param boolean $clearonly only clear the cache, gets rebuild automatically on the fly. + * Recommended to set to true to avoid unnecessary multiple rebuilding. */ function rebuild_course_cache($courseid=0, $clearonly=false) { global $COURSE, $SITE, $DB, $CFG; @@ -1894,29 +2027,40 @@ function rebuild_course_cache($courseid=0, $clearonly=false) { format_base::reset_course_cache($courseid); } - if ($clearonly) { - if (empty($courseid)) { - $DB->execute('UPDATE {course} set modinfo = ?, sectioncache = ?', array(null, null)); + $cachecoursemodinfo = cache::make('core', 'coursemodinfo'); + if (empty($courseid)) { + // Clearing caches for all courses. + increment_revision_number('course', 'cacherev', ''); + $cachecoursemodinfo->purge(); + course_modinfo::clear_instance_cache(); + // Update global values too. + $sitecacherev = $DB->get_field('course', 'cacherev', array('id' => SITEID)); + $SITE->cachrev = $sitecacherev; + if ($COURSE->id == SITEID) { + $COURSE->cacherev = $sitecacherev; } else { - // Clear both fields in one update - $resetobj = (object)array('id' => $courseid, 'modinfo' => null, 'sectioncache' => null); - $DB->update_record('course', $resetobj); + $COURSE->cacherev = $DB->get_field('course', 'cacherev', array('id' => $COURSE->id)); } - // update cached global COURSE too ;-) - if ($courseid == $COURSE->id or empty($courseid)) { - $COURSE->modinfo = null; - $COURSE->sectioncache = null; + } else { + // Clearing cache for one course, make sure it is deleted from user request cache as well. + increment_revision_number('course', 'cacherev', 'id = :id', array('id' => $courseid)); + $cachecoursemodinfo->delete($courseid); + course_modinfo::clear_instance_cache($courseid); + // Update global values too. + if ($courseid == $COURSE->id || $courseid == $SITE->id) { + $cacherev = $DB->get_field('course', 'cacherev', array('id' => $courseid)); + if ($courseid == $COURSE->id) { + $COURSE->cacherev = $cacherev; + } + if ($courseid == $SITE->id) { + $SITE->cachrev = $cacherev; + } } - if ($courseid == $SITE->id) { - $SITE->modinfo = null; - $SITE->sectioncache = null; - } - // reset the fast modinfo cache - get_fast_modinfo($courseid, 0, true); - return; } - require_once("$CFG->dirroot/course/lib.php"); + if ($clearonly) { + return; + } if ($courseid) { $select = array('id'=>$courseid); @@ -1925,26 +2069,12 @@ function rebuild_course_cache($courseid=0, $clearonly=false) { @set_time_limit(0); // this could take a while! MDL-10954 } - $rs = $DB->get_recordset("course", $select,'','id,fullname'); + $rs = $DB->get_recordset("course", $select,'','id,'.join(',', course_modinfo::$cachedfields)); + // Rebuild cache for each course. foreach ($rs as $course) { - $modinfo = serialize(get_array_of_activities($course->id)); - $sectioncache = serialize(course_modinfo::build_section_cache($course->id)); - $updateobj = (object)array('id' => $course->id, - 'modinfo' => $modinfo, 'sectioncache' => $sectioncache); - $DB->update_record("course", $updateobj); - // update cached global COURSE too ;-) - if ($course->id == $COURSE->id) { - $COURSE->modinfo = $modinfo; - $COURSE->sectioncache = $sectioncache; - } - if ($course->id == $SITE->id) { - $SITE->modinfo = $modinfo; - $SITE->sectioncache = $sectioncache; - } + course_modinfo::build_course_cache($course); } $rs->close(); - // reset the fast modinfo cache - get_fast_modinfo($courseid, 0, true); } @@ -2243,12 +2373,11 @@ class section_info implements IteratorAggregate { // Cached course format data. $course = $modinfo->get_course(); - if (!isset($course->format) || !isset(self::$sectionformatoptions[$course->format])) { - $courseformat = course_get_format(isset($course->format) ? $course : $course->id); - if (!isset($course->format)) { - $course->format = $courseformat->get_format(); - } - self::$sectionformatoptions[$course->format] = $courseformat->section_format_options(); + if (!isset(self::$sectionformatoptions[$course->format])) { + // Store list of section format options defined in each used course format. + // They do not depend on particular course but only on its format. + self::$sectionformatoptions[$course->format] = + course_get_format($course)->section_format_options(); } foreach (self::$sectionformatoptions[$course->format] as $field => $option) { if (!empty($option['cache'])) { diff --git a/lib/moodlelib.php b/lib/moodlelib.php index ff068d4a46c..2183c21a61c 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -1590,6 +1590,13 @@ function purge_all_caches() { get_string_manager()->reset_caches(); core_text::reset_caches(); + // Bump up cacherev field for all courses. + try { + increment_revision_number('course', 'cacherev', ''); + } catch (moodle_exception $e) { + // Ignore exception since this function is also called before upgrade script when field course.cacherev does not exist yet. + } + cache_helper::purge_all(); // Purge all other caches: rss, simplepie, etc. @@ -5073,6 +5080,10 @@ function remove_course_contents($courseid, $showfeedback = true, array $options // also some non-standard unsupported plugins may try to store something there. fulldelete($CFG->dataroot.'/'.$course->id); + // Delete from cache to reduce the cache size especially makes sense in case of bulk course deletion. + $cachemodinfo = cache::make('core', 'coursemodinfo'); + $cachemodinfo->delete($courseid); + // Trigger a course content deleted event. $event = \core\event\course_content_deleted::create(array( 'objectid' => $course->id, diff --git a/lib/tests/modinfolib_test.php b/lib/tests/modinfolib_test.php index 92ac1221f53..6b2fc6e9c30 100644 --- a/lib/tests/modinfolib_test.php +++ b/lib/tests/modinfolib_test.php @@ -267,6 +267,100 @@ class core_modinfolib_testcase extends advanced_testcase { set_config('enablecompletion', $oldcfgenablecompletion); } + public function test_matching_cacherev() { + global $DB, $CFG; + + $this->resetAfterTest(); + $this->setAdminUser(); + $cache = cache::make('core', 'coursemodinfo'); + + // Generate the course and pre-requisite module. + $course = $this->getDataGenerator()->create_course( + array('format' => 'topics', + 'numsections' => 3), + array('createsections' => true)); + + // Make sure the cacherev is set. + $cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id)); + $this->assertGreaterThan(0, $cacherev); + $prevcacherev = $cacherev; + + // Reset course cache and make sure cacherev is bumped up but cache is empty. + 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)); + $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); + $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))); + + // 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); + $this->assertNotEmpty($cachedvalue); + $this->assertEquals($cacherev, $cachedvalue->cacherev); + $this->assertNotEmpty($cachedvalue->secretfield); + $this->assertEquals($cacherev, $modinfo->get_course()->cacherev); + $prevcacherev = $cacherev; + + // Rebuild course cache. Cacherev must be incremented everywhere. + rebuild_course_cache($course->id); + $cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id)); + $this->assertGreaterThan($prevcacherev, $cacherev); + $cachedvalue = $cache->get($course->id); + $this->assertNotEmpty($cachedvalue); + $this->assertEquals($cacherev, $cachedvalue->cacherev); + $modinfo = get_fast_modinfo($course->id); + $this->assertEquals($cacherev, $modinfo->get_course()->cacherev); + $prevcacherev = $cacherev; + + // Update cacherev in DB and make sure the cache will be rebuilt on the next call to get_fast_modinfo(). + increment_revision_number('course', 'cacherev', 'id = ?', array($course->id)); + // We need to clear static cache for course_modinfo instances too. + course_modinfo::clear_instance_cache(); + $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); + $this->assertNotEmpty($cachedvalue); + $this->assertEquals($cacherev, $cachedvalue->cacherev); + $this->assertEquals($cacherev, $modinfo->get_course()->cacherev); + $prevcacherev = $cacherev; + + // Reset cache for all courses and make sure this course cache is reset. + 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)); + // Rebuild again. + $modinfo = get_fast_modinfo($course->id); + $cachedvalue = $cache->get($course->id); + $this->assertNotEmpty($cachedvalue); + $this->assertEquals($cacherev, $cachedvalue->cacherev); + $this->assertEquals($cacherev, $modinfo->get_course()->cacherev); + $prevcacherev = $cacherev; + + // Purge all caches and make sure cacherev is increased and data from MUC erased. + purge_all_caches(); + $cacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id)); + $this->assertGreaterThan($prevcacherev, $cacherev); + $this->assertEmpty($cache->get($course->id)); + } + public function test_course_modinfo_properties() { global $USER, $DB; diff --git a/version.php b/version.php index 3ed09528b96..86279535266 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2013090901.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2013091000.02; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.