From 7e3e2077f09414621f4ae9a62af353b9098a0d01 Mon Sep 17 00:00:00 2001 From: Mi Jia Looi Date: Wed, 2 Jul 2025 12:58:08 +1000 Subject: [PATCH] MDL-73845 enrol_database: Add lock to prevent race condition --- enrol/database/db/upgrade.php | 51 +++++++++++++++++++++++++++++++++++ enrol/database/lib.php | 33 +++++++++++++++++++++-- enrol/database/version.php | 2 +- 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/enrol/database/db/upgrade.php b/enrol/database/db/upgrade.php index b30cb9418b3..e1b606ba6cc 100644 --- a/enrol/database/db/upgrade.php +++ b/enrol/database/db/upgrade.php @@ -23,6 +23,7 @@ */ function xmldb_enrol_database_upgrade($oldversion) { + global $DB; // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. @@ -38,5 +39,55 @@ function xmldb_enrol_database_upgrade($oldversion) { // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2024100701) { + // Remove duplicated enrolment records, keeping only the earliest records. + $transaction = $DB->start_delegated_transaction(); + $courses = $DB->get_records_sql( + " + SELECT courseid + FROM {enrol} + WHERE enrol = 'database' + GROUP BY courseid + HAVING COUNT(*) > 1" + ); + foreach ($courses as $course) { + $instances = $DB->get_records('enrol', ['enrol' => 'database', 'courseid' => $course->courseid], 'id ASC'); + $idtokeep = array_key_first($instances); + $idstodelete = array_slice(array_keys($instances), 1); + [$insql, $inparams] = $DB->get_in_or_equal($idstodelete, SQL_PARAMS_NAMED); + + // Migrate enrolments where possible. + $DB->execute( + " + UPDATE {user_enrolments} ue + SET enrolid = :idtokeep1 + WHERE ue.enrolid $insql + AND NOT EXISTS ( + SELECT 1 + FROM {user_enrolments} ue2 + WHERE ue2.userid = ue.userid + AND ue2.enrolid = :idtokeep2)", + array_merge($inparams, ['idtokeep1' => $idtokeep, 'idtokeep2' => $idtokeep]) + ); + + $DB->delete_records_select('user_enrolments', "enrolid $insql", $inparams); + + // Migrate role assignments. + $DB->execute( + " + UPDATE {role_assignments} + SET itemid = :idtokeep + WHERE component = :component + AND itemid $insql", + array_merge($inparams, ['component' => 'enrol_database', 'idtokeep' => $idtokeep]) + ); + $DB->delete_records_select('role_assignments', "itemid $insql", $inparams); + + $DB->delete_records_list('enrol', 'id', $idstodelete); + } + $transaction->allow_commit(); + upgrade_plugin_savepoint(true, 2024100701, 'enrol', 'database'); + } + return true; } diff --git a/enrol/database/lib.php b/enrol/database/lib.php index ec3d8d65e64..5b2173189f1 100644 --- a/enrol/database/lib.php +++ b/enrol/database/lib.php @@ -186,8 +186,37 @@ class enrol_database_plugin extends enrol_plugin { continue; } - $enrolid = $this->add_instance($course); - $instances[$course->id] = $DB->get_record('enrol', array('id'=>$enrolid)); + $timeout = 5; + $locktype = 'enrol_database_user_enrolments'; + $resource = 'course:' . $course->id; + $lockfactory = \core\lock\lock_config::get_lock_factory($locktype); + if ($lock = $lockfactory->get_lock($resource, $timeout)) { + try { + $instance = $DB->get_record('enrol', ['enrol' => 'database', 'courseid' => $course->id]); + if (!$instance) { + $enrolid = $this->add_instance($course); + $instance = $DB->get_record('enrol', ['id' => $enrolid]); + } + } finally { + $lock->release(); + } + } else { + // Attempt to reuse an existing record added by another process during race condition. + if ($instance = $DB->get_record('enrol', ['enrol' => 'database', 'courseid' => $course->id])) { + $instances[$course->id] = $instance; + continue; + } else { + // Give up. + throw new moodle_exception( + 'locktimeout', + 'enrol_database', + '', + null, + 'Could not create database enrolment instance for course ' . $course->id + ); + } + } + $instances[$course->id] = $instance; } } $rs->Close(); diff --git a/enrol/database/version.php b/enrol/database/version.php index 08a673e8be6..da6107c7df7 100644 --- a/enrol/database/version.php +++ b/enrol/database/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024100700; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2024100701; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2024100100; // Requires this Moodle version. $plugin->component = 'enrol_database'; // Full name of the plugin (used for diagnostics)