MDL-20625 new delegated transaction support in DML

This commit is contained in:
Petr Skoda 2009-11-07 08:52:56 +00:00
parent 88d39d6992
commit d5a8d9aa71
25 changed files with 797 additions and 449 deletions

View File

@ -796,7 +796,7 @@ if ( !is_object($PHPCAS_CLIENT) ) {
$creatorrole = false;
}
$DB->begin_sql();
$transaction = $DB->start_delegated_transaction();
$xcount = 0;
$maxxcount = 100;
@ -816,14 +816,8 @@ if ( !is_object($PHPCAS_CLIENT) ) {
role_unassign($creatorrole->id, $user->id, 0, $sitecontext->id, 'cas');
}
}
if ($xcount++ > $maxxcount) {
$DB->commit_sql();
$DB->begin_sql();
$xcount = 0;
}
}
$DB->commit_sql();
$transaction->allow_commit();
unset($users); // free mem
}
} else { // end do updates
@ -851,7 +845,7 @@ if ( !is_object($PHPCAS_CLIENT) ) {
$creatorrole = false;
}
$DB->begin_sql();
$transaction = $DB->start_delegated_transaction();
foreach ($add_users as $user) {
$user = $this->get_userinfo_asobj($user->username);
@ -879,7 +873,7 @@ if ( !is_object($PHPCAS_CLIENT) ) {
role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'cas');
}
}
$DB->commit_sql();
$transaction->allow_commit();
unset($add_users); // free mem
} else {
print "No users to be added\n";

View File

@ -344,7 +344,7 @@ class auth_plugin_db extends auth_plugin_base {
if (!empty($add_users)) {
print_string('auth_dbuserstoadd','auth_db',count($add_users)); echo "\n";
$DB->begin_sql();
$transaction = $DB->start_delegated_transaction();
foreach($add_users as $user) {
$username = $user;
$user = $this->get_userinfo_asobj($user);
@ -375,7 +375,7 @@ class auth_plugin_db extends auth_plugin_base {
echo "\t"; print_string('auth_dbinsertusererror', 'auth_db', $user->username); echo "\n";
}
}
$DB->commit_sql();
$transaction->allow_commit();
unset($add_users); // free mem
}
return true;

View File

@ -729,7 +729,7 @@ class auth_plugin_ldap extends auth_plugin_base {
$creatorrole = false;
}
$DB->begin_sql();
$transaction = $DB->start_delegated_transaction();
$xcount = 0;
$maxxcount = 100;
@ -749,14 +749,8 @@ class auth_plugin_ldap extends auth_plugin_base {
role_unassign($creatorrole->id, $user->id, 0, $sitecontext->id, 'ldap');
}
}
if ($xcount++ > $maxxcount) {
$DB->commit_sql();
$DB->begin_sql();
$xcount = 0;
}
}
$DB->commit_sql();
$transaction->allow_commit();
unset($users); // free mem
}
} else { // end do updates
@ -785,7 +779,7 @@ class auth_plugin_ldap extends auth_plugin_base {
$creatorrole = false;
}
$DB->begin_sql();
$transaction = $DB->start_delegated_transaction();
foreach ($add_users as $user) {
$user = $this->get_userinfo_asobj($user->username);
@ -816,7 +810,7 @@ class auth_plugin_ldap extends auth_plugin_base {
role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'ldap');
}
}
$DB->commit_sql();
$transaction->allow_commit();
unset($add_users); // free mem
} else {
print "No users to be added\n";

View File

@ -854,7 +854,7 @@ class auth_plugin_mnet extends auth_plugin_base {
$start = ob_start();
$returnString = '';
$DB->begin_sql();
$transaction = $DB->start_delegated_transaction();
$useridarray = array();
foreach($array as $logEntry) {
@ -883,7 +883,7 @@ class auth_plugin_mnet extends auth_plugin_base {
}
}
$MNET_REMOTE_CLIENT->commit();
$DB->commit_sql();
$transaction->allow_commit();
$end = ob_end_clean();

View File

@ -219,7 +219,8 @@ function sync_enrolments($role = null) {
return true;
}
$DB->begin_sql();
$transaction = $DB->start_delegated_transaction();
$extcourses = array();
while ($extcourse_obj = (object)$rs->FetchRow()) { // there are more course records
$extcourse_obj = (object)array_change_key_case((array)$extcourse_obj , CASE_LOWER);
@ -416,7 +417,7 @@ function sync_enrolments($role = null) {
$ers->close(); // release the handle
}
$DB->commit_sql();
$transaction->allow_commit();
// we are done now, a bit of housekeeping
fix_course_sortorder();

View File

@ -49,11 +49,11 @@ if ($confirm && data_submitted()) {
if (!confirm_sesskey() ) {
print_error('confirmsesskeybad','error',$returnurl);
}
$DB->begin_sql();
foreach($groupidarray as $groupid) {
groups_delete_group($groupid);
}
$DB->commit_sql();
redirect($returnurl);
} else {
$PAGE->set_title(get_string('deleteselectedgroup', 'group'));

View File

@ -60,36 +60,31 @@ class moodle_group_external extends external_api {
$params = self::validate_parameters(self::create_groups_parameters(), array('groups'=>$groups));
// ideally create all groups or none at all, unfortunately myisam engine does not support transactions :-(
$DB->begin_sql();
try {
//TODO: there is a potential problem with events propagating actions to external systems :-(
$groups = array();
$transaction = $DB->start_delegated_transaction();
foreach ($params['groups'] as $group) {
$group = (object)$group;
$groups = array();
if (trim($group->name) == '') {
throw new invalid_parameter_exception('Invalid group name');
}
if ($DB->get_record('groups', array('courseid'=>$group->courseid, 'name'=>$group->name))) {
throw new invalid_parameter_exception('Group with the same name already exists in the course');
}
foreach ($params['groups'] as $group) {
$group = (object)$group;
// now security checks
$context = get_context_instance(CONTEXT_COURSE, $group->courseid);
self::validate_context($context);
require_capability('moodle/course:managegroups', $context);
// finally create the group
$group->id = groups_create_group($group, false);
$groups[] = (array)$group;
if (trim($group->name) == '') {
throw new invalid_parameter_exception('Invalid group name');
}
} catch (Exception $ex) {
$DB->rollback_sql();
throw $ex;
if ($DB->get_record('groups', array('courseid'=>$group->courseid, 'name'=>$group->name))) {
throw new invalid_parameter_exception('Group with the same name already exists in the course');
}
// now security checks
$context = get_context_instance(CONTEXT_COURSE, $group->courseid);
self::validate_context($context);
require_capability('moodle/course:managegroups', $context);
// finally create the group
$group->id = groups_create_group($group, false);
$groups[] = (array)$group;
}
$DB->commit_sql();
$transaction->allow_commit();
return $groups;
}
@ -242,30 +237,27 @@ class moodle_group_external extends external_api {
$params = self::validate_parameters(self::delete_groups_parameters(), array('groupids'=>$groupids));
$DB->begin_sql();
try {
$transaction = $DB->start_delegated_transaction();
// TODO: this is problematic because the DB rollback does not handle deleting of images!!
// there is also potential problem with events propagating action to external systems :-(
foreach ($params['groupids'] as $groupid) {
// validate params
$groupid = validate_param($groupid, PARAM_INTEGER);
if (!$group = groups_get_group($groupid, 'id, courseid', IGNORE_MISSING)) {
// silently ignore attempts to delete nonexisting groups
continue;
}
// now security checks
$context = get_context_instance(CONTEXT_COURSE, $group->courseid);
self::validate_context($context);
require_capability('moodle/course:managegroups', $context);
groups_delete_group($group);
foreach ($params['groupids'] as $groupid) {
// validate params
$groupid = validate_param($groupid, PARAM_INTEGER);
if (!$group = groups_get_group($groupid, 'id, courseid', IGNORE_MISSING)) {
// silently ignore attempts to delete nonexisting groups
continue;
}
} catch (Exception $ex) {
$DB->rollback_sql();
throw $ex;
// now security checks
$context = get_context_instance(CONTEXT_COURSE, $group->courseid);
self::validate_context($context);
require_capability('moodle/course:managegroups', $context);
groups_delete_group($group);
}
$DB->commit_sql();
$transaction->allow_commit();
}
/**
@ -361,33 +353,29 @@ class moodle_group_external extends external_api {
$params = self::validate_parameters(self::add_groupmembers_parameters(), array('members'=>$members));
$DB->begin_sql();
try {
// TODO: there is a potential problem with events propagating action to external systems :-(
foreach ($params['members'] as $member) {
// validate params
$groupid = $member['groupid'];
$userid = $member['userid'];
$transaction = $DB->start_delegated_transaction();
// TODO: there is a potential problem with events propagating action to external systems :-(
foreach ($params['members'] as $member) {
// validate params
$groupid = $member['groupid'];
$userid = $member['userid'];
$group = groups_get_group($groupid, 'id, courseid', MUST_EXIST);
$user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
$group = groups_get_group($groupid, 'id, courseid', MUST_EXIST);
$user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
// now security checks
$context = get_context_instance(CONTEXT_COURSE, $group->courseid);
self::validate_context($context);
require_capability('moodle/course:managegroups', $context);
// now security checks
$context = get_context_instance(CONTEXT_COURSE, $group->courseid);
self::validate_context($context);
require_capability('moodle/course:managegroups', $context);
// now make sure user is enrolled in course - this is mandatory requirement,
// unfortunately this is extermely slow
require_capability('moodle/course:view', $context, $userid, false);
// now make sure user is enrolled in course - this is mandatory requirement,
// unfortunately this is extermely slow
require_capability('moodle/course:view', $context, $userid, false);
groups_add_member($group, $user);
}
} catch (Exception $ex) {
$DB->rollback_sql();
throw $ex;
groups_add_member($group, $user);
}
$DB->commit_sql();
$transaction->allow_commit();
}
/**
@ -429,29 +417,26 @@ class moodle_group_external extends external_api {
$params = self::validate_parameters(self::delete_groupmembers_parameters(), array('members'=>$members));
$DB->begin_sql();
try {
$transaction = $DB->start_delegated_transaction();
// TODO: there is a potential problem with events propagating action to external systems :-(
foreach ($params['members'] as $member) {
// validate params
$groupid = $member['groupid'];
$userid = $member['userid'];
// validate params
$groupid = $member['groupid'];
$userid = $member['userid'];
$group = groups_get_group($groupid, 'id, courseid', MUST_EXIST);
$user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
$group = groups_get_group($groupid, 'id, courseid', MUST_EXIST);
$user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
// now security checks
$context = get_context_instance(CONTEXT_COURSE, $group->courseid);
self::validate_context($context);
require_capability('moodle/course:managegroups', $context);
// now security checks
$context = get_context_instance(CONTEXT_COURSE, $group->courseid);
self::validate_context($context);
require_capability('moodle/course:managegroups', $context);
groups_remove_member($group, $user);
}
} catch (Exception $ex) {
$DB->rollback_sql();
throw $ex;
groups_remove_member($group, $user);
}
$DB->commit_sql();
$transaction->allow_commit();
}
/**

View File

@ -178,6 +178,7 @@ $string['ddltablenotexist'] = 'Table \"$a\" does not exist';
$string['ddlunknownerror'] = 'Unknown DDL library error';
$string['ddlxmlfileerror'] = 'XML database file errors found';
$string['dmlreadexception'] = 'Error reading from database';
$string['dmltransactionexception'] = 'Database transaction error';
$string['dmlwriteexception'] = 'Error writing to database';
$string['destinationcmnotexit'] = 'The destination course module does not exist';
$string['detectedbrokenplugin'] = 'Plugin \"$a\" is defective, can not continue, sorry.';

View File

@ -2455,24 +2455,18 @@ function cleanup_contexts() {
ON c.instanceid = t.id
WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK."
";
// transactions used only for performance reasons here
$transaction = $DB->start_delegated_transaction();
if ($rs = $DB->get_recordset_sql($sql)) {
$DB->begin_sql();
$ok = true;
foreach ($rs as $ctx) {
if (!delete_context($ctx->contextlevel, $ctx->instanceid)) {
$ok = false;
break;
}
delete_context($ctx->contextlevel, $ctx->instanceid);
}
$rs->close();
if ($ok) {
$DB->commit_sql();
return true;
} else {
$DB->rollback_sql();
return false;
}
}
$transaction->allow_commit();
return true;
}

View File

@ -27,6 +27,7 @@
require_once($CFG->libdir.'/dml/database_column_info.php');
require_once($CFG->libdir.'/dml/moodle_recordset.php');
require_once($CFG->libdir.'/dml/moodle_transaction.php');
/// GLOBAL CONSTANTS /////////////////////////////////////////////////////////
@ -109,8 +110,10 @@ abstract class moodle_database {
/** @var bool true if db used for db sessions */
protected $used_for_db_sessions = false;
/** @var bool Flag indicating transaction in progress */
protected $intransaction = false;
/** @var array open transactions */
private $transactions = array();
/** @var bool force rollback of all current transactions */
private $force_rollback = false;
/** @var int internal temporary variable */
private $fix_sql_params_i;
@ -281,8 +284,10 @@ abstract class moodle_database {
* Do NOT use connect() again, create a new instance if needed.
*/
public function dispose() {
if ($this->intransaction) {
// unfortunately we can not access global $CFG any more and can not print debug
if ($this->transactions) {
// unfortunately we can not access global $CFG any more and can not print debug,
// the diagnostic info should be printed in footer instead
$this->force_transaction_rollback();
error_log('Active database transaction detected when disposing database!');
}
if ($this->used_for_db_sessions) {
@ -1883,59 +1888,181 @@ abstract class moodle_database {
}
/// transactions
/**
* Are transactions supported?
* It is not responsible to run productions servers
* on databases without transaction support ;-)
*
* Override in driver if needed.
*
* @return bool
*/
protected function transactions_supported() {
// protected for now, this might be changed to public if really necessary
return true;
}
/**
* Returns true if transaction in progress
* @return bool
*/
function is_transaction_started() {
return $this->intransaction;
public function is_transaction_started() {
return !empty($this->transactions);
}
/**
* on DBs that support it, switch to transaction mode and begin a transaction
* you'll need to ensure you call commit_sql() or your changes *will* be lost.
* Throws exception if transaction in progress.
* This test does not force rollback of active transactions.
* @return void
*/
public function transactions_forbidden() {
if ($this->is_transaction_started()) {
throw new dml_transaction_exception('This code can not be excecuted in transaction');
}
}
/**
* On DBs that support it, switch to transaction mode and begin a transaction
* you'll need to ensure you call commit() or your changes *will* be lost.
*
* this is _very_ useful for massive updates
*
* Please note only one level of transactions is supported, please do not use
* transaction in moodle core! Transaction are intended for web services
* enrolment and auth synchronisation scripts, etc.
* Delegated database transactions can be nested, but only one actual database
* transaction is used for the outer-most delegated transaction. This method
* returns a transaction object which you should keep until the end of the
* delegated transaction. The actual database transaction will
* only be committed if all the nested delegated transactions commit
* successfully. If any part of the transaction rolls back then the whole
* thing is rolled back.
*
* @return bool success
* @return moodle_transaction
*/
public function begin_sql() {
if ($this->intransaction) {
debugging('Transaction already in progress');
return false;
public function start_delegated_transaction() {
$transaction = new moodle_transaction($this);
$this->transactions[] = $transaction;
if (count($this->transactions) == 1) {
$this->begin_transaction();
}
$this->intransaction = true;
return true;
return $transaction;
}
/**
* on DBs that support it, commit the transaction
* @return bool success
* Driver specific start of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function commit_sql() {
if (!$this->intransaction) {
debugging('Transaction not in progress');
return false;
protected abstract function begin_transaction();
/**
* Indicates delegated transaction finished successfully.
* The real database transaction is committed only if
* all delegated transactions committed.
* @return void
*/
public function commit_delegated_transaction(moodle_transaction $transaction) {
if ($transaction->is_disposed()) {
throw new dml_transaction_exception('Transactions already disposed', $transaction);
}
$this->intransaction = false;
return true;
// mark as disposed so that it can not be used again
$transaction->dispose();
if (empty($this->transactions)) {
throw new dml_transaction_exception('Transaction not started', $transaction);
}
if ($this->force_rollback) {
throw new dml_transaction_exception('Tried to commit transaction after lower level rollback', $transaction);
}
if ($transaction !== $this->transactions[count($this->transactions) - 1]) {
// one incorrect commit at any level rollbacks everything
$this->force_rollback = true;
throw new dml_transaction_exception('Invalid transaction commit attempt', $transaction);
}
if (count($this->transactions) == 1) {
// only commit the top most level
$this->commit_transaction();
}
array_pop($this->transactions);
}
/**
* on DBs that support it, rollback the transaction
* @return bool success
* Driver specific commit of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function rollback_sql() {
if (!$this->intransaction) {
debugging('Transaction not in progress');
return false;
protected abstract function commit_transaction();
/**
* Call when delegated transaction failed, this rolls back
* all delegated transactions up to the top most level.
*
* In many cases you do not need to call this method manually,
* because all open delegated transactions are rolled back
* automatically if exceptions not caucht.
*
* @param moodle_transaction $transaction
* @param Exception $e exception that caused the problem
* @return does not return, exception is rethrown
*/
public function rollback_delegated_transaction(moodle_transaction $transaction, Exception $e) {
if ($transaction->is_disposed()) {
throw new dml_transaction_exception('Transactions already disposed', $transaction);
}
$this->intransaction = false;
return true;
// mark as disposed so that it can not be used again
$transaction->dispose();
// one rollback at any level rollbacks everything
$this->force_rollback = true;
if (empty($this->transactions) or $transaction !== $this->transactions[count($this->transactions) - 1]) {
// this may or may not be a coding problem, better just rethrow the exception,
// because we do not want to loose the original $e
throw $e;
}
if (count($this->transactions) == 1) {
// only rollback the top most level
$this->rollback_transaction();
}
array_pop($this->transactions);
if (empty($this->transactions)) {
// finally top most level rolled back
$this->force_rollback = false;
}
throw $e;
}
/**
* Driver specific bort of real database transaction,
* this can not be used directly in code.
* @return void
*/
protected abstract function rollback_transaction();
/**
* Force rollback of all delegted transaction.
* Does not trow any exceptions and does not log anything.
*
* This method should be used only from default exception handlers and other
* core code.
*
* @return void
*/
public function force_transaction_rollback() {
if ($this->transactions) {
try {
$this->rollback_transaction();
} catch (dml_exception $e) {
// ignore any sql errors here, the connection might be broken
}
}
// now enable transactions again
$this->transactions = array(); // unfortunately all unfinished exceptions are kept in memory
$this->force_rollback = false;
}
/// session locking

View File

@ -0,0 +1,93 @@
<?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/>.
/**
* Delegated database transaction support.
*
* @package moodlecore
* @subpackage DML
* @copyright 2009 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Delegated transaction class.
*/
class moodle_transaction {
private $start_backtrace;
private $database = null;
/**
* Delegated transaction constructor,
* can be called only from moodle_database class.
* Unfortunately PHP's protected keyword is useless.
* @param moodle_database $database
*/
public function __construct($database) {
$this->database = $database;
$this->start_backtrace = debug_backtrace();
}
/**
* Is the delegated transaction already used?
* @return bool true if commit and rollback allowed, false if already done
*/
public function is_disposed() {
return empty($this->database);
}
/**
* Mark transaction as disposed, no more
* commits and rollbacks allowed.
* To be used only from moodle_database class
* @return unknown_type
*/
public function dispose() {
return $this->database = null;
}
/**
* Commit delegated transaction.
* The real database commit SQL is executed
* only after commiting all delegated transactions.
*
* Incorrect order of nested commits or rollback
* at any level is resulting in rollback of SQL transaction.
*
* @return void
*/
public function allow_commit() {
if ($this->is_disposed()) {
throw new dml_transaction_exception('Transactions already disposed', $this);
}
$this->database->commit_delegated_transaction($this);
}
/**
* Rollback all current delegated transactions.
*
* @param Exception $e mandatory exception
* @return void
*/
public function rollback(Exception $e) {
if ($this->is_disposed()) {
throw new dml_transaction_exception('Transactions already disposed', $this);
}
$this->database->rollback_delegated_transaction($this, $e);
}
}

View File

@ -1199,56 +1199,44 @@ s only returning name of SQL substring function, it now requires all parameters.
/// transactions
/**
* on DBs that support it, switch to transaction mode and begin a transaction
* you'll need to ensure you call commit_sql() or your changes *will* be lost.
*
* this is _very_ useful for massive updates
* Driver specific start of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function begin_sql() {
if (!parent::begin_sql()) {
return false;
}
protected function begin_transaction() {
$sql = "BEGIN TRANSACTION"; // Will be using READ COMMITTED isolation
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = mssql_query($sql, $this->mssql);
$this->query_end($result);
$this->free_result($result);
return true;
}
/**
* on DBs that support it, commit the transaction
* Driver specific commit of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function commit_sql() {
if (!parent::commit_sql()) {
return false;
}
protected function commit_transaction() {
$sql = "COMMIT TRANSACTION";
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = mssql_query($sql, $this->mssql);
$this->query_end($result);
$this->free_result($result);
return true;
}
/**
* on DBs that support it, rollback the transaction
* Driver specific abort of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function rollback_sql() {
if (!parent::rollback_sql()) {
return false;
}
protected function rollback_transaction() {
$sql = "ROLLBACK TRANSACTION";
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = mssql_query($sql, $this->mssql);
$this->query_end($result);
$this->free_result($result);
return true;
}
}

View File

@ -37,6 +37,8 @@ class mysqli_native_moodle_database extends moodle_database {
protected $mysqli = null;
private $temptables; // Control existing temptables (mysql_moodle_temptables object)
private $transactions_supported = null;
/**
* Attempt to create the database
* @param string $dbhost
@ -1039,28 +1041,45 @@ class mysqli_native_moodle_database extends moodle_database {
/// transactions
/**
* on DBs that support it, switch to transaction mode and begin a transaction
* you'll need to ensure you call commit_sql() or your changes *will* be lost.
* Are transactions supported?
* It is not responsible to run productions servers
* on databases without transaction support ;-)
*
* this is _very_ useful for massive updates
* MyISAM does not support support transactions.
*
* @return bool
*/
public function begin_sql() {
protected function transactions_supported() {
if (!is_null($this->transactions_supported)) {
return $this->transactions_supported;
}
// Only will accept transactions if using InnoDB storage engine (more engines can be added easily BDB, Falcon...)
$this->transactions_supported = false;
$sql = "SELECT @@storage_engine";
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = $this->mysqli->query($sql);
$this->query_end($result);
if ($rec = $result->fetch_assoc()) {
if (!in_array($rec['@@storage_engine'], array('InnoDB'))) {
return false;
if (in_array($rec['@@storage_engine'], array('InnoDB'))) {
$this->transactions_supported = true;
}
} else {
return false;
}
$result->close();
if (!parent::begin_sql()) {
return false;
return $this->transactions_supported;
}
/**
* Driver specific start of real database transaction,
* this can not be used directly in code.
* @return void
*/
protected function begin_transaction() {
if (!$this->transactions_supported()) {
return;
}
$sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED";
@ -1072,32 +1091,34 @@ class mysqli_native_moodle_database extends moodle_database {
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = $this->mysqli->query($sql);
$this->query_end($result);
return true;
}
/**
* on DBs that support it, commit the transaction
* Driver specific commit of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function commit_sql() {
if (!parent::commit_sql()) {
return false;
protected function commit_transaction() {
if (!$this->transactions_supported()) {
return;
}
$sql = "COMMIT";
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = $this->mysqli->query($sql);
$this->query_end($result);
return true;
}
/**
* on DBs that support it, rollback the transaction
* Driver specific abort of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function rollback_sql() {
if (!parent::rollback_sql()) {
return false;
protected function rollback_transaction() {
if (!$this->transactions_supported()) {
return;
}
$sql = "ROLLBACK";
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = $this->mysqli->query($sql);

View File

@ -1527,46 +1527,35 @@ class oci_native_moodle_database extends moodle_database {
/// transactions
/**
* on DBs that support it, switch to transaction mode and begin a transaction
* you'll need to ensure you call commit_sql() or your changes *will* be lost.
*
* this is _very_ useful for massive updates
* Driver specific start of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function begin_sql() {
if (!parent::begin_sql()) {
return false;
}
protected function begin_transaction() {
$this->commit_status = OCI_DEFAULT; //Done! ;-)
return true;
}
/**
* on DBs that support it, commit the transaction
* Driver specific commit of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function commit_sql() {
if (!parent::commit_sql()) {
return false;
}
protected function commit_transaction() {
$this->query_start('--oracle_commit', NULL, SQL_QUERY_AUX);
$result = oci_commit($this->oci);
$this->commit_status = OCI_COMMIT_ON_SUCCESS;
$this->query_end($result);
return true;
}
/**
* on DBs that support it, rollback the transaction
* Driver specific abort of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function rollback_sql() {
if (!parent::rollback_sql()) {
return false;
}
protected function rollback_transaction() {
$this->query_start('--oracle_rollback', NULL, SQL_QUERY_AUX);
$result = oci_rollback($this->oci);
$this->commit_status = OCI_COMMIT_ON_SUCCESS;
$this->query_end($result);
return true;
}
}

View File

@ -542,57 +542,36 @@ abstract class pdo_moodle_database extends moodle_database {
print_error('TODO');
}
public function begin_sql() {
if (!parent::begin_sql()) {
return false;
}
protected function begin_transaction() {
$this->query_start('', NULL, SQL_QUERY_AUX);
$result = true;
try {
$this->pdb->beginTransaction();
} catch(PDOException $ex) {
$this->lastError = $ex->getMessage();
$result = false;
}
$this->query_end($result);
return $result;
}
public function commit_sql() {
if (!parent::commit_sql()) {
return false;
}
protected function commit_transaction() {
$this->query_start('', NULL, SQL_QUERY_AUX);
$result = true;
try {
$this->pdb->commit();
} catch(PDOException $ex) {
$this->lastError = $ex->getMessage();
$result = false;
}
$this->query_end($result);
return $result;
}
public function rollback_sql() {
if (!parent::rollback_sql()) {
return false;
}
protected function rollback_transaction() {
$this->query_start('', NULL, SQL_QUERY_AUX);
$result = true;
try {
$this->pdb->rollBack();
} catch(PDOException $ex) {
$this->lastError = $ex->getMessage();
$result = false;
}
$this->query_end($result);
return $result;
}
/**

View File

@ -1122,54 +1122,45 @@ class pgsql_native_moodle_database extends moodle_database {
/// transactions
/**
* on DBs that support it, switch to transaction mode and begin a transaction
* you'll need to ensure you call commit_sql() or your changes *will* be lost.
*
* this is _very_ useful for massive updates
* Driver specific start of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function begin_sql() {
if (!parent::begin_sql()) {
return false;
}
protected function begin_transaction() {
$sql = "BEGIN ISOLATION LEVEL READ COMMITTED";
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = pg_query($this->pgsql, $sql);
$this->query_end($result);
pg_free_result($result);
return true;
}
/**
* on DBs that support it, commit the transaction
* Driver specific commit of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function commit_sql() {
if (!parent::commit_sql()) {
return false;
}
protected function commit_transaction() {
$sql = "COMMIT";
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = pg_query($this->pgsql, $sql);
$this->query_end($result);
pg_free_result($result);
return true;
}
/**
* on DBs that support it, rollback the transaction
* Driver specific abort of real database transaction,
* this can not be used directly in code.
* @return void
*/
public function rollback_sql() {
if (!parent::rollback_sql()) {
return false;
}
protected function rollback_transaction() {
$sql = "ROLLBACK";
$this->query_start($sql, NULL, SQL_QUERY_AUX);
$result = pg_query($this->pgsql, $sql);
$this->query_end($result);
pg_free_result($result);
return true;
}
/**

View File

@ -2297,81 +2297,6 @@ class dml_test extends UnitTestCase {
}
function test_begin_sql() {
$DB = $this->tdb;
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
$this->tables[$tablename] = $table;
$active = $DB->begin_sql();
if ($active) {
// test only if driver supports transactions
$data = (object)array('course'=>3);
$DB->insert_record($tablename, $data);
$this->assertEqual(1, $DB->count_records($tablename));
$DB->commit_sql();
} else {
$this->assertTrue(true, 'DB Transactions not supported. Test skipped');
}
}
function test_commit_sql() {
$DB = $this->tdb;
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
$this->tables[$tablename] = $table;
$active = $DB->begin_sql();
if ($active) {
// test only if driver supports transactions
$data = (object)array('course'=>3);
$DB->insert_record($tablename, $data);
$DB->commit_sql();
$this->assertEqual(1, $DB->count_records($tablename));
} else {
$this->assertTrue(true, 'BD Transactions not supported. Test skipped');
}
}
function test_rollback_sql() {
$DB = $this->tdb;
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
$this->tables[$tablename] = $table;
$active = $DB->begin_sql();
if ($active) {
// test only if driver supports transactions
$data = (object)array('course'=>3);
$DB->insert_record($tablename, $data);
$DB->rollback_sql();
$this->assertEqual(0, $DB->count_records($tablename));
} else {
$this->assertTrue(true, 'DB Transactions not supported. Test skipped');
}
}
/**
* Test some more complex SQL syntax which moodle uses and depends on to work
* useful to determine if new database libraries can be supported.
@ -2407,6 +2332,254 @@ class dml_test extends UnitTestCase {
$this->assertEqual(1, reset($records)->id);
$this->assertEqual(2, next($records)->id);
}
function test_onelevel_commit() {
$DB = $this->tdb;
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
$this->tables[$tablename] = $table;
$transaction = $DB->start_delegated_transaction();
$data = (object)array('course'=>3);
$this->assertEqual(0, $DB->count_records($tablename));
$DB->insert_record($tablename, $data);
$this->assertEqual(1, $DB->count_records($tablename));
$transaction->allow_commit();
$this->assertEqual(1, $DB->count_records($tablename));
}
function test_onelevel_rollback() {
$DB = $this->tdb;
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
$this->tables[$tablename] = $table;
// this might in fact encourage ppl to migrate from myisam to innodb
$transaction = $DB->start_delegated_transaction();
$data = (object)array('course'=>3);
$this->assertEqual(0, $DB->count_records($tablename));
$DB->insert_record($tablename, $data);
$this->assertEqual(1, $DB->count_records($tablename));
try {
$transaction->rollback(new Exception('test'));
$this->fail('transaction rollback must rethrow exception');
} catch (Exception $e) {
}
$this->assertEqual(0, $DB->count_records($tablename));
}
function test_nested_transactions() {
$DB = $this->tdb;
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
$this->tables[$tablename] = $table;
// two level commit
$this->assertFalse($DB->is_transaction_started());
$transaction1 = $DB->start_delegated_transaction();
$this->assertTrue($DB->is_transaction_started());
$data = (object)array('course'=>3);
$DB->insert_record($tablename, $data);
$transaction2 = $DB->start_delegated_transaction();
$data = (object)array('course'=>4);
$DB->insert_record($tablename, $data);
$transaction2->allow_commit();
$this->assertTrue($DB->is_transaction_started());
$transaction1->allow_commit();
$this->assertFalse($DB->is_transaction_started());
$this->assertEqual(2, $DB->count_records($tablename));
$DB->delete_records($tablename);
// rollback from top level
$transaction1 = $DB->start_delegated_transaction();
$data = (object)array('course'=>3);
$DB->insert_record($tablename, $data);
$transaction2 = $DB->start_delegated_transaction();
$data = (object)array('course'=>4);
$DB->insert_record($tablename, $data);
$transaction2->allow_commit();
try {
$transaction1->rollback(new Exception('test'));
$this->fail('transaction rollback must rethrow exception');
} catch (Exception $e) {
$this->assertEqual(get_class($e), 'Exception');
}
$this->assertEqual(0, $DB->count_records($tablename));
$DB->delete_records($tablename);
// rollback from nested level
$transaction1 = $DB->start_delegated_transaction();
$data = (object)array('course'=>3);
$DB->insert_record($tablename, $data);
$transaction2 = $DB->start_delegated_transaction();
$data = (object)array('course'=>4);
$DB->insert_record($tablename, $data);
try {
$transaction2->rollback(new Exception('test'));
$this->fail('transaction rollback must rethrow exception');
} catch (Exception $e) {
$this->assertEqual(get_class($e), 'Exception');
}
$this->assertEqual(2, $DB->count_records($tablename)); // not rolled back yet
try {
$transaction1->allow_commit();
} catch (Exception $e) {
$this->assertEqual(get_class($e), 'dml_transaction_exception');
}
$this->assertEqual(2, $DB->count_records($tablename)); // not rolled back yet
// the forced rollback is done from the default_exception hadnler and similar places,
// let's do it manually here
$this->assertTrue($DB->is_transaction_started());
$DB->force_transaction_rollback();
$this->assertFalse($DB->is_transaction_started());
$this->assertEqual(0, $DB->count_records($tablename)); // finally rolled back
$DB->delete_records($tablename);
}
function test_transactions_forbidden() {
$DB = $this->tdb;
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
$this->tables[$tablename] = $table;
$DB->transactions_forbidden();
$transaction = $DB->start_delegated_transaction();
$data = (object)array('course'=>1);
$DB->insert_record($tablename, $data);
try {
$DB->transactions_forbidden();
} catch (Exception $e) {
$this->assertEqual(get_class($e), 'dml_transaction_exception');
}
// the previous test does not force rollback
$transaction->allow_commit();
$this->assertFalse($DB->is_transaction_started());
$this->assertEqual(1, $DB->count_records($tablename));
}
function test_wrong_transactions() {
$DB = $this->tdb;
$dbman = $DB->get_manager();
$table = $this->get_test_table();
$tablename = $table->getName();
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('course', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);
$this->tables[$tablename] = $table;
// wrong order of nested commits
$transaction1 = $DB->start_delegated_transaction();
$data = (object)array('course'=>3);
$DB->insert_record($tablename, $data);
$transaction2 = $DB->start_delegated_transaction();
$data = (object)array('course'=>4);
$DB->insert_record($tablename, $data);
try {
$transaction1->allow_commit();
$this->fail('wrong order of commits must throw exception');
} catch (Exception $e) {
$this->assertEqual(get_class($e), 'dml_transaction_exception');
}
try {
$transaction2->allow_commit();
$this->fail('first wrong commit forces rollback');
} catch (Exception $e) {
$this->assertEqual(get_class($e), 'dml_transaction_exception');
}
// this is done in default exception hadnler usually
$this->assertTrue($DB->is_transaction_started());
$this->assertEqual(2, $DB->count_records($tablename)); // not rolled back yet
$DB->force_transaction_rollback();
$this->assertEqual(0, $DB->count_records($tablename));
$DB->delete_records($tablename);
// wrong order of nested rollbacks
$transaction1 = $DB->start_delegated_transaction();
$data = (object)array('course'=>3);
$DB->insert_record($tablename, $data);
$transaction2 = $DB->start_delegated_transaction();
$data = (object)array('course'=>4);
$DB->insert_record($tablename, $data);
try {
// this first rollback should prevent all otehr rollbacks
$transaction1->rollback(new Exception('test'));
} catch (Exception $e) {
$this->assertEqual(get_class($e), 'Exception');
}
try {
$transaction2->rollback(new Exception('test'));
} catch (Exception $e) {
$this->assertEqual(get_class($e), 'Exception');
}
try {
$transaction1->rollback(new Exception('test'));
} catch (Exception $e) {
// the rollback was used already once, no way to use it again
$this->assertEqual(get_class($e), 'dml_transaction_exception');
}
// this is done in default exception hadnler usually
$this->assertTrue($DB->is_transaction_started());
$DB->force_transaction_rollback();
$DB->delete_records($tablename);
// unknown transaction object
$transaction1 = $DB->start_delegated_transaction();
$data = (object)array('course'=>3);
$DB->insert_record($tablename, $data);
$transaction2 = new moodle_transaction($DB);
try {
$transaction2->allow_commit();
$this->fail('foreign transaction must fail');
} catch (Exception $e) {
$this->assertEqual(get_class($e), 'dml_transaction_exception');
}
try {
$transaction1->allow_commit();
$this->fail('first wrong commit forces rollback');
} catch (Exception $e) {
$this->assertEqual(get_class($e), 'dml_transaction_exception');
}
$DB->force_transaction_rollback();
$DB->delete_records($tablename);
}
}
/**
@ -2454,4 +2627,7 @@ class moodle_database_for_testing extends moodle_database {
public function sql_concat(){}
public function sql_concat_join($separator="' '", $elements=array()){}
public function sql_substr($expr, $start, $length=false){}
public function begin_transaction() {}
public function commit_transaction() {}
public function rollback_transaction() {}
}

View File

@ -196,6 +196,23 @@ class dml_write_exception extends dml_exception {
}
}
/**
* DML transaction exception - triggered by probles related to DB transactions
*/
class dml_transaction_exception extends dml_exception {
/** @var moodle_transaction */
public $transaction;
/**
* Constructor
* @param array $start_backtrace
*/
function __construct($debuginfo=null, $transaction=null) {
$this->transaction = $transaction; // TODO: MDL-20625 use the info from $transaction for debugging purposes
parent::__construct('dmltransactionexception', NULL, $debuginfo);
}
}
/**
* Sets up global $DB moodle_database instance
*

View File

@ -57,6 +57,8 @@ class database_importer {
* How to use transactions.
*/
protected $transactionmode = 'allinone';
/** Transaction object */
protected $transaction;
/**
* Object constructor.
@ -123,7 +125,7 @@ class database_importer {
throw new dbtransfer_exception('importschemaexception', $details);
}
if ($this->transactionmode == 'allinone') {
$this->mdb->begin_sql();
$this->transaction = $this->mdb->start_delegated_transaction();
}
}
@ -140,7 +142,7 @@ class database_importer {
*/
public function begin_table_import($tablename, $schemaHash) {
if ($this->transactionmode == 'pertable') {
$this->mdb->begin_sql();
$this->transaction = $this->mdb->start_delegated_transaction();
}
if (!$table = $this->schema->getTable($tablename)) {
throw new dbtransfer_exception('unknowntableexception', $tablename);
@ -171,7 +173,7 @@ class database_importer {
}
}
if ($this->transactionmode == 'pertable') {
$this->mdb->commit_sql();
$this->transaction->allow_commit();
}
}
@ -182,7 +184,7 @@ class database_importer {
*/
public function finish_database_import() {
if ($this->transactionmode == 'allinone') {
$this->mdb->commit_sql();
$this->transaction->allow_commit();
}
}

View File

@ -3392,62 +3392,51 @@ function delete_user($user) {
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->dirroot.'/message/lib.php');
// TODO: decide if this transaction is really needed
$DB->begin_sql();
try {
// delete all grades - backup is kept in grade_grades_history table
if ($grades = grade_grade::fetch_all(array('userid'=>$user->id))) {
foreach ($grades as $grade) {
$grade->delete('userdelete');
}
// delete all grades - backup is kept in grade_grades_history table
if ($grades = grade_grade::fetch_all(array('userid'=>$user->id))) {
foreach ($grades as $grade) {
$grade->delete('userdelete');
}
//move unread messages from this user to read
message_move_userfrom_unread2read($user->id);
// remove from all groups
$DB->delete_records('groups_members', array('userid'=>$user->id));
// unenrol from all roles in all contexts
role_unassign(0, $user->id); // this might be slow but it is really needed - modules might do some extra cleanup!
// now do a final accesslib cleanup - removes all role assingments in user context and context itself
delete_context(CONTEXT_USER, $user->id);
require_once($CFG->dirroot.'/tag/lib.php');
tag_set('user', $user->id, array());
// workaround for bulk deletes of users with the same email address
$delname = "$user->email.".time();
while ($DB->record_exists('user', array('username'=>$delname))) { // no need to use mnethostid here
$delname++;
}
// mark internal user record as "deleted"
$updateuser = new object();
$updateuser->id = $user->id;
$updateuser->deleted = 1;
$updateuser->username = $delname; // Remember it just in case
$updateuser->email = ''; // Clear this field to free it up
$updateuser->idnumber = ''; // Clear this field to free it up
$updateuser->timemodified = time();
$DB->update_record('user', $updateuser);
$DB->commit_sql();
// notify auth plugin - do not block the delete even when plugin fails
$authplugin = get_auth_plugin($user->auth);
$authplugin->user_delete($user);
events_trigger('user_deleted', $user);
} catch (Exception $e) {
$DB->rollback_sql();
throw $e;
}
//move unread messages from this user to read
message_move_userfrom_unread2read($user->id);
// remove from all groups
$DB->delete_records('groups_members', array('userid'=>$user->id));
// unenrol from all roles in all contexts
role_unassign(0, $user->id); // this might be slow but it is really needed - modules might do some extra cleanup!
// now do a final accesslib cleanup - removes all role assingments in user context and context itself
delete_context(CONTEXT_USER, $user->id);
require_once($CFG->dirroot.'/tag/lib.php');
tag_set('user', $user->id, array());
// workaround for bulk deletes of users with the same email address
$delname = "$user->email.".time();
while ($DB->record_exists('user', array('username'=>$delname))) { // no need to use mnethostid here
$delname++;
}
// mark internal user record as "deleted"
$updateuser = new object();
$updateuser->id = $user->id;
$updateuser->deleted = 1;
$updateuser->username = $delname; // Remember it just in case
$updateuser->email = ''; // Clear this field to free it up
$updateuser->idnumber = ''; // Clear this field to free it up
$updateuser->timemodified = time();
$DB->update_record('user', $updateuser);
// notify auth plugin - do not block the delete even when plugin fails
$authplugin = get_auth_plugin($user->auth);
$authplugin->user_delete($user);
events_trigger('user_deleted', $user);
return true;
}

View File

@ -859,6 +859,9 @@ class xhtml_container_stack {
return;
}
// TODO: MDL-20625 this looks dangerous and problematic because we never know
// the order of calling of constructors ==> the transaction warning will not be included
// It seems you cannot rely on $CFG, and hence the debugging function here,
// becuase $CFG may be destroyed before this object is.
if ($this->isdebugging) {

View File

@ -874,12 +874,16 @@ class moodle_core_renderer extends moodle_renderer_base {
* @return string HTML fragment
*/
public function footer() {
global $CFG;
global $CFG, $DB;
$output = $this->opencontainers->pop_all_but_last(true);
$footer = $this->opencontainers->pop('header/footer');
if (debugging() and $DB and $DB->is_transaction_started()) {
// TODO: MDL-20625 print warning - transaction will be rolled back
}
// Provide some performance info if required
$performanceinfo = '';
if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {

View File

@ -208,14 +208,12 @@ function default_exception_handler($ex) {
function abort_all_db_transactions() {
global $CFG, $DB, $SCRIPT;
// default exception handler MUST not throw any exceptions!!
if ($DB && $DB->is_transaction_started()) {
error_log('Database transaction aborted automatically in ' . $CFG->dirroot . $SCRIPT);
try {
// note: transaction blocks should never change current $_SESSION
$DB->rollback_sql();
} catch (Exception $ignored) {
// default exception handler MUST not throw any exceptions!!
}
// note: transaction blocks should never change current $_SESSION
$DB->force_transaction_rollback();
}
}

View File

@ -185,7 +185,7 @@ function populate_test_database($syscontext, $numcategories, $numcourses, $nummo
// Activities contexts.
$mods = array();
$prog = new progress_bar('modbar', 500, true);
$DB->begin_sql();
$transaction = $DB->start_delegated_transaction();
for ($i = 0; $i < $nummodules; $i++) {
$context = insert_context(CONTEXT_MODULE, $i, $courses[array_rand($courses)]);
$mods[$context->id] = $context;
@ -193,7 +193,7 @@ function populate_test_database($syscontext, $numcategories, $numcourses, $nummo
$prog->update($i, $nummodules, '');
}
}
$DB->commit_sql();
$transaction->allow_commit();
echo $OUTPUT->notification('Created ' . $nummodules . ' module contexts.', 'notifysuccess'); flush();
$contexts = $categories + $courses + $mods;
@ -212,20 +212,20 @@ function populate_test_database($syscontext, $numcategories, $numcourses, $nummo
// Local overrides.
$localstates = array(TEXTFILTER_OFF => 0, TEXTFILTER_ON => 0);
$prog = new progress_bar('locbar', 500, true);
$DB->begin_sql();
$transaction = $DB->start_delegated_transaction();
for ($i = 0; $i < $numoverrides; $i++) {
filter_set_local_state(array_rand($installedfilters), array_rand($contexts), array_rand($localstates));
if ($i % 50) {
$prog->update($i, $numoverrides, '');
}
}
$DB->commit_sql();
$transaction->allow_commit();
echo $OUTPUT->notification('Set ' . $numoverrides . ' local overrides.', 'notifysuccess'); flush();
// Local config.
$variablenames = array('frog' => 0, 'toad' => 0, 'elver' => 0, 'eft' => 0, 'tadpole' => 0);
$prog = new progress_bar('confbar', 500, true);
$DB->begin_sql();
$transaction = $DB->start_delegated_transaction();
for ($i = 0; $i < $numconfigs; $i++) {
filter_set_local_config(array_rand($installedfilters), array_rand($contexts),
array_rand($variablenames), random_string(rand(20, 40)));
@ -233,7 +233,7 @@ function populate_test_database($syscontext, $numcategories, $numcourses, $nummo
$prog->update($i, $numconfigs, '');
}
}
$DB->commit_sql();
$transaction->allow_commit();
echo $OUTPUT->notification('Set ' . $numconfigs . ' local configs.', 'notifysuccess'); flush();
}

View File

@ -496,7 +496,7 @@ function quiz_has_feedback($quiz) {
*
* @param float $newgrade the new maximum grade for the quiz.
* @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too.
* @return boolean indicating success or failure.
* @return boolean indicating success or failure. TODO: MDL-20625
*/
function quiz_set_grade($newgrade, &$quiz) {
global $DB;
@ -507,43 +507,45 @@ function quiz_set_grade($newgrade, &$quiz) {
}
// Use a transaction, so that on those databases that support it, this is safer.
$DB->begin_sql();
$transaction = $DB->start_delegated_transaction();
// Update the quiz table.
$success = $DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance));
try {
// Update the quiz table.
$DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance));
// Rescaling the other data is only possible if the old grade was non-zero.
if ($quiz->grade > 1e-7) {
global $CFG;
// Rescaling the other data is only possible if the old grade was non-zero.
if ($quiz->grade > 1e-7) {
global $CFG;
$factor = $newgrade/$quiz->grade;
$quiz->grade = $newgrade;
$factor = $newgrade/$quiz->grade;
$quiz->grade = $newgrade;
// Update the quiz_grades table.
$timemodified = time();
$success = $success && $DB->execute("
UPDATE {quiz_grades}
SET grade = ? * grade, timemodified = ?
WHERE quiz = ?
", array($factor, $timemodified, $quiz->id));
// Update the quiz_grades table.
$timemodified = time();
$DB->execute("
UPDATE {quiz_grades}
SET grade = ? * grade, timemodified = ?
WHERE quiz = ?
", array($factor, $timemodified, $quiz->id));
// Update the quiz_feedback table.
$success = $success && $DB->execute("
UPDATE {quiz_feedback}
SET mingrade = ? * mingrade, maxgrade = ? * maxgrade
WHERE quizid = ?
", array($factor, $factor, $quiz->id));
}
// Update the quiz_feedback table.
$DB->execute("
UPDATE {quiz_feedback}
SET mingrade = ? * mingrade, maxgrade = ? * maxgrade
WHERE quizid = ?
", array($factor, $factor, $quiz->id));
}
// update grade item and send all grades to gradebook
quiz_grade_item_update($quiz);
quiz_update_grades($quiz);
// update grade item and send all grades to gradebook
quiz_grade_item_update($quiz);
quiz_update_grades($quiz);
if ($success) {
return $DB->commit_sql();
} else {
$DB->rollback_sql();
return false;
$transaction->allow_commit();
return true;
} catch (Exception $e) {
//TODO: MDL-20625 this part was returning false, but now throws exception
$transaction->rollback($e);
}
}