- Patch #669794 by andypost, Josh Waihi, aspilicious: critical task: use savepoints for nested transactions.
parent
dc17b0f2cd
commit
48e05803fd
|
@ -200,16 +200,9 @@ abstract class DatabaseConnection extends PDO {
|
|||
* nested calls to transactions and collapse them into a single
|
||||
* transaction.
|
||||
*
|
||||
* @var int
|
||||
* @var array
|
||||
*/
|
||||
protected $transactionLayers = 0;
|
||||
|
||||
/**
|
||||
* Whether or not the active transaction (if any) will be rolled back.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $willRollback;
|
||||
protected $transactionLayers = array();
|
||||
|
||||
/**
|
||||
* Array of argument arrays for logging post-rollback.
|
||||
|
@ -870,29 +863,42 @@ abstract class DatabaseConnection extends PDO {
|
|||
* TRUE if we're currently in a transaction, FALSE otherwise.
|
||||
*/
|
||||
public function inTransaction() {
|
||||
return ($this->transactionLayers > 0);
|
||||
return ($this->transactionDepth() > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines current transaction depth.
|
||||
*/
|
||||
public function transactionDepth() {
|
||||
return count($this->transactionLayers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new DatabaseTransaction object on this connection.
|
||||
*
|
||||
* @param $name
|
||||
* Optional name of the savepoint.
|
||||
*
|
||||
* @see DatabaseTransaction
|
||||
*/
|
||||
public function startTransaction() {
|
||||
public function startTransaction($name = '') {
|
||||
if (empty($this->transactionClass)) {
|
||||
$this->transactionClass = 'DatabaseTransaction_' . $this->driver();
|
||||
if (!class_exists($this->transactionClass)) {
|
||||
$this->transactionClass = 'DatabaseTransaction';
|
||||
}
|
||||
}
|
||||
return new $this->transactionClass($this);
|
||||
return new $this->transactionClass($this, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the current transaction for rollback.
|
||||
* Rolls back the transaction entirely or to a named savepoint.
|
||||
*
|
||||
* This method throws an exception if no transaction is active.
|
||||
*
|
||||
* @param $savepoint_name
|
||||
* The name of the savepoint. The default, 'drupal_transaction', will roll
|
||||
* the entire transaction back.
|
||||
* @param $type
|
||||
* The category to which the rollback message belongs.
|
||||
* @param $message
|
||||
|
@ -912,9 +918,14 @@ abstract class DatabaseConnection extends PDO {
|
|||
* @see DatabaseTransaction::rollback()
|
||||
* @see watchdog()
|
||||
*/
|
||||
public function rollback($type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL) {
|
||||
if ($this->transactionLayers == 0) {
|
||||
throw new NoActiveTransactionException();
|
||||
public function rollback($savepoint_name = 'drupal_transaction', $type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL) {
|
||||
if (!$this->inTransaction()) {
|
||||
throw new DatabaseTransactionNoActiveException();
|
||||
}
|
||||
// A previous rollback to an earlier savepoint may mean that the savepoint
|
||||
// in question has already been rolled back.
|
||||
if (!in_array($savepoint_name, $this->transactionLayers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the severity to the configured default if not specified.
|
||||
|
@ -940,25 +951,49 @@ abstract class DatabaseConnection extends PDO {
|
|||
);
|
||||
}
|
||||
|
||||
$this->willRollback = TRUE;
|
||||
// We need to find the point we're rolling back to, all other savepoints
|
||||
// before are no longer needed.
|
||||
while ($savepoint = array_pop($this->transactionLayers)) {
|
||||
if ($savepoint == $savepoint_name) {
|
||||
// If it is the last the transaction in the stack, then it is not a
|
||||
// savepoint, it is the transaction itself so we will need to roll back
|
||||
// the transaction rather than a savepoint.
|
||||
if (empty($this->transactionLayers)) {
|
||||
break;
|
||||
}
|
||||
$this->query('ROLLBACK TO SAVEPOINT ' . $savepoint);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ($this->supportsTransactions()) {
|
||||
parent::rollBack();
|
||||
}
|
||||
$this->logRollback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this transaction will roll back.
|
||||
*
|
||||
* Use this function to skip further operations if the current transaction
|
||||
* is already scheduled to roll back. Throws an exception if no transaction
|
||||
* is active.
|
||||
*
|
||||
* @return
|
||||
* TRUE if the transaction will roll back, FALSE otherwise.
|
||||
* Logs messages from rollback().
|
||||
*/
|
||||
public function willRollback() {
|
||||
if ($this->transactionLayers == 0) {
|
||||
throw new NoActiveTransactionException();
|
||||
protected function logRollback() {
|
||||
$logging = Database::getLoggingCallback();
|
||||
// If there is no callback defined. We can't do anything.
|
||||
if (!is_array($logging)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->willRollback;
|
||||
$logging_callback = $logging['callback'];
|
||||
|
||||
// Log the failed rollback.
|
||||
$logging_callback('database', 'Explicit rollback failed: not supported on active connection.', array(), $logging['error_severity']);
|
||||
|
||||
// Play back the logged errors to the specified logging callback post-
|
||||
// rollback.
|
||||
foreach ($this->rollbackLogs as $log_item) {
|
||||
$logging_callback($log_item['type'], $log_item['message'], $log_item['variables'], $log_item['severity'], $log_item['link']);
|
||||
}
|
||||
|
||||
// Reset the error logs.
|
||||
$this->rollbackLogs = array();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -968,72 +1003,57 @@ abstract class DatabaseConnection extends PDO {
|
|||
*
|
||||
* @see DatabaseTransaction
|
||||
*/
|
||||
public function pushTransaction() {
|
||||
++$this->transactionLayers;
|
||||
|
||||
if ($this->transactionLayers == 1) {
|
||||
if ($this->supportsTransactions()) {
|
||||
parent::beginTransaction();
|
||||
}
|
||||
public function pushTransaction($name) {
|
||||
if (!$this->supportsTransactions()) {
|
||||
return;
|
||||
}
|
||||
if (isset($this->transactionLayers[$name])) {
|
||||
throw new DatabaseTransactionNameNonUniqueException($name . " is already in use.");
|
||||
}
|
||||
// If we're already in a transaction then we want to create a savepoint
|
||||
// rather than try to create another transaction.
|
||||
if ($this->inTransaction()) {
|
||||
$this->query('SAVEPOINT ' . $name);
|
||||
}
|
||||
else {
|
||||
parent::beginTransaction();
|
||||
}
|
||||
$this->transactionLayers[$name] = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decreases the depth of transaction nesting.
|
||||
*
|
||||
* This function first attempts to decrease the number of layers of
|
||||
* transaction nesting by one. If there was no active transaction, the
|
||||
* function throws an exception. If this was the last transaction layer, the
|
||||
* function either rolls back or commits the transaction, depending on whether
|
||||
* the transaction was marked for rollback or not.
|
||||
* If we pop off the last transaction layer, then we either commit or roll
|
||||
* back the transaction as necessary. If no transaction is active, we return
|
||||
* because the transaction may have manually been rolled back.
|
||||
*
|
||||
* @param $name
|
||||
* The name of the savepoint
|
||||
*
|
||||
* @see DatabaseTransaction
|
||||
*/
|
||||
public function popTransaction() {
|
||||
if ($this->transactionLayers == 0) {
|
||||
throw new NoActiveTransactionException();
|
||||
public function popTransaction($name) {
|
||||
if (!$this->supportsTransactions()) {
|
||||
return;
|
||||
}
|
||||
if (!$this->inTransaction()) {
|
||||
throw new DatabaseTransactionNoActiveException();
|
||||
}
|
||||
|
||||
--$this->transactionLayers;
|
||||
// Commit everything since SAVEPOINT $name.
|
||||
while($savepoint = array_pop($this->transactionLayers)) {
|
||||
if ($savepoint != $name) continue;
|
||||
|
||||
if ($this->transactionLayers == 0) {
|
||||
if ($this->willRollback) {
|
||||
// Reset the rollback status so that the next transaction starts clean.
|
||||
$this->willRollback = FALSE;
|
||||
|
||||
// Reset the error log.
|
||||
$rollback_logs = $this->rollbackLogs;
|
||||
$this->rollbackLogs = array();
|
||||
|
||||
$logging = Database::getLoggingCallback();
|
||||
$logging_callback = NULL;
|
||||
if (is_array($logging)) {
|
||||
$logging_callback = $logging['callback'];
|
||||
}
|
||||
|
||||
if ($this->supportsTransactions()) {
|
||||
parent::rollBack();
|
||||
}
|
||||
else {
|
||||
if (isset($logging_callback)) {
|
||||
// Log the failed rollback.
|
||||
call_user_func($logging_callback, 'database', 'Explicit rollback failed: not supported on active connection.', array(), $logging['error_severity']);
|
||||
}
|
||||
|
||||
// It would be nice to throw an exception here if logging failed,
|
||||
// but throwing exceptions in destructors is not supported.
|
||||
}
|
||||
|
||||
if (isset($logging_callback)) {
|
||||
// Play back the logged errors to the specified logging callback post-
|
||||
// rollback.
|
||||
foreach ($rollback_logs as $log_item) {
|
||||
call_user_func($logging_callback, $log_item['type'], $log_item['message'], $log_item['variables'], $log_item['severity'], $log_item['link']);
|
||||
}
|
||||
// If there are no more layers left then we should commit.
|
||||
if (empty($this->transactionLayers)) {
|
||||
if (!parent::commit()) {
|
||||
throw new DatabaseTransactionCommitFailedException();
|
||||
}
|
||||
}
|
||||
elseif ($this->supportsTransactions()) {
|
||||
parent::commit();
|
||||
else {
|
||||
$this->query('RELEASE SAVEPOINT ' . $name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1166,7 +1186,7 @@ abstract class DatabaseConnection extends PDO {
|
|||
* @see DatabaseTransaction
|
||||
*/
|
||||
public function commit() {
|
||||
throw new ExplicitTransactionsNotSupportedException();
|
||||
throw new DatabaseTransactionExplicitCommitNotAllowedException();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1611,7 +1631,17 @@ abstract class Database {
|
|||
/**
|
||||
* Exception for when popTransaction() is called with no active transaction.
|
||||
*/
|
||||
class NoActiveTransactionException extends Exception { }
|
||||
class DatabaseTransactionNoActiveException extends Exception { }
|
||||
|
||||
/**
|
||||
* Exception thrown when a savepoint or transaction name occurs twice.
|
||||
*/
|
||||
class DatabaseTransactionNameNonUniqueException extends Exception { }
|
||||
|
||||
/**
|
||||
* Exception thrown when a commit() function fails.
|
||||
*/
|
||||
class DatabaseTransactionCommitFailedException extends Exception { }
|
||||
|
||||
/**
|
||||
* Exception to deny attempts to explicitly manage transactions.
|
||||
|
@ -1619,7 +1649,7 @@ class NoActiveTransactionException extends Exception { }
|
|||
* This exception will be thrown when the PDO connection commit() is called.
|
||||
* Code should never call this method directly.
|
||||
*/
|
||||
class ExplicitTransactionsNotSupportedException extends Exception { }
|
||||
class DatabaseTransactionExplicitCommitNotAllowedException extends Exception { }
|
||||
|
||||
/**
|
||||
* Exception thrown for merge queries that do not make semantic sense.
|
||||
|
@ -1670,13 +1700,51 @@ class DatabaseTransaction {
|
|||
*/
|
||||
protected $connection;
|
||||
|
||||
public function __construct(DatabaseConnection &$connection) {
|
||||
/**
|
||||
* A boolean value to indicate whether this transaction has been rolled back.
|
||||
*
|
||||
* @var Boolean
|
||||
*/
|
||||
protected $rolledBack = FALSE;
|
||||
|
||||
/**
|
||||
* The name of the transaction.
|
||||
*
|
||||
* This is used to label the transaction savepoint. It will be overridden to
|
||||
* 'drupal_transaction' if there is no transaction depth.
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
public function __construct(DatabaseConnection &$connection, $name = NULL) {
|
||||
$this->connection = &$connection;
|
||||
$this->connection->pushTransaction();
|
||||
// If there is no transaction depth, then no transaction has started. Name
|
||||
// the transaction 'drupal_transaction'.
|
||||
if (!$depth = $connection->transactionDepth()) {
|
||||
$this->name = 'drupal_transaction';
|
||||
}
|
||||
// Within transactions, savepoints are used. Each savepoint requires a
|
||||
// name. So if no name is present we need to create one.
|
||||
elseif (!$name) {
|
||||
$this->name = 'savepoint_' . $depth;
|
||||
}
|
||||
else {
|
||||
$this->name = $name;
|
||||
}
|
||||
$this->connection->pushTransaction($this->name);
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->connection->popTransaction();
|
||||
// If we rolled back then the transaction would have already been popped.
|
||||
if ($this->connection->inTransaction() && !$this->rolledBack) {
|
||||
$this->connection->popTransaction($this->name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the name of the transaction or savepoint.
|
||||
*/
|
||||
public function name() {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1704,22 +1772,15 @@ class DatabaseTransaction {
|
|||
* @see watchdog()
|
||||
*/
|
||||
public function rollback($type = NULL, $message = NULL, $variables = array(), $severity = NULL, $link = NULL) {
|
||||
$this->rolledBack = TRUE;
|
||||
if (!isset($severity)) {
|
||||
$logging = Database::getLoggingCallback();
|
||||
if (is_array($logging)) {
|
||||
$severity = $logging['default_severity'];
|
||||
}
|
||||
}
|
||||
$this->connection->rollback($type, $message, $variables, $severity, $link);
|
||||
$this->connection->rollback($this->name, $type, $message, $variables, $severity, $link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this transaction will roll back.
|
||||
*/
|
||||
public function willRollback() {
|
||||
return $this->connection->willRollback();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2310,22 +2371,20 @@ function db_select($table, $alias = NULL, array $options = array()) {
|
|||
/**
|
||||
* Returns a new transaction object for the active database.
|
||||
*
|
||||
* @param $required
|
||||
* TRUE if the calling code will not function properly without transaction
|
||||
* support. If set to TRUE and the active database does not support
|
||||
* transactions, a TransactionsNotSupportedException exception will be thrown.
|
||||
* @param $options
|
||||
* An array of options to control how the transaction operates. Only the
|
||||
* target key has any meaning in this case.
|
||||
* @param string $name
|
||||
* Optional name of the transaction.
|
||||
* @param array $options
|
||||
* An array of options to control how the transaction operates:
|
||||
* - target: The database target name.
|
||||
*
|
||||
* @return DatabaseTransaction
|
||||
* A new DatabaseTransaction object for this connection.
|
||||
*/
|
||||
function db_transaction($required = FALSE, Array $options = array()) {
|
||||
function db_transaction($name = NULL, array $options = array()) {
|
||||
if (empty($options['target'])) {
|
||||
$options['target'] = 'default';
|
||||
}
|
||||
return Database::getConnection($options['target'])->startTransaction($required);
|
||||
return Database::getConnection($options['target'])->startTransaction($name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2413,7 +2472,7 @@ function db_driver() {
|
|||
* Closes the active database connection.
|
||||
*
|
||||
* @param $options
|
||||
* An array of options to control which connection is closed. Only the target
|
||||
* An array of options to control which connection is closed. Only the target
|
||||
* key has any meaning in this case.
|
||||
*/
|
||||
function db_close(array $options = array()) {
|
||||
|
|
|
@ -2908,6 +2908,7 @@ class DatabaseTransactionTestCase extends DatabaseTestCase {
|
|||
*/
|
||||
protected function transactionOuterLayer($suffix, $rollback = FALSE) {
|
||||
$connection = Database::getConnection();
|
||||
$depth = $connection->transactionDepth();
|
||||
$txn = db_transaction();
|
||||
|
||||
// Insert a single row into the testing table.
|
||||
|
@ -2925,6 +2926,13 @@ class DatabaseTransactionTestCase extends DatabaseTestCase {
|
|||
$this->transactionInnerLayer($suffix, $rollback);
|
||||
|
||||
$this->assertTrue($connection->inTransaction(), t('In transaction after calling nested transaction.'));
|
||||
|
||||
if ($rollback) {
|
||||
// Roll back the transaction, if requested.
|
||||
// This rollback should propagate to the last savepoint.
|
||||
$txn->rollback();
|
||||
$this->assertTrue(($connection->transactionDepth() == $depth), t('Transaction has rolled back to the last savepoint after calling rollback().'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2939,12 +2947,18 @@ class DatabaseTransactionTestCase extends DatabaseTestCase {
|
|||
protected function transactionInnerLayer($suffix, $rollback = FALSE) {
|
||||
$connection = Database::getConnection();
|
||||
|
||||
$this->assertTrue($connection->inTransaction(), t('In transaction in nested transaction.'));
|
||||
|
||||
$depth = $connection->transactionDepth();
|
||||
// Start a transaction. If we're being called from ->transactionOuterLayer,
|
||||
// then we're already in a transaction. Normally, that would make starting
|
||||
// a transaction here dangerous, but the database API handles this problem
|
||||
// for us by tracking the nesting and avoiding the danger.
|
||||
$txn = db_transaction();
|
||||
|
||||
$depth2 = $connection->transactionDepth();
|
||||
$this->assertTrue($depth < $depth2, t('Transaction depth is has increased with new transaction.'));
|
||||
|
||||
// Insert a single row into the testing table.
|
||||
db_insert('test')
|
||||
->fields(array(
|
||||
|
@ -2957,9 +2971,9 @@ class DatabaseTransactionTestCase extends DatabaseTestCase {
|
|||
|
||||
if ($rollback) {
|
||||
// Roll back the transaction, if requested.
|
||||
// This rollback should propagate to the the outer transaction, if present.
|
||||
// This rollback should propagate to the last savepoint.
|
||||
$txn->rollback();
|
||||
$this->assertTrue($txn->willRollback(), t('Transaction is scheduled to roll back after calling rollback().'));
|
||||
$this->assertTrue(($connection->transactionDepth() == $depth), t('Transaction has rolled back to the last savepoint after calling rollback().'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue