diff --git a/admin/cli/purge_caches.php b/admin/cli/purge_caches.php index 9846b2024b8..e79af0d051c 100644 --- a/admin/cli/purge_caches.php +++ b/admin/cli/purge_caches.php @@ -31,6 +31,7 @@ require_once($CFG->libdir.'/clilib.php'); $longoptions = [ 'help' => false, 'muc' => false, + 'courses' => false, 'theme' => false, 'lang' => false, 'js' => false, @@ -55,6 +56,8 @@ all caches will be purged. Options: -h, --help Print out this help --muc Purge all MUC caches (includes lang cache) + --courses Purge all course caches (or only those specified by a comma-separated list). + e.g. --courses=4,67,145 --theme Purge theme cache --lang Purge language string cache --js Purge JavaScript cache diff --git a/lib/modinfolib.php b/lib/modinfolib.php index 6531d775d6f..70822065b40 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -850,6 +850,39 @@ class course_modinfo { self::purge_course_modules_cache($courseid, [$cmid]); } + /** + * Purges the coursemodinfo caches stored in MUC. + * + * @param int[] $courseids Array of course ids to purge the course caches + * for (or all courses if empty array). + * + */ + public static function purge_course_caches(array $courseids = []): void { + global $DB; + + // Purging might purge all course caches, so use a recordset and close it. + $select = ''; + $params = null; + if (!empty($courseids)) { + [$sql, $params] = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); + $select = 'id ' . $sql; + } + + $courses = $DB->get_recordset_select( + table: 'course', + select: $select, + params: $params, + fields: 'id', + ); + + // Purge each course's cache to make sure cache is recalculated next time + // the course is viewed. + foreach ($courses as $course) { + self::purge_course_cache($course->id); + } + $courses->close(); + } + /** * Purge the cache of multiple course modules. * diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 22b56caf6f7..cdc4d0235ea 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -1188,7 +1188,7 @@ function purge_all_caches() { * 'other' Purge all other caches? */ function purge_caches($options = []) { - $defaults = array_fill_keys(['muc', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false); + $defaults = array_fill_keys(['muc', 'courses', 'theme', 'lang', 'js', 'template', 'filter', 'other'], false); if (empty(array_filter($options))) { $options = array_fill_keys(array_keys($defaults), true); // Set all options to true. } else { @@ -1197,6 +1197,14 @@ function purge_caches($options = []) { if ($options['muc']) { cache_helper::purge_all(); } + if ($options['courses']) { + if ($options['courses'] === true) { + $courseids = []; + } else { + $courseids = preg_split('/\s*,\s*/', $options['courses'], -1, PREG_SPLIT_NO_EMPTY); + } + course_modinfo::purge_course_caches($courseids); + } if ($options['theme']) { theme_reset_all_caches(); } diff --git a/lib/tests/modinfolib_test.php b/lib/tests/modinfolib_test.php index 578dcc578c1..55cb55d6129 100644 --- a/lib/tests/modinfolib_test.php +++ b/lib/tests/modinfolib_test.php @@ -1572,4 +1572,163 @@ class modinfolib_test extends advanced_testcase { $this->assertFalse($sectioninfos[1]->is_delegated()); $this->assertTrue($sectioninfos[2]->is_delegated()); } + + /** + * Test the course_modinfo::purge_course_caches() function with a + * one-course array, a two-course array, and an empty array, and ensure + * that only the courses specified have their course cache version + * incremented (or all course caches if none specified). + * + * @covers \course_modinfo + */ + public function test_multiple_modinfo_cache_purge(): void { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + $cache = cache::make('core', 'coursemodinfo'); + + // Generate two courses and pre-requisite modules for targeted course + // cache tests. + $courseone = $this->getDataGenerator()->create_course( + [ + 'format' => 'topics', + 'numsections' => 3, + ], + [ + 'createsections' => true, + ]); + $coursetwo = $this->getDataGenerator()->create_course( + [ + 'format' => 'topics', + 'numsections' => 3, + ], + [ + 'createsections' => true, + ]); + + $coursethree = $this->getDataGenerator()->create_course( + [ + 'format' => 'topics', + 'numsections' => 3, + ], + [ + 'createsections' => true, + ]); + + // Make sure the cacherev is set for all three. + $cacherevone = $DB->get_field('course', 'cacherev', ['id' => $courseone->id]); + $this->assertGreaterThan(0, $cacherevone); + $prevcacherevone = $cacherevone; + + $cacherevtwo = $DB->get_field('course', 'cacherev', ['id' => $coursetwo->id]); + $this->assertGreaterThan(0, $cacherevtwo); + $prevcacherevtwo = $cacherevtwo; + + $cacherevthree = $DB->get_field('course', 'cacherev', ['id' => $coursethree->id]); + $this->assertGreaterThan(0, $cacherevthree); + $prevcacherevthree = $cacherevthree; + + // Reset course caches and make sure cacherev is bumped up but cache is empty. + rebuild_course_cache($courseone->id, true); + $cacherevone = $DB->get_field('course', 'cacherev', ['id' => $courseone->id]); + $this->assertGreaterThan($prevcacherevone, $cacherevone); + $this->assertEmpty($cache->get_versioned($courseone->id, $prevcacherevone)); + $prevcacherevone = $cacherevone; + + rebuild_course_cache($coursetwo->id, true); + $cacherevtwo = $DB->get_field('course', 'cacherev', ['id' => $coursetwo->id]); + $this->assertGreaterThan($prevcacherevtwo, $cacherevtwo); + $this->assertEmpty($cache->get_versioned($coursetwo->id, $prevcacherevtwo)); + $prevcacherevtwo = $cacherevtwo; + + rebuild_course_cache($coursethree->id, true); + $cacherevthree = $DB->get_field('course', 'cacherev', ['id' => $coursethree->id]); + $this->assertGreaterThan($prevcacherevthree, $cacherevthree); + $this->assertEmpty($cache->get_versioned($coursethree->id, $prevcacherevthree)); + $prevcacherevthree = $cacherevthree; + + // Build course caches. Cacherev should not change but caches are now not empty. Make sure cacherev is the same everywhere. + $modinfoone = get_fast_modinfo($courseone->id); + $cacherevone = $DB->get_field('course', 'cacherev', ['id' => $courseone->id]); + $this->assertEquals($prevcacherevone, $cacherevone); + $cachedvalueone = $cache->get_versioned($courseone->id, $cacherevone); + $this->assertNotEmpty($cachedvalueone); + $this->assertEquals($cacherevone, $cachedvalueone->cacherev); + $this->assertEquals($cacherevone, $modinfoone->get_course()->cacherev); + $prevcacherevone = $cacherevone; + + $modinfotwo = get_fast_modinfo($coursetwo->id); + $cacherevtwo = $DB->get_field('course', 'cacherev', ['id' => $coursetwo->id]); + $this->assertEquals($prevcacherevtwo, $cacherevtwo); + $cachedvaluetwo = $cache->get_versioned($coursetwo->id, $cacherevtwo); + $this->assertNotEmpty($cachedvaluetwo); + $this->assertEquals($cacherevtwo, $cachedvaluetwo->cacherev); + $this->assertEquals($cacherevtwo, $modinfotwo->get_course()->cacherev); + $prevcacherevtwo = $cacherevtwo; + + $modinfothree = get_fast_modinfo($coursethree->id); + $cacherevthree = $DB->get_field('course', 'cacherev', ['id' => $coursethree->id]); + $this->assertEquals($prevcacherevthree, $cacherevthree); + $cachedvaluethree = $cache->get_versioned($coursethree->id, $cacherevthree); + $this->assertNotEmpty($cachedvaluethree); + $this->assertEquals($cacherevthree, $cachedvaluethree->cacherev); + $this->assertEquals($cacherevthree, $modinfothree->get_course()->cacherev); + $prevcacherevthree = $cacherevthree; + + // Purge course one's cache. Cacherev must be incremented (but only for + // course one, check course two and three in next step). + course_modinfo::purge_course_caches([$courseone->id]); + + get_fast_modinfo($courseone->id); + $cacherevone = $DB->get_field('course', 'cacherev', ['id' => $courseone->id]); + $this->assertGreaterThan($prevcacherevone, $cacherevone); + $prevcacherevone = $cacherevone; + + // Confirm course two and three's cache shouldn't have been affected. + get_fast_modinfo($coursetwo->id); + $cacherevtwo = $DB->get_field('course', 'cacherev', ['id' => $coursetwo->id]); + $this->assertEquals($prevcacherevtwo, $cacherevtwo); + $prevcacherevtwo = $cacherevtwo; + + get_fast_modinfo($coursethree->id); + $cacherevthree = $DB->get_field('course', 'cacherev', ['id' => $coursethree->id]); + $this->assertEquals($prevcacherevthree, $cacherevthree); + $prevcacherevthree = $cacherevthree; + + // Purge course two and three's cache. Cacherev must be incremented (but only for + // course two and three, then check course one hasn't changed in next step). + course_modinfo::purge_course_caches([$coursetwo->id, $coursethree->id]); + + get_fast_modinfo($coursetwo->id); + $cacherevtwo = $DB->get_field('course', 'cacherev', ['id' => $coursetwo->id]); + $this->assertGreaterThan($prevcacherevtwo, $cacherevtwo); + $prevcacherevtwo = $cacherevtwo; + + get_fast_modinfo($coursethree->id); + $cacherevthree = $DB->get_field('course', 'cacherev', ['id' => $coursethree->id]); + $this->assertGreaterThan($prevcacherevthree, $cacherevthree); + $prevcacherevthree = $cacherevthree; + + // Confirm course one's cache shouldn't have been affected. + get_fast_modinfo($courseone->id); + $cacherevone = $DB->get_field('course', 'cacherev', ['id' => $courseone->id]); + $this->assertEquals($prevcacherevone, $cacherevone); + $prevcacherevone = $cacherevone; + + // Purge all course caches. Cacherev must be incremented for all three courses. + course_modinfo::purge_course_caches(); + get_fast_modinfo($courseone->id); + $cacherevone = $DB->get_field('course', 'cacherev', ['id' => $courseone->id]); + $this->assertGreaterThan($prevcacherevone, $cacherevone); + + get_fast_modinfo($coursetwo->id); + $cacherevtwo = $DB->get_field('course', 'cacherev', ['id' => $coursetwo->id]); + $this->assertGreaterThan($prevcacherevtwo, $cacherevtwo); + + get_fast_modinfo($coursethree->id); + $cacherevthree = $DB->get_field('course', 'cacherev', ['id' => $coursethree->id]); + $this->assertGreaterThan($prevcacherevthree, $cacherevthree); + } + }