Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
Release Notes

Release 4.4.0.02 (Feature/completion-cache)

Performance:
* Added completion state caching to eliminate redundant DB queries during grade
recalculation and completion reports.

Problem:
* The function `questionnaire_get_completion_state()` executed 2 DB queries per check (load questionnaire record + check response existence). During grade recalculation, this is called for every student on every activity that has an availability condition based on questionnaire completion. On a course with 500 students and 45 dependent activities, this produced ~46,000 queries per regrade.
At 100,000 users this would reach ~9,000,000 queries.

Solution:
* [SPECAPPS-205] New class `\mod_questionnaire\completion_cache` provides two layers of caching:
- Questionnaire record cache: loads each questionnaire's settings once per request, eliminating repeated identical queries across users.
- Lazy bulk preload: on first completion check for a questionnaire, loads ALL completed user IDs in a single `SELECT DISTINCT userid` query. All subsequent checks for any user on the same questionnaire are PHP array lookups with zero DB queries.

Cache invalidation:
* Cache is invalidated on response submit (both initial and resume paths) and response delete, ensuring correctness when completion state changes mid-request.

Files changed:
* classes/completion_cache.php (new) - cache class with check(), clear(), invalidate() methods
* lib.php - questionnaire_get_completion_state() delegates to cache
* questionnaire.class.php - cache invalidation on submit
* locallib.php - cache invalidation on delete
* tests/custom_completion_test.php - setUp() added to clear cache between tests

Release 4.4.0 (Build - 2025110900)
New Features:
* [PR590](https://github.com/PoetOS/moodle-mod_questionnaire/pull/590): Allow responses to be deleted automatically after a specified time. This is disabled by default.
Expand Down
101 changes: 101 additions & 0 deletions classes/completion_cache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace mod_questionnaire;

/**
* Caches questionnaire completion state to avoid repeated DB queries.
*
* On first check for a questionnaire, bulk-loads all completed user IDs
* for that questionnaire into memory. Subsequent checks for any user on
* the same questionnaire are served from the cache with zero DB queries.
*
* Also caches questionnaire records so the same instance isn't loaded
* repeatedly across different users.
*
* @package mod_questionnaire
* @copyright 2026 Jamie Burgess, NSW Department of Education
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class completion_cache {

/** @var array Questionnaire records keyed by instance ID. */
private static $questionnaires = [];

/** @var array Completed user sets keyed by questionnaire ID. Each value is userid => true. */
private static $completed = [];

/**
* Check whether a user has completed a questionnaire.
*
* @param \stdClass $cm Course module object.
* @param int $userid User ID to check.
* @param mixed $type Completion type (returned if completionsubmit is disabled).
* @return bool|mixed True if completed, false if not, $type if completion not enabled.
*/
public static function check($cm, int $userid, $type) {
global $DB;

$instanceid = $cm->instance;

// Load questionnaire record from cache or DB.
if (!isset(self::$questionnaires[$instanceid])) {
self::$questionnaires[$instanceid] = $DB->get_record('questionnaire',
['id' => $instanceid], '*', MUST_EXIST);
}
$questionnaire = self::$questionnaires[$instanceid];

if (!$questionnaire->completionsubmit) {
return $type;
}

$qid = $questionnaire->id;

// Lazy bulk preload: on first check for this questionnaire,
// load ALL completed user IDs in a single query.
if (!isset(self::$completed[$qid])) {
self::$completed[$qid] = [];
$records = $DB->get_records_sql(
'SELECT DISTINCT userid FROM {questionnaire_response} WHERE questionnaireid = ? AND complete = ?',
[$qid, 'y']);
foreach ($records as $record) {
self::$completed[$qid][$record->userid] = true;
}
}

return isset(self::$completed[$qid][$userid]);
}

/**
* Clear all caches.
*
* Call after any operation that changes questionnaire responses
* (submit, delete) within the same request.
*/
public static function clear(): void {
self::$questionnaires = [];
self::$completed = [];
}

/**
* Invalidate the completion cache for a specific questionnaire.
*
* @param int $questionnaireid The questionnaire ID to invalidate.
*/
public static function invalidate(int $questionnaireid): void {
unset(self::$completed[$questionnaireid]);
}
}
14 changes: 1 addition & 13 deletions lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -1232,19 +1232,7 @@ function questionnaire_reset_userdata($data) {
*
*/
function questionnaire_get_completion_state($cm, $userid, $type) {
global $DB;

// Get questionnaire details.
$questionnaire = $DB->get_record('questionnaire', array('id' => $cm->instance), '*', MUST_EXIST);

// If completion option is enabled, evaluate it and return true/false.
if ($questionnaire->completionsubmit) {
$params = ['userid' => $userid, 'questionnaireid' => $questionnaire->id, 'complete' => 'y'];
return $DB->record_exists('questionnaire_response', $params);
} else {
// Completion option is not enabled so just return $type.
return $type;
}
return \mod_questionnaire\completion_cache::check($cm, $userid, $type);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions locallib.php
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,9 @@ function questionnaire_delete_response($response, $questionnaire='') {
$status = $status && $DB->delete_records('questionnaire_response', array('id' => $rid));

if ($status && $cm) {
// Invalidate completion cache after response deletion.
\mod_questionnaire\completion_cache::invalidate($questionnaire->id);

// Update completion state if necessary.
$completion = new completion_info($questionnaire->course);
if ($completion->is_enabled($cm) == COMPLETION_TRACKING_AUTOMATIC && $questionnaire->completionsubmit) {
Expand Down
6 changes: 6 additions & 0 deletions questionnaire.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ public function view() {

$this->update_grades($quser);

// Invalidate completion cache before updating state.
\mod_questionnaire\completion_cache::invalidate($this->id);

// Update completion state.
$completion = new completion_info($this->course);
if ($completion->is_enabled($this->cm) && $this->completionsubmit) {
Expand Down Expand Up @@ -376,6 +379,9 @@ public function commit_submission_response($rid, $quser) {

$this->update_grades($quser);

// Invalidate completion cache before updating state.
\mod_questionnaire\completion_cache::invalidate($this->id);

// Update completion state.
$completion = new \completion_info($this->course);
if ($completion->is_enabled($this->cm) && $this->completionsubmit) {
Expand Down
8 changes: 8 additions & 0 deletions tests/custom_completion_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@
*/
class custom_completion_test extends \advanced_testcase {

/**
* Clear completion cache between tests.
*/
public function setUp(): void {
parent::setUp();
\mod_questionnaire\completion_cache::clear();
}

/**
* Data provider for get_state().
*
Expand Down
4 changes: 2 additions & 2 deletions version.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@

defined('MOODLE_INTERNAL') || die();

$plugin->version = 2024080100.01; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2024080100.02; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2024042200.00; // Moodle version (4.4.0).

$plugin->component = 'mod_questionnaire';

$plugin->release = '4.4.0 (Build - 2025110900)';
$plugin->release = '4.4.0.02 (Feature/completion-cache)';
$plugin->maturity = MATURITY_STABLE;
Loading