/** * Call this method from test if you want to make sure that * the resetting of database is done the slow way without transaction * rollback. * * This is useful especially when testing stuff that is not compatible with transactions. * * @return void */ public function preventResetByRollback() { if ($this->testdbtransaction and !$this->testdbtransaction->is_disposed()) { $this->testdbtransaction->allow_commit(); $this->testdbtransaction = null; } }
/** * 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 caught. * * @param moodle_transaction $transaction An instance of a moodle_transaction. * @param Exception|Throwable $e The related exception/throwable to this transaction rollback. * @return void This does not return, instead the exception passed in will be rethrown. */ public function rollback_delegated_transaction(moodle_transaction $transaction, $e) { if (!$e instanceof Exception && !$e instanceof Throwable) { // PHP7 - we catch Throwables in phpunit but can't use that as the type hint in PHP5. $e = new \coding_exception("Must be given an Exception or Throwable object!"); } if ($transaction->is_disposed()) { throw new dml_transaction_exception('Transactions already disposed', $transaction); } // 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; \core\event\manager::database_transaction_rolledback(); \core\message\manager::database_transaction_rolledback(); } throw $e; }
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); // 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 handler 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 other 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 handler 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); }
/** * 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 caught. * * @param moodle_transaction $transaction An instance of a moodle_transaction. * @param Exception $e The related exception to this transaction rollback. * @return void This does not return, instead the exception passed in will be rethrown. */ public function rollback_delegated_transaction(moodle_transaction $transaction, Exception $e) { if ($transaction->is_disposed()) { throw new dml_transaction_exception('Transactions already disposed', $transaction); } // 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; }
/** * Persist the passed in grades (keyed by userid) to the database * * @param array $grades * @param \moodle_transaction $transaction * * @return bool */ protected function persist_grades($grades, \moodle_transaction $transaction) { global $DB; foreach ($grades as $userid => $grade) { if ($usergrade = $DB->get_record('activequiz_grades', array('userid' => $userid, 'activequizid' => $this->rtq->getRTQ()->id))) { // we're updating $usergrade->grade = $grade; $usergrade->timemodified = time(); if (!$DB->update_record('activequiz_grades', $usergrade)) { $transaction->rollback(new \Exception('Can\'t update user grades')); } } else { // we're adding $usergrade = new \stdClass(); $usergrade->activequizid = $this->rtq->getRTQ()->id; $usergrade->userid = $userid; $usergrade->grade = $grade; $usergrade->timemodified = time(); if (!$DB->insert_record('activequiz_grades', $usergrade)) { $transaction->rollback(new \Exception('Can\'t insert user grades')); } } } return true; }