MDL-55231 core_course: Partial course cache rebuild

This commit is contained in:
Dongsheng Cai 2021-07-16 13:49:35 +10:00
parent e5894c0455
commit 9a900492bc
15 changed files with 406 additions and 177 deletions

View file

@ -614,8 +614,10 @@ class dndupload_ajax_processor {
$visible = get_fast_modinfo($this->course)->get_section_info($this->section)->visible;
$DB->set_field('course_modules', 'instance', $instanceid, array('id' => $this->cm->id));
course_purge_module_cache($this->cm);
// Rebuild the course cache after update action
rebuild_course_cache($this->course->id, true);
rebuild_course_cache($this->course->id, true, true);
$sectionid = course_add_cm_to_section($this->course, $this->cm->id, $this->section);

View file

@ -1009,7 +1009,15 @@ abstract class base {
}
}
if ($needrebuild) {
rebuild_course_cache($this->courseid, true);
if ($sectionid) {
$sectionrecord = $DB->get_record('course_sections', ['id' => $sectionid], 'course, section', MUST_EXIST);
course_purge_section_cache($sectionrecord);
// Partial rebuild sections that have been invalidated.
rebuild_course_cache($this->courseid, true, true);
} else {
// Full rebuild if sectionid is null.
rebuild_course_cache($this->courseid);
}
}
if ($changed) {
// Reset internal caches.
@ -1277,7 +1285,7 @@ abstract class base {
}
if (!is_object($section)) {
$section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section),
'id,section,sequence,summary');
'id,course,section,sequence,summary');
}
if (!$section || !$section->section) {
// Not possible to delete 0-section.
@ -1314,7 +1322,9 @@ abstract class base {
// Delete section and it's format options.
$DB->delete_records('course_format_options', array('sectionid' => $section->id));
$DB->delete_records('course_sections', array('id' => $section->id));
rebuild_course_cache($course->id, true);
course_purge_section_cache($section);
// Partial rebuild section cache that has been purged.
rebuild_course_cache($course->id, true, true);
// Delete section summary files.
$context = \context_course::instance($course->id);

View file

@ -389,8 +389,12 @@ function course_integrity_check($courseid, $rawmods = null, $sections = null, $f
/**
* For a given course, returns an array of course activity objects
* Each item in the array contains he following properties:
*
* @param int $courseid course id
* @param bool $usecache get activities from cache if modinfo exists when $usecache is true
* @return array list of activities
*/
function get_array_of_activities($courseid) {
function get_array_of_activities(int $courseid, bool $usecache = false): array {
// cm - course module id
// mod - name of the module (eg forum)
// section - the number of the section (eg week or topic)
@ -406,12 +410,21 @@ function get_array_of_activities($courseid) {
throw new moodle_exception('courseidnotfound');
}
$mod = array();
$rawmods = get_course_mods($courseid);
if (empty($rawmods)) {
return $mod; // always return array
return [];
}
$mods = [];
if ($usecache) {
// Get existing cache.
$cachecoursemodinfo = cache::make('core', 'coursemodinfo');
$coursemodinfo = $cachecoursemodinfo->get($courseid);
if ($coursemodinfo !== false) {
$mods = $coursemodinfo->modinfo;
}
}
$courseformat = course_get_format($course);
if ($sections = $DB->get_records('course_sections', array('course' => $courseid),
@ -423,140 +436,152 @@ function get_array_of_activities($courseid) {
$sections = $DB->get_records('course_sections', array('course' => $courseid),
'section ASC', 'id,section,sequence,visible');
}
// Build array of activities.
foreach ($sections as $section) {
if (!empty($section->sequence)) {
$sequence = explode(",", $section->sequence);
foreach ($sequence as $seq) {
if (empty($rawmods[$seq])) {
continue;
}
// Adjust visibleoncoursepage, value in DB may not respect format availability.
$rawmods[$seq]->visibleoncoursepage = (!$rawmods[$seq]->visible
|| $rawmods[$seq]->visibleoncoursepage
|| empty($CFG->allowstealth)
|| !$courseformat->allow_stealth_module_visibility($rawmods[$seq], $section)) ? 1 : 0;
// Create an object that will be cached.
$mod[$seq] = new stdClass();
$mod[$seq]->id = $rawmods[$seq]->instance;
$mod[$seq]->cm = $rawmods[$seq]->id;
$mod[$seq]->mod = $rawmods[$seq]->modname;
// Build array of activities.
foreach ($sections as $section) {
if (!empty($section->sequence)) {
$cmids = explode(",", $section->sequence);
$numberofmods = count($cmids);
for ($order = 0; $order < $numberofmods; $order++) {
$cmid = $cmids[$order];
// Activity not exist in database.
$notexistindb = empty($rawmods[$cmid]);
$activitycached = isset($mods[$cmid]);
if ($activitycached || $notexistindb) {
continue;
}
$modposition = ($order === 0) ? 0 : array_search($cmids[$order - 1], array_keys($mods)) + 1;
$mods = array_slice($mods, 0, $modposition, true)
+ [$cmid => new stdClass()]
+ array_slice($mods, $modposition, null, true);
// Adjust visibleoncoursepage, value in DB may not respect format availability.
$rawmods[$cmid]->visibleoncoursepage = (!$rawmods[$cmid]->visible
|| $rawmods[$cmid]->visibleoncoursepage
|| empty($CFG->allowstealth)
|| !$courseformat->allow_stealth_module_visibility($rawmods[$cmid], $section)) ? 1 : 0;
$mods[$cmid]->id = $rawmods[$cmid]->instance;
$mods[$cmid]->cm = $rawmods[$cmid]->id;
$mods[$cmid]->mod = $rawmods[$cmid]->modname;
// Oh dear. Inconsistent names left here for backward compatibility.
$mod[$seq]->section = $section->section;
$mod[$seq]->sectionid = $rawmods[$seq]->section;
$mods[$cmid]->section = $section->section;
$mods[$cmid]->sectionid = $rawmods[$cmid]->section;
$mod[$seq]->module = $rawmods[$seq]->module;
$mod[$seq]->added = $rawmods[$seq]->added;
$mod[$seq]->score = $rawmods[$seq]->score;
$mod[$seq]->idnumber = $rawmods[$seq]->idnumber;
$mod[$seq]->visible = $rawmods[$seq]->visible;
$mod[$seq]->visibleoncoursepage = $rawmods[$seq]->visibleoncoursepage;
$mod[$seq]->visibleold = $rawmods[$seq]->visibleold;
$mod[$seq]->groupmode = $rawmods[$seq]->groupmode;
$mod[$seq]->groupingid = $rawmods[$seq]->groupingid;
$mod[$seq]->indent = $rawmods[$seq]->indent;
$mod[$seq]->completion = $rawmods[$seq]->completion;
$mod[$seq]->extra = "";
$mod[$seq]->completiongradeitemnumber =
$rawmods[$seq]->completiongradeitemnumber;
$mod[$seq]->completionpassgrade = $rawmods[$seq]->completionpassgrade;
$mod[$seq]->completionview = $rawmods[$seq]->completionview;
$mod[$seq]->completionexpected = $rawmods[$seq]->completionexpected;
$mod[$seq]->showdescription = $rawmods[$seq]->showdescription;
$mod[$seq]->availability = $rawmods[$seq]->availability;
$mod[$seq]->deletioninprogress = $rawmods[$seq]->deletioninprogress;
$mods[$cmid]->module = $rawmods[$cmid]->module;
$mods[$cmid]->added = $rawmods[$cmid]->added;
$mods[$cmid]->score = $rawmods[$cmid]->score;
$mods[$cmid]->idnumber = $rawmods[$cmid]->idnumber;
$mods[$cmid]->visible = $rawmods[$cmid]->visible;
$mods[$cmid]->visibleoncoursepage = $rawmods[$cmid]->visibleoncoursepage;
$mods[$cmid]->visibleold = $rawmods[$cmid]->visibleold;
$mods[$cmid]->groupmode = $rawmods[$cmid]->groupmode;
$mods[$cmid]->groupingid = $rawmods[$cmid]->groupingid;
$mods[$cmid]->indent = $rawmods[$cmid]->indent;
$mods[$cmid]->completion = $rawmods[$cmid]->completion;
$mods[$cmid]->extra = "";
$mods[$cmid]->completiongradeitemnumber =
$rawmods[$cmid]->completiongradeitemnumber;
$mods[$cmid]->completionpassgrade = $rawmods[$cmid]->completionpassgrade;
$mods[$cmid]->completionview = $rawmods[$cmid]->completionview;
$mods[$cmid]->completionexpected = $rawmods[$cmid]->completionexpected;
$mods[$cmid]->showdescription = $rawmods[$cmid]->showdescription;
$mods[$cmid]->availability = $rawmods[$cmid]->availability;
$mods[$cmid]->deletioninprogress = $rawmods[$cmid]->deletioninprogress;
$modname = $mod[$seq]->mod;
$functionname = $modname."_get_coursemodule_info";
$modname = $mods[$cmid]->mod;
$functionname = $modname . "_get_coursemodule_info";
if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {
continue;
}
if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {
continue;
}
include_once("$CFG->dirroot/mod/$modname/lib.php");
include_once("$CFG->dirroot/mod/$modname/lib.php");
if ($hasfunction = function_exists($functionname)) {
if ($info = $functionname($rawmods[$seq])) {
if (!empty($info->icon)) {
$mod[$seq]->icon = $info->icon;
}
if (!empty($info->iconcomponent)) {
$mod[$seq]->iconcomponent = $info->iconcomponent;
}
if (!empty($info->name)) {
$mod[$seq]->name = $info->name;
}
if ($info instanceof cached_cm_info) {
// When using cached_cm_info you can include three new fields
// that aren't available for legacy code
if (!empty($info->content)) {
$mod[$seq]->content = $info->content;
}
if (!empty($info->extraclasses)) {
$mod[$seq]->extraclasses = $info->extraclasses;
}
if (!empty($info->iconurl)) {
// Convert URL to string as it's easier to store. Also serialized object contains \0 byte and can not be written to Postgres DB.
$url = new moodle_url($info->iconurl);
$mod[$seq]->iconurl = $url->out(false);
}
if (!empty($info->onclick)) {
$mod[$seq]->onclick = $info->onclick;
}
if (!empty($info->customdata)) {
$mod[$seq]->customdata = $info->customdata;
}
} else {
// When using a stdclass, the (horrible) deprecated ->extra field
// is available for BC
if (!empty($info->extra)) {
$mod[$seq]->extra = $info->extra;
}
}
}
}
// When there is no modname_get_coursemodule_info function,
// but showdescriptions is enabled, then we use the 'intro'
// and 'introformat' fields in the module table
if (!$hasfunction && $rawmods[$seq]->showdescription) {
if ($modvalues = $DB->get_record($rawmods[$seq]->modname,
array('id' => $rawmods[$seq]->instance), 'name, intro, introformat')) {
// Set content from intro and introformat. Filters are disabled
// because we filter it with format_text at display time
$mod[$seq]->content = format_module_intro($rawmods[$seq]->modname,
$modvalues, $rawmods[$seq]->id, false);
if ($hasfunction = function_exists($functionname)) {
if ($info = $functionname($rawmods[$cmid])) {
if (!empty($info->icon)) {
$mods[$cmid]->icon = $info->icon;
}
if (!empty($info->iconcomponent)) {
$mods[$cmid]->iconcomponent = $info->iconcomponent;
}
if (!empty($info->name)) {
$mods[$cmid]->name = $info->name;
}
if ($info instanceof cached_cm_info) {
// When using cached_cm_info you can include three new fields.
// That aren't available for legacy code.
if (!empty($info->content)) {
$mods[$cmid]->content = $info->content;
}
if (!empty($info->extraclasses)) {
$mods[$cmid]->extraclasses = $info->extraclasses;
}
if (!empty($info->iconurl)) {
// Convert URL to string as it's easier to store.
// Also serialized object contains \0 byte,
// ... and can not be written to Postgres DB.
$url = new moodle_url($info->iconurl);
$mods[$cmid]->iconurl = $url->out(false);
}
if (!empty($info->onclick)) {
$mods[$cmid]->onclick = $info->onclick;
}
if (!empty($info->customdata)) {
$mods[$cmid]->customdata = $info->customdata;
}
} else {
// When using a stdclass, the (horrible) deprecated ->extra field,
// ... that is available for BC.
if (!empty($info->extra)) {
$mods[$cmid]->extra = $info->extra;
}
}
}
}
// When there is no modname_get_coursemodule_info function,
// ... but showdescriptions is enabled, then we use the 'intro',
// ... and 'introformat' fields in the module table.
if (!$hasfunction && $rawmods[$cmid]->showdescription) {
if ($modvalues = $DB->get_record($rawmods[$cmid]->modname,
['id' => $rawmods[$cmid]->instance], 'name, intro, introformat')) {
// Set content from intro and introformat. Filters are disabled.
// Because we filter it with format_text at display time.
$mods[$cmid]->content = format_module_intro($rawmods[$cmid]->modname,
$modvalues, $rawmods[$cmid]->id, false);
// To save making another query just below, put name in here
$mod[$seq]->name = $modvalues->name;
}
}
if (!isset($mod[$seq]->name)) {
$mod[$seq]->name = $DB->get_field($rawmods[$seq]->modname, "name", array("id"=>$rawmods[$seq]->instance));
}
// To save making another query just below, put name in here.
$mods[$cmid]->name = $modvalues->name;
}
}
if (!isset($mods[$cmid]->name)) {
$mods[$cmid]->name = $DB->get_field($rawmods[$cmid]->modname, "name",
["id" => $rawmods[$cmid]->instance]);
}
// Minimise the database size by unsetting default options when they are
// 'empty'. This list corresponds to code in the cm_info constructor.
foreach (array('idnumber', 'groupmode', 'groupingid',
// Minimise the database size by unsetting default options when they are 'empty'.
// This list corresponds to code in the cm_info constructor.
foreach (['idnumber', 'groupmode', 'groupingid',
'indent', 'completion', 'extra', 'extraclasses', 'iconurl', 'onclick', 'content',
'icon', 'iconcomponent', 'customdata', 'availability', 'completionview',
'completionexpected', 'score', 'showdescription', 'deletioninprogress') as $property) {
if (property_exists($mod[$seq], $property) &&
empty($mod[$seq]->{$property})) {
unset($mod[$seq]->{$property});
}
}
// Special case: this value is usually set to null, but may be 0
if (property_exists($mod[$seq], 'completiongradeitemnumber') &&
is_null($mod[$seq]->completiongradeitemnumber)) {
unset($mod[$seq]->completiongradeitemnumber);
}
}
'completionexpected', 'score', 'showdescription', 'deletioninprogress'] as $property) {
if (property_exists($mods[$cmid], $property) &&
empty($mods[$cmid]->{$property})) {
unset($mods[$cmid]->{$property});
}
}
// Special case: this value is usually set to null, but may be 0.
if (property_exists($mods[$cmid], 'completiongradeitemnumber') &&
is_null($mods[$cmid]->completiongradeitemnumber)) {
unset($mods[$cmid]->completiongradeitemnumber);
}
}
}
}
}
return $mod;
return $mods;
}
/**
@ -814,11 +839,7 @@ function course_add_cm_to_section($courseorid, $cmid, $sectionnum, $beforemod =
}
$DB->set_field("course_sections", "sequence", $newsequence, array("id" => $section->id));
$DB->set_field('course_modules', 'section', $section->id, array('id' => $cmid));
if (is_object($courseorid)) {
rebuild_course_cache($courseorid->id, true);
} else {
rebuild_course_cache($courseorid, true);
}
rebuild_course_cache($courseid, true);
return $section->id; // Return course_sections ID that was used.
}
@ -837,7 +858,8 @@ function set_coursemodule_groupmode($id, $groupmode) {
$cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,groupmode', MUST_EXIST);
if ($cm->groupmode != $groupmode) {
$DB->set_field('course_modules', 'groupmode', $groupmode, array('id' => $cm->id));
rebuild_course_cache($cm->course, true);
course_purge_module_cache($cm);
rebuild_course_cache($cm->course, false, true);
}
return ($cm->groupmode != $groupmode);
}
@ -847,7 +869,8 @@ function set_coursemodule_idnumber($id, $idnumber) {
$cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,idnumber', MUST_EXIST);
if ($cm->idnumber != $idnumber) {
$DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
rebuild_course_cache($cm->course, true);
course_purge_module_cache($cm);
rebuild_course_cache($cm->course, false, true);
}
return ($cm->idnumber != $idnumber);
}
@ -924,7 +947,8 @@ function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1) {
}
}
rebuild_course_cache($cm->course, true);
course_purge_module_cache($cm);
rebuild_course_cache($cm->course, false, true);
return true;
}
@ -961,7 +985,8 @@ function set_coursemodule_name($id, $name) {
$DB->update_record($cm->modname, $module);
$cm->name = $module->name;
\core\event\course_module_updated::create_from_cm($cm)->trigger();
rebuild_course_cache($cm->course, true);
course_purge_module_cache($cm);
rebuild_course_cache($cm->course, false, true);
// Attempt to update the grade item if relevant.
$grademodule = $DB->get_record($cm->modname, array('id' => $cm->instance));
@ -1113,13 +1138,14 @@ function course_delete_module($cmid, $async = false) {
'context' => $modcontext,
'objectid' => $cm->id,
'other' => array(
'modulename' => $modulename,
'modulename' => $modulename,
'instanceid' => $cm->instance,
)
));
$event->add_record_snapshot('course_modules', $cm);
$event->trigger();
rebuild_course_cache($cm->course, true);
course_purge_module_cache($cm);
rebuild_course_cache($cm->course, false, true);
}
/**
@ -1376,17 +1402,23 @@ function move_section_to($course, $section, $destination, $ignorenumsections = f
$movedsections = reorder_sections($sections, $section, $destination);
$sectioninfo = new stdClass;
$sectioninfo->course = $course->id;
// Update all sections. Do this in 2 steps to avoid breaking database
// uniqueness constraint
$transaction = $DB->start_delegated_transaction();
foreach ($movedsections as $id => $position) {
if ($sections[$id] !== $position) {
$DB->set_field('course_sections', 'section', -$position, array('id' => $id));
$DB->set_field('course_sections', 'section', -$position, ['id' => $id]);
$sectioninfo->section = $id;
course_purge_section_cache($sectioninfo);
}
}
foreach ($movedsections as $id => $position) {
if ($sections[$id] !== $position) {
$DB->set_field('course_sections', 'section', $position, array('id' => $id));
$DB->set_field('course_sections', 'section', $position, ['id' => $id]);
$sectioninfo->section = $id;
course_purge_section_cache($sectioninfo);
}
}
@ -1394,14 +1426,14 @@ function move_section_to($course, $section, $destination, $ignorenumsections = f
// Adjust the higlighted section location if we move something over it either direction.
if ($section == $course->marker) {
course_set_marker($course->id, $destination);
} elseif ($section > $course->marker && $course->marker >= $destination) {
} else if ($section > $course->marker && $course->marker >= $destination) {
course_set_marker($course->id, $course->marker+1);
} elseif ($section < $course->marker && $course->marker <= $destination) {
} else if ($section < $course->marker && $course->marker <= $destination) {
course_set_marker($course->id, $course->marker-1);
}
$transaction->allow_commit();
rebuild_course_cache($course->id, true);
rebuild_course_cache($course->id, true, true);
return true;
}
@ -1576,7 +1608,8 @@ function course_update_section($course, $section, $data) {
$data['id'] = $section->id;
$data['timemodified'] = time();
$DB->update_record('course_sections', $data);
rebuild_course_cache($courseid, true);
course_purge_section_cache($section);
rebuild_course_cache($courseid, false, true);
course_get_format($courseid)->update_section_format_options($data);
// Update fields of the $section object.
@ -1608,11 +1641,13 @@ function course_update_section($course, $section, $data) {
} else {
// We hide the section, so we hide the module but we store the original state in visibleold.
set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage);
$DB->set_field('course_modules', 'visibleold', $cm->visible, array('id' => $moduleid));
$DB->set_field('course_modules', 'visibleold', $cm->visible, ['id' => $moduleid]);
course_purge_module_cache($cm);
}
\core\event\course_module_updated::create_from_cm($cm)->trigger();
}
}
rebuild_course_cache($courseid, false, true);
}
}
@ -5130,3 +5165,50 @@ function course_output_fragment_new_base_form($args) {
return $o;
}
/**
* Purge the cache of a course section.
*
* $sectioninfo must have following attributes:
* - course: course id
* - section: section number
*
* @param object $sectioninfo section info
* @return void
*/
function course_purge_section_cache(object $sectioninfo): void {
$section = $sectioninfo->section;
$courseid = $sectioninfo->course;
$cache = cache::make('core', 'coursemodinfo');
$cache->acquire_lock($courseid);
$coursemodinfo = $cache->get($courseid);
if (($coursemodinfo !== false) && array_key_exists($section, $coursemodinfo->sectioncache)) {
unset($coursemodinfo->sectioncache[$section]);
$cache->set($courseid, $coursemodinfo);
}
$cache->release_lock($courseid);
}
/**
* Purge the cache of a course module.
*
* $cm must have following attributes:
* - id: cmid
* - course: course id
*
* @param cm_info|stdClass $cm course module
* @return void
*/
function course_purge_module_cache($cm): void {
$cmid = $cm->id;
$courseid = $cm->course;
$cache = cache::make('core', 'coursemodinfo');
$cache->acquire_lock($courseid);
$coursemodinfo = $cache->get($courseid);
$hascache = ($coursemodinfo !== false) && array_key_exists($cmid, $coursemodinfo->modinfo);
if ($hascache) {
unset($coursemodinfo->modinfo[$cmid]);
$cache->set($courseid, $coursemodinfo);
}
$cache->release_lock($courseid);
}

View file

@ -190,7 +190,9 @@ if ((!empty($movetosection) or !empty($moveto)) and confirm_sesskey()) {
$DB->set_field('course_modules', 'indent', $cm->indent, array('id'=>$cm->id));
rebuild_course_cache($cm->course);
course_purge_module_cache($cm);
// Rebuild invalidated module cache.
rebuild_course_cache($cm->course, false, true);
redirect(course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn)));

View file

@ -374,7 +374,11 @@ function edit_module_post_actions($moduleinfo, $course) {
$moduleinfo->showgradingmanagement = $showgradingmanagement;
}
rebuild_course_cache($course->id, true);
$cminfo = new stdClass();
$cminfo->course = $moduleinfo->course;
$cminfo->id = $moduleinfo->coursemodule;
course_purge_module_cache($cminfo);
rebuild_course_cache($course->id, true, true);
if ($hasgrades) {
grade_regrade_final_grades($course->id);
}

View file

@ -1099,6 +1099,7 @@ class core_course_courselib_testcase extends advanced_testcase {
// Delete section in the middle (2).
$this->assertFalse(course_delete_section($course, 2, false));
$this->assertEquals(4, course_get_format($course)->get_last_section_number());
$this->assertTrue(course_delete_section($course, 2, true));
$this->assertFalse($DB->record_exists('course_modules', array('id' => $assign21->cmid)));
$this->assertFalse($DB->record_exists('course_modules', array('id' => $assign22->cmid)));

View file

@ -1085,8 +1085,9 @@ class externallib_test extends externallib_advanced_testcase {
array('course' => $course->id, 'intro' => 'forum completion tracking auto', 'trackingtype' => 2),
array('showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC));
$forumcompleteautocm = get_coursemodule_from_id('forum', $forumcompleteauto->cmid);
rebuild_course_cache($course->id, true);
$sectionrecord = $DB->get_record('course_sections', $conditions);
course_purge_section_cache($sectionrecord);
rebuild_course_cache($course->id, true, true);
return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm);
}