From 6a042d9e3fd91df97507a7d88a2c957dcd6eef59 Mon Sep 17 00:00:00 2001 From: Paul McKeown Date: Sat, 7 Dec 2024 12:16:17 +1300 Subject: [PATCH] Progress on scheduled grading cache purging and scripts for purging by course. Also contains new functionality in bulktester (eg, nruns setting) and updates to the Coderunner settings.php --- bulktest.php | 3 +- bulktestindex.php | 11 +- cachepurge.php | 65 ++++++ cachepurgeindex.php | 107 ++++++++++ classes/bulk_tester.php | 50 ++++- classes/cache_purger.php | 225 +++++++++++++++++++++ classes/constants.php | 4 +- classes/external/run_in_sandbox.php | 2 +- classes/jobesandbox.php | 18 +- classes/student.php | 2 + classes/task/cache_cleaner.php | 130 ++++-------- db/caches.php | 4 +- lang/en/qtype_coderunner.php | 2 + lib.php | 5 + settings.php | 34 +++- tests/behat/test_combinator_grader.feature | 1 + version.php | 2 +- 17 files changed, 547 insertions(+), 118 deletions(-) create mode 100644 cachepurge.php create mode 100644 cachepurgeindex.php create mode 100644 classes/cache_purger.php diff --git a/bulktest.php b/bulktest.php index 6cfbba8ad..1e835fe5e 100644 --- a/bulktest.php +++ b/bulktest.php @@ -32,6 +32,7 @@ // Get the parameters from the URL. $contextid = required_param('contextid', PARAM_INT); $categoryid = optional_param('categoryid', null, PARAM_INT); +$nruns = optional_param('nruns', 1, PARAM_INT); // Login and check permissions. $context = context::instance_by_id($contextid); @@ -60,7 +61,7 @@ echo $OUTPUT->heading($title); // Run the tests. -[$numpasses, $failingtests, $missinganswers] = $bulktester->run_all_tests_for_context($context, $categoryid); +[$numpasses, $failingtests, $missinganswers] = $bulktester->run_all_tests_for_context($context, $categoryid, $nruns); // Display the final summary. $bulktester->print_overall_result($numpasses, $failingtests, $missinganswers); diff --git a/bulktestindex.php b/bulktestindex.php index a4cb26dc1..3801d4309 100644 --- a/bulktestindex.php +++ b/bulktestindex.php @@ -34,6 +34,13 @@ $PAGE->set_context($context); $PAGE->set_title(get_string('bulktestindextitle', 'qtype_coderunner')); +$nruns = 1; +$nrunsfromsettings = get_config('qtype_coderunner', 'bulktestdefaultnruns'); +if (abs($nrunsfromsettings) > 1) { + $nruns = abs($nrunsfromsettings); +} + + // Create the helper class. $bulktester = new qtype_coderunner_bulk_tester(); @@ -75,7 +82,7 @@ $contextid = $info['contextid']; $numcoderunnerquestions = $info['numquestions']; - $testallurl = new moodle_url('/question/type/coderunner/bulktest.php', ['contextid' => $contextid]); + $testallurl = new moodle_url('/question/type/coderunner/bulktest.php', ['contextid' => $contextid, 'nruns' => $nruns]); $testalllink = html_writer::link( $testallurl, get_string('bulktestallincontext', 'qtype_coderunner'), @@ -110,7 +117,7 @@ if ($cat->count > 0) { $url = new moodle_url( '/question/type/coderunner/bulktest.php', - ['contextid' => $contextid, 'categoryid' => $cat->id] + ['contextid' => $contextid, 'categoryid' => $cat->id, 'nruns' => $nruns] ); $linktext = $cat->name . ' (' . $cat->count . ')'; $link = html_writer::link($url, $linktext, ['style' => $buttonstyle]); diff --git a/cachepurge.php b/cachepurge.php new file mode 100644 index 000000000..91bb47741 --- /dev/null +++ b/cachepurge.php @@ -0,0 +1,65 @@ +. + +/** + * This script runs all the question tests for all deployed versions of all + * questions in a given context and, optionally, a given question category. + * It is a modified version of the script from the qtype_stack plugin. + * + * @package qtype_coderunner + * @copyright 2016 Richard Lobb, The University of Canterbury + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace qtype_coderunner; + +use context; + +define('NO_OUTPUT_BUFFERING', true); + +require_once(__DIR__ . '/../../../config.php'); +// require_once($CFG->libdir . '/questionlib.php'); + + + +// Get the parameters from the URL. +$contextid = required_param('contextid', PARAM_INT); +$usettl = required_param('usettl', PARAM_INT); // 1 for use, 0 for don't use. + +// Login and check permissions. +$context = context::instance_by_id($contextid); +require_login(); +require_capability('moodle/question:editall', $context); +$PAGE->set_url('/question/type/coderunner/cachepurge.php', ['contextid' => $context->id, 'useTTL' => $usettl]); +$PAGE->set_context($context); +$title = 'Purging cache for ' . $context->get_context_name(); //get_string('bulktesttitle', 'qtype_coderunner', $context->get_context_name()); +$PAGE->set_title($title); + + + +// Release the session, so the user can do other things while this runs. +\core\session\manager::write_close(); + +// Create the helper class. +$purger = new cache_purger(); + + +echo $OUTPUT->header(); +echo $OUTPUT->heading($title, 4); + +$purger->purge_cache_for_context($context->id, $usettl); + + +echo $OUTPUT->footer(); diff --git a/cachepurgeindex.php b/cachepurgeindex.php new file mode 100644 index 000000000..ee2031c40 --- /dev/null +++ b/cachepurgeindex.php @@ -0,0 +1,107 @@ +. + +/** + * This script provides an index for purging grading cache entries by course. + * + * @package qtype_coderunner + * @copyright 2024 Paul McKeown, The University of Canterbury + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace qtype_coderunner; + +use context_system; +use context; +use html_writer; +use moodle_url; + + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir . '/questionlib.php'); + +// Login and check permissions. +$context = context_system::instance(); +require_login(); + +$PAGE->set_url('/question/type/coderunner/cachepurgeindex.php'); +$PAGE->set_context($context); +$PAGE->set_title('Coderunner Cache Purge Index'); //get_string('bulktestindextitle', 'qtype_coderunner')); + + +// Display. +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('coderunnercontexts', 'qtype_coderunner')); + +// Find in which contexts the user can edit questions. +//$questionsbycontext = $bulktester->get_num_coderunner_questions_by_context(); + + +$cachepurger = new cache_purger(); +$allvisiblecoursecontexts = $cachepurger->get_all_visible_course_contextids(); +krsort($allvisiblecoursecontexts); // Effectively newest first. +$keycounts = $cachepurger->key_count_by_course($allvisiblecoursecontexts); + +// List all contexts available to the user. +if (count($allvisiblecoursecontexts) == 0) { + echo html_writer::tag('p', get_string('unauthorisedbulktest', 'qtype_coderunner')); +} else { + echo html_writer::start_tag('ul'); + //$buttonstyle = 'border: 1px solid gray; padding: 2px 2px 0px 2px;'; + $buttonstyle = 'border: 1px solid #F0F0F0; background-color: #FFFFC0; padding: 2px 2px 0px 2px;border: 4px solid white'; + foreach ($allvisiblecoursecontexts as $contextid) { + $context = context::instance_by_id($contextid); + $name = $context->get_context_name(true, true); + $courseid = $context->instanceid; + + $purgeusingttlurl = new moodle_url('/question/type/coderunner/cachepurge.php', ['contextid' => $contextid, 'usettl' => 1]); + $purgeusingttllink = html_writer::link( + $purgeusingttlurl, + 'Purge all old cache entries (ie, using TTL)', + // get_string('bulktestallincontext', 'qtype_coderunner'), + ['title' => 'Purge all old', //get_string('testalltitle', 'qtype_coderunner'), + 'style' => $buttonstyle] + ); + + $purgeallurl = new moodle_url('/question/type/coderunner/cachepurge.php', ['contextid' => $contextid, 'usettl' => 0]); + $purgealllink = html_writer::link( + $purgeallurl, + 'Purge all in context', + // get_string('bulktestallincontext', 'qtype_coderunner'), + ['title' => 'Purge all', //get_string('testalltitle', 'qtype_coderunner'), + 'style' => $buttonstyle] + ); + + + $litext = $name . ' [Course id= ' . $courseid . ']    cache size=' . $keycounts[$contextid] . '   ' . $purgeusingttllink . '   ' . $purgealllink; + $class = 'cachepurge coderunner context normal'; + echo html_writer::start_tag('li', ['class' => $class]); + echo $litext; + echo html_writer::end_tag('li'); + } + echo html_writer::end_tag('ul'); + + // Maybe do a purge all later... + // if (has_capability('moodle/site:config', context_system::instance())) { + // echo html_writer::tag('p', html_writer::link( + // new moodle_url('/question/type/coderunner/bulktestall.php'), + // get_string('bulktestrun', 'qtype_coderunner') + // )); + // } +} + + +echo $OUTPUT->footer(); diff --git a/classes/bulk_tester.php b/classes/bulk_tester.php index fbad17031..0c5593b03 100644 --- a/classes/bulk_tester.php +++ b/classes/bulk_tester.php @@ -178,12 +178,13 @@ public function get_all_coderunner_questions_in_context($contextid, $includeprot * * @param context $context the context to run the tests for. * @param int $categoryid test only questions in this category. Default to all. + * @param int $nruns the number times to test each question. Default to 1. * @return array with three elements: * int a count of how many tests passed * array of messages relating to the questions with failures * array of messages relating to the questions without sample answers */ - public function run_all_tests_for_context(context $context, $categoryid = null) { + public function run_all_tests_for_context(context $context, $categoryid = null, $nruns = 1) { global $OUTPUT; // Load the necessary data. @@ -221,33 +222,62 @@ public function run_all_tests_for_context(context $context, $categoryid = null) ); $enhancedname = "{$question->name} (V{$question->version})"; $questionnamelink = html_writer::link($previewurl, $enhancedname, ['target' => '_blank']); - echo "
  • $questionnamelink:"; + echo "
  • $questionnamelink: "; flush(); // Force output to prevent timeouts and show progress. + $passstr = get_string('pass', 'qtype_coderunner'); + $failstr = get_string('fail', 'qtype_coderunner'); + $npasses = 0; + $nfails = 0; // Now run the test. - try { - [$outcome, $message] = $this->load_and_test_question($question->id); - } catch (Exception $e) { - $message = $e->getMessage(); - $outcome = self::FAIL; + for ($i = 0; $i < $nruns; $i++) { + // only records last outcome and message + try { + [$outcome, $message] = $this->load_and_test_question($question->id); + } catch (Exception $e) { + $message = $e->getMessage(); + $outcome = self::FAIL; + echo "x"; + } + if ($outcome == self::MISSINGANSWER) { + echo " $message "; + break; // No point trying again as there is no answer to check. + } else { + if ($outcome == self::PASS) { + $npasses += 1; + echo "."; + } else { + $nfails += 1; + echo "."; + } + //echo " $message, "; + } } // Report the result, and record failures for the summary. - echo " $message
  • "; + if ($outcome != self::MISSINGANSWER) { + echo "   " . $passstr . "=" . $npasses. ""; + if ($nfails > 0) { + echo ", " . $failstr . '=' . $nfails. ""; + } + } + echo ""; flush(); // Force output to prevent timeouts and show progress. $qparams['category'] = $currentcategoryid . ',' . $context->id; $qparams['lastchanged'] = $question->id; $qparams['qperpage'] = 1000; $questionbankurl = new moodle_url('/question/edit.php', $qparams); $questionbanklink = html_writer::link($questionbankurl, $nameandcount->name, ['target' => '_blank']); - if ($outcome === self::PASS) { + if ($npasses == $nruns) { $numpasses += 1; } else if ($outcome === self::MISSINGANSWER) { $missinganswers[] = "$coursename / $questionbanklink / $questionnamelink"; } else { - $failingtests[] = "$coursename / $questionbanklink / $questionnamelink: $message"; + $failmessage = " " . get_string('fail', 'qtype_coderunner') . '=' . $nfails. ""; + $failingtests[] = "$coursename / $questionbanklink / $questionnamelink: $failmessage"; } } + // echo " $message "; echo "\n"; } diff --git a/classes/cache_purger.php b/classes/cache_purger.php new file mode 100644 index 000000000..3e616e69f --- /dev/null +++ b/classes/cache_purger.php @@ -0,0 +1,225 @@ +. + +/** + * This script provides a class with support methods for running question tests in bulk. + * It is taken from the qtype_stack plugin with slight modifications. + * + * Modified to provide services for the prototype usage script and the + * autotagger script. + * + * @package qtype_coderunner + * @copyright 2024 Paul McKeown, The University of Canterbury + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or laterfunction local_qtype_coderunner_reload_cache_definitions_after_ttl_update(string $caller) { + cache_helper::update_definitions(); +} + */ + +namespace qtype_coderunner; + +use cache; +use cache_helper; +use cachestore_file; +use cache_definition; +use context; +use moodle_url; +use core\chart_bar; +use core\chart_series; + +defined('MOODLE_INTERNAL') || die(); + +//require_once(__DIR__ . '/../../../../config.php'); + +class cache_purger { + + /** + * Get all the visible course contexts. + * + * @return array context ids + */ + public function get_all_visible_course_contextids() { + global $DB; + + $allcontexts = $DB->get_records_sql(" + SELECT ctx.id as contextid + FROM {context} ctx + ORDER BY contextid"); //, ['contextid' => $contextid]); + $result = []; + foreach ($allcontexts as $record) { + $contextid = $record->contextid; + $context = context::instance_by_id($contextid); + if (has_capability('moodle/question:editall', $context)) { + // Only add in courses for now. + if ($context->contextlevel == CONTEXT_COURSE) { + $result[] = $contextid; + } + } + } // endfor each contextid + return $result; + } + + + + // Returns a map from contextid to count of keys + public function key_count_by_course(array $contextids) { + $contextcounts = []; + $coursetocontext = []; // Maps courseid to contextid. + foreach ($contextids as $contextid) { + $contextcounts[$contextid] = 0; + $context = context::instance_by_id($contextid); + $coursename = $context->get_context_name(true, true); + if ($context->contextlevel == CONTEXT_COURSE) { + $courseid = $context->instanceid; + $coursetocontext[$courseid] = $contextid; + } else { + // should be an error here + } + } + $definition = $this->get_coderunner_cache_definition(); + $store = $this->get_first_file_store($definition); + $keys = $store->find_all(); + //print_r($keys); + $pattern = '/_courseid_(\d+)_/'; + foreach ($keys as $key) { + preg_match($pattern, $key, $match); + $courseid = $match[1]; + if (array_key_exists($courseid, $coursetocontext)) { + $contextid = $coursetocontext[$courseid]; + $contextcounts[$contextid] += 1; + } + } + // go through all keys and count by context... + return $contextcounts; + } + + + public function get_coderunner_cache_definition() { + $configerer = \cache_config::instance(); + $defs = $configerer->get_definitions(); + foreach ($defs as $id => $def) { + if ($def['component'] == 'qtype_coderunner' && $def['area'] == 'coderunner_grading_cache') { + $definition = cache_definition::load($id, $def); + return $definition; + } + } + return null; // Probably should raise an exception here... + } + + + public function get_first_file_store(cache_definition $definition) { + $stores = cache_helper::get_cache_stores($definition); + // Should really only be one file store but go through them if needed... + foreach ($stores as $store) { + if ($store instanceof cachestore_file) { + return $store; + } + } + return null; // whoops! + } + + + + + public function purge_cache_for_context(int $contextid, int $usettl) { + global $OUTPUT; + global $CFG; + // Load the necessary data. + $context = context::instance_by_id($contextid); + $coursename = $context->get_context_name(true, true); + if ($context->contextlevel == CONTEXT_COURSE) { + $courseid = $context->instanceid; + } else { + echo 'Nothing to do as context_id $contextid is not a course.'; + return; + } + //echo $OUTPUT->heading("Purging cache for course " . $courseid, 4); + $definition = self::get_coderunner_cache_definition(); + $ttl = $definition->get_ttl(); + $days = round($ttl / 60 / 60 / 24, 4); + + if ($usettl) { + echo "

    Purging only old keys for course, based on Time to Live. TTL="; + echo "$ttl seconds (= $days days)

    "; + } else { + echo "

    Purging all keys for course, regardless of Time to Live (TTL).

    "; + } + $store = $this->get_first_file_store($definition); + // $store->purge_old_entries(); + + // Use reflection to access the private cachestore_file method file_path_for_key + $reflection = new \ReflectionClass($store); + $filepathmethod = $reflection->getMethod('file_path_for_key'); + $filepathmethod->setAccessible(true); + + $keys = $store->find_all(); + $originalcount = count($keys); + + // Delete all keys for course if usettl is false otherwise only old ones. + $maxtime = cache::now() - $ttl; + $onepercent = round($originalcount / 100, 0); + $numdeleted = 0; + $tooyoungtodie = 0; + $keysforcourse = 0; + $numprocessed = 0; + if ($originalcount > 0){ + $progressbar = new \progress_bar('cache_purge_progress_bar', width:800, autostart:true); + } + foreach ($keys as $key) { + $numprocessed += 1; + // Call the private file_path_for_key method on the cache store. + $path = $filepathmethod->invoke($store, $key); + $pattern = '/_courseid_' . $courseid . '_/'; + $file = basename($path); + if (preg_match($pattern, $file)) { + $keysforcourse += 1; + if (!$usettl) { + $store->delete($key); + $numdeleted += 1; + } else { + $filetime = filemtime($path); + if ($ttl && $filetime < $maxtime) { + $store->delete($key); + $numdeleted += 1; + } else { + $tooyoungtodie += 1; + } + } + // $value = $store->get($key); // Would delete old key if fixed in file store. + } + if ($originalcount > 0 && ($originalcount < 100 || $numprocessed % $onepercent == 0)){ + $progressbar->update($numprocessed, + $originalcount, + "$numprocessed / $originalcount" + //get_string('regradingattemptxofywithdetails', //'quiz_overview', [$numprocessed, $originalcount]) + ); + } + } + // Make sure it gets to 100% + if ($originalcount > 0) { + $progressbar->update($numprocessed, + $originalcount, + "$numprocessed / $originalcount", + //get_string('regradingattemptxofywithdetails', //'quiz_overview', [$numprocessed, $originalcount]) + ); + } + echo "$originalcount keys scanned, in total.
    "; + echo "$keysforcourse keys found for course. "; + echo "$numdeleted keys purged for course.
    "; + echo "$tooyoungtodie keys were too young to die.
    "; + } + + +} diff --git a/classes/constants.php b/classes/constants.php index 858b57afb..b97b13e7d 100644 --- a/classes/constants.php +++ b/classes/constants.php @@ -54,5 +54,7 @@ class constants { const DEFAULT_NUM_ROWS = 18; // Default answerbox size. - const ANSWER_CODE_KEY = 'answer_code'; // The key to the code in a Scratchpad UI question . + const ANSWER_CODE_KEY = 'answer_code'; // The key to the code in a Scratchpad UI question. + + const GRADING_CACHE_DEFAULT_TTL = 1209600; // Two weeks in seconds. } diff --git a/classes/external/run_in_sandbox.php b/classes/external/run_in_sandbox.php index d73b429f2..f42a5a89c 100644 --- a/classes/external/run_in_sandbox.php +++ b/classes/external/run_in_sandbox.php @@ -172,7 +172,7 @@ public static function execute( if ($filesarray === null || $paramsarray === null) { throw new qtype_coderunner_exception(get_string('wsbadjson', 'qtype_coderunner')); } - $maxcputime = intval(get_config('qtype_coderunner', 'wsmaxcputime')); // Limit CPU time through this service. + $maxcputime = floatval(get_config('qtype_coderunner', 'wsmaxcputime')); // Limit CPU time through this service. if (isset($paramsarray['cputime'])) { if ($paramsarray['cputime'] > $maxcputime) { throw new qtype_coderunner_exception(get_string('wscputimeexcess', 'qtype_coderunner')); diff --git a/classes/jobesandbox.php b/classes/jobesandbox.php index 17eae29e4..5dbc890fc 100644 --- a/classes/jobesandbox.php +++ b/classes/jobesandbox.php @@ -30,6 +30,9 @@ global $CFG; require_once($CFG->libdir . '/filelib.php'); // Needed when run as web service. +require_login(); + + class qtype_coderunner_jobesandbox extends qtype_coderunner_sandbox { const DEBUGGING = 0; @@ -152,7 +155,16 @@ public function get_languages() { public function execute($sourcecode, $language, $input, $files = null, $params = null, $usecache = true) { global $CFG; - + global $COURSE; + global $PAGE; + // Get the current context. + $context = $PAGE->context; + $contextid = $context->id; + if ($context->contextlevel == CONTEXT_COURSE) { + $courseid = $context->instanceid; + } else { + $courseid = 1; // seems to be the fall back if it's not a course, eg, when bulktesting all q's + } $language = strtolower($language); if (is_null($input)) { $input = ''; @@ -223,7 +235,7 @@ public function execute($sourcecode, $language, $input, $files = null, $params = // eg, adding another jobeserver to a list of servers will mean the // jobeserver parameter has changed and therefore the key will change. - $key = hash("md5", serialize($runspec)); + $key = hash("md5", serialize($runspec)) . '_courseid_' . $courseid .'_'; // Debugger: echo '
    ' . serialize($runspec) . '
    ';. $runresult = $cache->get($key); // Unserializes the returned value :) false if not found. } @@ -296,7 +308,7 @@ public function execute($sourcecode, $language, $input, $files = null, $params = // Got a useable result from Jobe server so cache it if required. if (get_config('qtype_coderunner', 'enablegradecache') && $usecache) { - $key = hash("md5", serialize($runspec)); + $key = hash("md5", serialize($runspec)) . '_courseid_' . $courseid . '_'; $cache->set($key, $runresult); // Set serializes the result, get will unserialize. } } diff --git a/classes/student.php b/classes/student.php index c04e85ee4..c9c33c57c 100644 --- a/classes/student.php +++ b/classes/student.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + + class qtype_coderunner_student { /** @var int */ diff --git a/classes/task/cache_cleaner.php b/classes/task/cache_cleaner.php index 9b6693dfa..c172aaf9d 100644 --- a/classes/task/cache_cleaner.php +++ b/classes/task/cache_cleaner.php @@ -20,6 +20,9 @@ * This version doesn't do any authentication; it's assumed the server is * firewalled to accept connections only from Moodle. * + * This class is used by the scheduler to cleanup the cache + * Admins can change the schedule in Site Adminstration -> Server -> Scheduled Tasks -> Purge Old Coderunner Cache Entries + * * @package qtype_coderunner * @copyright 2024 Paul McKeown, University of Canterbury * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -33,16 +36,16 @@ use cache; use cache_helper; -use cache_store_file; +use cachestore_file; use cache_definition; +use qtype_coderunner\cache_purger; global $CFG; /** - * Task for purging old Coderunner cach entries + * Task for purging old Coderunner cache entries. */ class cache_cleaner extends \core\task\scheduled_task { - const MAX_AGE = 10; // 7 * 24 * 60 * 6; // Seven days. /** * Return the task's name as shown in admin screens. @@ -50,102 +53,45 @@ class cache_cleaner extends \core\task\scheduled_task { * @return string */ public function get_name() { - return ('Purge Old Coderunner Cache Entries'); // A get_string('purgeoldcacheentries', 'qtype_coderunner');. + return ('Purge Old Coderunner Cache Entries - task'); // A get_string('purgeoldcacheentries', 'qtype_coderunner');. } /** * Execute the task. */ public function execute() { - - $configerer = \cache_config::instance(); - $defs = $configerer->get_definitions(); - foreach ($defs as $id => $def) { - if ($def['component'] == 'qtype_coderunner' && $def['area'] == 'coderunner_grading_cache') { - $definition = cache_definition::load($id, $def); - mtrace('ID for created definition '.$definition->get_id()); - $stores = cache_helper::get_cache_stores($definition); - $ttl = $definition->get_ttl(); - $days = $ttl / 60 / 60 / 24; - mtrace("Time to live (TTL) is $ttl seconds (= $days days)"); - foreach ($stores as $store) { - mtrace("Store is searchable: " . $store->is_searchable()); - if ($store->is_searchable()) { - $keys = $store->find_all(); - $originalcount = count($keys); - // Do a get on every key. - // The file cache get method should delete keys that are older than ttl - foreach ($keys as $key) { - $value = $store->get($key); - } - $remainingkeys = $store->find_all(); - $newcount = count($remainingkeys); - mtrace("Did a get on all $originalcount keys."); - mtrace("There are $newcount keys remaining."); - } else { - mtrace("Cache isn't searchable so can find all the keys..."); - } - } - break; // Should be only one definition for Coderunner. + $purger = new cache_purger(); + $definition = $purger->get_coderunner_cache_definition(); + $store = $purger->get_first_file_store($definition); + $ttl = $definition->get_ttl(); + if ($ttl) { + $days = round($ttl / 60 / 60 / 24, 4); + mtrace("Time to live (TTL) is $ttl seconds (= $days days)"); + } + // $store->purge_old_entries(); + + // Use reflection to access the private cachestore_file method file_path_for_key + $reflection = new \ReflectionClass($store); + $filepathmethod = $reflection->getMethod('file_path_for_key'); + $filepathmethod->setAccessible(true); + + $keys = $store->find_all(); + $originalcount = count($keys); + // Do a get on every key. + // The file cache get method should delete keys that are older than ttl + $maxtime = cache::now() - $ttl; + foreach ($keys as $key) { + // Call the private method + $path = $filepathmethod->invoke($store, $key); + $filetime = filemtime($path); + if ($ttl && $filetime < $maxtime) { + $store->delete($key); } } - mtrace("Done for now."); - - // Really needed ??? global $DB;. - // $cache = cache::make('qtype_coderunner', 'coderunner_grading_cache'); - // $keys = $cache->find_all(); - // foreach ($keys as $key) { - // mtrace($key); - // } - - - - //if (! $cache instanceof cachestore_file) { - // echo($cache); - // mtrace('Cache store is not a file cache store - cleanup not needed'); - //} else { - // $reflection = new \ReflectionClass($cache); - // $property = $reflection->getProperty('store'); - // $property->setAccessible(true); - - - - - // // Use reflection to access the private method - // $reflection = new \ReflectionClass($cache); - // $method = $reflection->getMethod('get_store'); - // $method->setAccessible(true); - // // Call the private method - // $store = $method->invoke($cache); - - // $reflection = new \ReflectionClass($store); - // $property = $reflection->getProperty('path'); - // $property->setAccessible(true); - // $path = $property->getValue($store); - - // mtrace("qtype_coderunner coderunner_grading_cache cleanup initiated."); - // mtrace("Cache files are stored in: $path"); - // if ($path) { - // // Scans cache directory. - // $files = new \RecursiveDirectoryIterator($path); - // $iterator = new \RecursiveIteratorIterator($files); - // $deleted = 0; - // $found = 0; - // $now = time(); - // foreach ($iterator as $file) { - // if ($file->isFile()) { - // $found++; - // $mtime = $file->getMTime(); - // if (($now - $mtime) > self::MAX_AGE) { - // if (unlink($file->getPathname())) { - // $deleted++; - // } - // } - // } - // } - // mtrace("Found $found cache files in total."); - // mtrace("Deleted $deleted old cache files."); - - //} + // $value = $store->get($key); // Would delete old key if fixed in file store. + $remainingkeys = $store->find_all(); + $newcount = count($remainingkeys); + mtrace("Originally found $originalcount keys."); + mtrace("There are $newcount keys remaining."); } } diff --git a/db/caches.php b/db/caches.php index 1464c4e12..09856e330 100644 --- a/db/caches.php +++ b/db/caches.php @@ -34,8 +34,6 @@ 'simplekeys' => true, 'simpledata' => false, 'canuselocalstore' => true, - 'staticacceleration' => true, - 'ttl' => 60, // 14 * 24 * 60 * 60, // Time to live is two weeks. Change as you see fit and reload cache definitions. - 'staticaccelerationsize' => 1000000, + 'ttl' => abs(get_config('qtype_coderunner', 'gradecachettl')), // Change this in Coderunner settings. ], ]; diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php index 82126aa85..cdb0a4139 100644 --- a/lang/en/qtype_coderunner.php +++ b/lang/en/qtype_coderunner.php @@ -1347,3 +1347,5 @@ function should be applied, e.g. {{STUDENT_ANSWER | e(\'py\')}} is $string['coderunner_grading_cache'] = 'Caches grading results so we can avoid going to Jobe so often.'; $string['cachedef_coderunner_grading_cache'] = 'Caches grading results so we can avoid going to Jobe so often.'; $string['enablegradecache_desc'] = 'Experimental. The cache is a local Moodle cache (currently file cache) to store results of grading questions. Mainly to speed up regrading by using cached results for jobe runs where the same jobe submission has already been graded. Currently WS jobs (eg, try-it boxes and scratchpad runs) will never be cached. NOTE: If you turn off grade caching then it is usually good to empty the Coderunner grade cache before you turn it on again so you have a known state for the cache. You should also clear the cache if you change the Jobe back-end (eg, installing a new version of Python there) as results may now differ from what is in the cache.'; +$string['settingsgradecachettl'] = 'Grade cache Time to Live (TTL)'; +$string['settingsgradecachettl_desc'] = 'Number of seconds for grade cache entries to live. Default is 1209600 seconds (two weeks). Used by scheduled task and helpful cachepurgeindex/cachepurge scripts. Admins can set schedule for running TTL enforcer in Admin->Server->Scheduled tasks - look for the Coderunner entry.'; diff --git a/lib.php b/lib.php index 910946137..e7f35e3f0 100644 --- a/lib.php +++ b/lib.php @@ -22,6 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + /** * Checks file access for CodeRunner questions. * Feedbackfiles are stored in the course context, because they are generated @@ -54,3 +55,7 @@ function qtype_coderunner_pluginfile($course, $cm, $context, $filearea, $args, $ require_once($CFG->libdir . '/questionlib.php'); question_pluginfile($course, $context, 'qtype_coderunner', $filearea, $args, $forcedownload, $options); } + +function qtype_coderunner_reload_cache_definitions_after_ttl_update(string $caller) { + cache_helper::update_definitions(); +} diff --git a/settings.php b/settings.php index ef2f178d8..08e1780d2 100644 --- a/settings.php +++ b/settings.php @@ -24,6 +24,8 @@ defined('MOODLE_INTERNAL') || die(); use qtype_coderunner\constants; +require_once('__DIR__'.'/../lib.php'); + $links = [ get_string( @@ -49,7 +51,9 @@ "qtype_coderunner/default_penalty_regime", get_string('default_penalty_regime', 'qtype_coderunner'), get_string('default_penalty_regime_desc', 'qtype_coderunner'), - '10, 20, ...' + '10, 20, ...', + PARAM_RAW, + 20 )); $sandboxes = qtype_coderunner_sandbox::available_sandboxes(); @@ -75,7 +79,9 @@ "qtype_coderunner/jobe_apikey", get_string('jobe_apikey', 'qtype_coderunner'), get_string('jobe_apikey_desc', 'qtype_coderunner'), - constants::JOBE_HOST_DEFAULT_API_KEY + constants::JOBE_HOST_DEFAULT_API_KEY, + PARAM_RAW, + 40 )); $settings->add(new admin_setting_configcheckbox( @@ -86,6 +92,22 @@ )); +$cachettlsetting = new admin_setting_configtext( + "qtype_coderunner/gradecachettl", + get_string('settingsgradecachettl', 'qtype_coderunner'), + get_string('settingsgradecachettl_desc', 'qtype_coderunner'), + constants::GRADING_CACHE_DEFAULT_TTL, + PARAM_INT, + 10 +); +$cachettlsetting->set_updatedcallback(function () { + cache_helper::update_definitions(); +}); +$settings->add($cachettlsetting); + + + + $settings->add(new admin_setting_configtext( "qtype_coderunner/ideone_user", get_string('ideone_user', 'qtype_coderunner'), @@ -135,12 +157,16 @@ "qtype_coderunner/wsmaxhourlyrate", get_string('wsmaxhourlyrate', 'qtype_coderunner'), get_string('wsmaxhourlyrate_desc', 'qtype_coderunner'), - '200' + 200, + PARAM_INT, + 10 )); $settings->add(new admin_setting_configtext( "qtype_coderunner/wsmaxcputime", get_string('wsmaxcputime', 'qtype_coderunner'), get_string('wsmaxcputime_desc', 'qtype_coderunner'), - '5' + 5.0, + PARAM_FLOAT, + 10 )); diff --git a/tests/behat/test_combinator_grader.feature b/tests/behat/test_combinator_grader.feature index 5c6245a96..b7ef3690a 100644 --- a/tests/behat/test_combinator_grader.feature +++ b/tests/behat/test_combinator_grader.feature @@ -28,6 +28,7 @@ Feature: test_combinator_grader And I set the field "id_customise" to "1" And I set the field "id_useace" to "0" And I set the field "id_uiplugin" to "None" + And I set CodeRunner behat testing flag And I set the field "id_template" to: """ import subprocess, json, sys diff --git a/version.php b/version.php index ba6ff99cd..c31dfd82f 100644 --- a/version.php +++ b/version.php @@ -22,7 +22,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024112400; +$plugin->version = 20241204004; $plugin->requires = 2022041900; $plugin->cron = 0; $plugin->component = 'qtype_coderunner';