This commit is contained in:
Jun Pataleta 2023-01-12 12:06:35 +08:00
commit e0d857f39f
4 changed files with 73 additions and 20 deletions

View file

@ -9307,6 +9307,25 @@ function mtrace($string, $eol="\n", $sleep=0) {
} }
} }
/**
* Helper to {@see mtrace()} an exception or throwable, including all relevant information.
*
* @param Throwable $e the error to ouptput.
*/
function mtrace_exception(Throwable $e): void {
$info = get_exception_info($e);
$message = $info->message;
if ($info->debuginfo) {
$message .= "\n\n" . $info->debuginfo;
}
if ($info->backtrace) {
$message .= "\n\n" . format_backtrace($info->backtrace, true);
}
mtrace($message);
}
/** /**
* Replace 1 or more slashes or backslashes to 1 slash * Replace 1 or more slashes or backslashes to 1 slash
* *

View file

@ -482,11 +482,12 @@ function is_early_init($backtrace) {
/** /**
* Returns detailed information about specified exception. * Returns detailed information about specified exception.
* @param exception $ex *
* @return object * @param Throwable $ex any sort of exception or throwable.
* @return stdClass standardised info to display. Fields are clear if you look at the end of this function.
*/ */
function get_exception_info($ex) { function get_exception_info($ex): stdClass {
global $CFG, $DB, $SESSION; global $CFG;
if ($ex instanceof moodle_exception) { if ($ex instanceof moodle_exception) {
$errorcode = $ex->errorcode; $errorcode = $ex->errorcode;

View file

@ -24,6 +24,7 @@ information provided here is intended especially for developers.
* In outputcomponents.php, initials_bar() can now be rendered in a smaller (mini) way. This provides purely the * In outputcomponents.php, initials_bar() can now be rendered in a smaller (mini) way. This provides purely the
initials bar without the bootstrapping and form handling on each initials bar. If you use this mini render, initials bar without the bootstrapping and form handling on each initials bar. If you use this mini render,
you'll need to implement your own form handling. Example usage can be found within the grader report. you'll need to implement your own form handling. Example usage can be found within the grader report.
* There is a new helper function mtrace_exception to help with reporting exceptions you have caught in scheduled tasks.
=== 4.1 === === 4.1 ===

View file

@ -36,21 +36,33 @@ require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/ */
class recalculate extends \core\task\scheduled_task { class recalculate extends \core\task\scheduled_task {
/** @var int the maximum length of time one instance of this task will run. */
const TIME_LIMIT = 3600;
public function get_name() { public function get_name(): string {
return get_string('recalculatetask', 'quiz_statistics'); return get_string('recalculatetask', 'quiz_statistics');
} }
public function execute() { public function execute(): void {
global $DB; global $DB;
$stoptime = time() + self::TIME_LIMIT;
$dateformat = get_string('strftimedatetimeshortaccurate', 'core_langconfig');
// TODO: MDL-75197, add quizid in quiz_statistics so that it is simpler to find quizzes for stats calculation. // TODO: MDL-75197, add quizid in quiz_statistics so that it is simpler to find quizzes for stats calculation.
// Only calculate stats for quizzes which have recently finished attempt. // Only calculate stats for quizzes which have recently finished attempt.
$sql = " $sql = "
SELECT qa.quiz, MAX(qa.timefinish) as timefinish SELECT q.id AS quizid,
q.name AS quizname,
c.id AS courseid,
c.shortname AS courseshortname,
MAX(qa.timefinish) AS mostrecentattempttime,
COUNT(1) AS numberofattempts
FROM {quiz_attempts} qa FROM {quiz_attempts} qa
JOIN {quiz} q ON q.id = qa.quiz
JOIN {course} c ON c.id = q.course
WHERE qa.preview = 0 WHERE qa.preview = 0
AND qa.state = :quizstatefinished AND qa.state = :quizstatefinished
GROUP BY qa.quiz GROUP BY q.id, q.name, c.id, c.shortname
"; ";
$params = [ $params = [
@ -59,30 +71,50 @@ class recalculate extends \core\task\scheduled_task {
$latestattempts = $DB->get_records_sql($sql, $params); $latestattempts = $DB->get_records_sql($sql, $params);
$anyexception = null;
foreach ($latestattempts as $attempt) { foreach ($latestattempts as $attempt) {
if (time() >= $stoptime) {
mtrace("This task has been running for more than " .
format_time(self::TIME_LIMIT) . " so stopping this execution.");
break;
}
mtrace(" Examining quiz '$attempt->quizname' ($attempt->quizid) in course " .
"$attempt->courseshortname ($attempt->courseid) with most recent attempt at " .
userdate($attempt->mostrecentattempttime, $dateformat) . ".");
$quizobj = quiz_settings::create($attempt->quiz); $quizobj = quiz_settings::create($attempt->quiz);
$quiz = $quizobj->get_quiz(); $quiz = $quizobj->get_quiz();
// Hash code for question stats option in question bank. // Hash code for question stats option in question bank.
$qubaids = quiz_statistics_qubaids_condition($quiz->id, new \core\dml\sql_join(), $quiz->grademethod); $qubaids = quiz_statistics_qubaids_condition($quiz->id, new \core\dml\sql_join(), $quiz->grademethod);
// Check if there is any existing question stats, and it has been calculated after latest quiz attempt. // Check if there is any existing question stats, and it has been calculated after latest quiz attempt.
$records = $DB->get_records_select( $lateststatstime = $DB->get_field('quiz_statistics', 'COALESCE(MAX(timemodified), 0)',
'quiz_statistics', ['hashcode' => $qubaids->get_hash_code()]);
'hashcode = :hashcode AND timemodified > :timefinish',
[
'hashcode' => $qubaids->get_hash_code(),
'timefinish' => $attempt->timefinish
]
);
if (empty($records)) { if ($lateststatstime >= $attempt->mostrecentattempttime) {
mtrace(" Statistics already calculated at " . userdate($lateststatstime, $dateformat) .
" so nothing to do for this quiz.");
continue;
}
mtrace(" Calculating statistics for $attempt->numberofattempts attempts, starting at " .
userdate(time(), $dateformat) . " ...");
try {
$report = new quiz_statistics_report(); $report = new quiz_statistics_report();
// Clear old cache.
$report->clear_cached_data($qubaids); $report->clear_cached_data($qubaids);
// Calculate new stats.
$report->calculate_questions_stats_for_question_bank($quiz->id); $report->calculate_questions_stats_for_question_bank($quiz->id);
mtrace(" Calculations completed at " . userdate(time(), $dateformat) . ".");
} catch (\Throwable $e) {
// We don't want an exception from one quiz to stop processing of other quizzes.
mtrace_exception($e);
$anyexception = $e;
} }
} }
return true;
if ($anyexception) {
// If there was any error, ensure the task fails.
throw $anyexception;
}
} }
} }