Unverified Commit 4815dccf authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3398767 by mondrake, daffie, alexpott, c960657, anybody: Allow...

Issue #3398767 by mondrake, daffie, alexpott, c960657, anybody: Allow returning explicitly to the prior nesting level in transactions (aka allow explicit COMMIT in Transaction objects)
parent ed28d46c
Loading
Loading
Loading
Loading
Loading
+18 −12
Original line number Diff line number Diff line
@@ -30,12 +30,12 @@ public function __construct(
  /**
   * Destructs the object.
   *
   * Depending on the nesting level of the object, this leads to a COMMIT (for
   * a root item) or to a RELEASE SAVEPOINT (for a savepoint item) executed on
   * the database.
   * If the transaction is still active at this stage, and depending on the
   * state of the transaction stack, this leads to a COMMIT (for a root item)
   * or to a RELEASE SAVEPOINT (for a savepoint item) executed on the database.
   */
  public function __destruct() {
    $this->connection->transactionManager()->unpile($this->name, $this->id);
    $this->connection->transactionManager()->purge($this->name, $this->id);
  }

  /**
@@ -46,16 +46,22 @@ public function name() {
  }

  /**
   * Rolls back the current transaction.
   * Returns the transaction to the parent nesting level.
   *
   * This is just a wrapper method to rollback whatever transaction stack we are
   * currently in, which is managed by the TransactionManager. Note that logging
   * needs to happen after a transaction has been rolled back or the log
   * messages will be rolled back too.
   * Depending on the state of the transaction stack, this leads to a COMMIT
   * operation (for a root item), or to a RELEASE SAVEPOINT operation (for a
   * savepoint item) executed on the database.
   */
  public function commitOrRelease(): void {
    $this->connection->transactionManager()->unpile($this->name, $this->id);
  }

  /**
   * Rolls back the transaction.
   *
   * Depending on the nesting level of the object, this leads to a ROLLBACK (for
   * a root item) or to a ROLLBACK TO SAVEPOINT (for a savepoint item) executed
   * on the database.
   * Depending on the state of the transaction stack, this leads to a ROLLBACK
   * operation (for a root item), or to a ROLLBACK TO SAVEPOINT + a RELEASE
   * SAVEPOINT operations (for a savepoint item) executed on the database.
   */
  public function rollBack() {
    $this->connection->transactionManager()->rollback($this->name, $this->id);
+102 −32
Original line number Diff line number Diff line
@@ -101,6 +101,16 @@ abstract class TransactionManagerBase implements TransactionManagerInterface {
   */
  private ClientConnectionTransactionState $connectionTransactionState;

  /**
   * Whether to trigger warnings when unpiling a void transaction.
   *
   * Normally FALSE, is set to TRUE by specific tests checking the internal
   * state of the transaction stack.
   *
   * @internal
   */
  public bool $triggerWarningWhenUnpilingOnVoidTransaction = FALSE;

  /**
   * Constructor.
   *
@@ -202,7 +212,9 @@ protected function removeStackItem(string $id): void {
  protected function voidStackItem(string $id): void {
    // The item should be removed from $stack and added to $voidedItems for
    // later processing.
    if (isset($this->stack[$id])) {
      $this->voidedItems[$id] = $this->stack[$id];
    }
    $this->removeStackItem($id);
  }

@@ -285,14 +297,29 @@ public function push(string $name = ''): Transaction {
  }

  /**
   * {@inheritdoc}
   * Purges a Drupal transaction from the manager.
   *
   * This is only called by a Transaction object's ::__destruct() method and
   * should only be called internally by a database driver.
   *
   * @param string $name
   *   The name of the transaction.
   * @param string $id
   *   The id of the transaction.
   *
   * @throws \Drupal\Core\Database\TransactionOutOfOrderException
   *   If a Drupal Transaction with the specified name does not exist.
   * @throws \Drupal\Core\Database\TransactionCommitFailedException
   *   If the commit of the root transaction failed.
   *
   * @internal
   */
  public function unpile(string $name, string $id): void {
  public function purge(string $name, string $id): void {
    // If this is a 'root' transaction, and it is voided (that is, no longer in
    // the stack), then the transaction on the database is no longer active. An
    // action such as a rollback, or a DDL statement, was executed that
    // terminated the database transaction. So, we can process the post
    // transaction callbacks.
    // action such as a commit, a release savepoint, a rollback, or a DDL
    // statement, was executed that terminated the database transaction. So, we
    // can process the post transaction callbacks.
    if (!isset($this->stack()[$id]) && isset($this->voidedItems[$id]) && $this->rootId === $id) {
      $this->processPostTransactionCallbacks();
      $this->rootId = NULL;
@@ -309,6 +336,62 @@ public function unpile(string $name, string $id): void {
      return;
    }

    // When we get here, the transaction (or savepoint) is still active on the
    // database. We can unpile it, and if we are left with no more items in the
    // stack, we can also process the post transaction callbacks.
    $this->commit($name, $id);
    $this->removeStackItem($id);
    if ($this->rootId === $id) {
      $this->processPostTransactionCallbacks();
      $this->rootId = NULL;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function unpile(string $name, string $id): void {
    // If the transaction was voided, we cannot unpile. Skip but trigger a user
    // warning if requested.
    if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Voided) {
      if ($this->triggerWarningWhenUnpilingOnVoidTransaction) {
        trigger_error('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', E_USER_WARNING);
      }
      return;
    }

    // If there is no $id to commit, or if $id does not correspond to the one
    // in the stack for that $name, the commit is out of order.
    if (!isset($this->stack()[$id]) || $this->stack()[$id]->name !== $name) {
      throw new TransactionOutOfOrderException("Error attempting commit of {$id}\\{$name}. Active stack: " . $this->dumpStackItemsAsString());
    }

    // Commit the transaction.
    $this->commit($name, $id);

    // Void the transaction stack item.
    $this->voidStackItem($id);
  }

  /**
   * Commits a Drupal transaction.
   *
   * @param string $name
   *   The name of the transaction.
   * @param string $id
   *   The id of the transaction.
   *
   * @throws \Drupal\Core\Database\TransactionOutOfOrderException
   *   If a Drupal Transaction with the specified name does not exist.
   * @throws \Drupal\Core\Database\TransactionCommitFailedException
   *   If the commit of the root transaction failed.
   */
  protected function commit(string $name, string $id): void {
    if ($this->getConnectionTransactionState() !== ClientConnectionTransactionState::Active) {
      // The stack got corrupted.
      throw new TransactionOutOfOrderException("Transaction {$id}\\{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString());
    }

    // If we are not releasing the last savepoint but an earlier one, or
    // committing a root transaction while savepoints are active, all
    // subsequent savepoints will be released as well. The stack must be
@@ -317,7 +400,6 @@ public function unpile(string $name, string $id): void {
      $this->voidStackItem((string) $i);
    }

    if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Active) {
    if ($this->stackDepth() > 1 && $this->stack()[$id]->type === StackItemType::Savepoint) {
      // Release the client transaction savepoint in case the Drupal
      // transaction is not a root one.
@@ -327,23 +409,11 @@ public function unpile(string $name, string $id): void {
      // If this was the root Drupal transaction, we can commit the client
      // transaction.
      $this->processRootCommit();
        if ($this->rootId === $id) {
          $this->processPostTransactionCallbacks();
          $this->rootId = NULL;
        }
    }
    else {
      // The stack got corrupted.
      throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString());
    }

      // Remove the transaction from the stack.
      $this->removeStackItem($id);
      return;
    }

    // The stack got corrupted.
    throw new TransactionOutOfOrderException("Transaction {$id}/{$name} is out of order. Active stack: " . $this->dumpStackItemsAsString());
  }

  /**
+2 −2
Original line number Diff line number Diff line
@@ -53,8 +53,8 @@ public function push(string $name = ''): Transaction;
   * Removes a Drupal transaction from the stack.
   *
   * The unpiled item does not necessarily need to be the last on the stack.
   * This method should only be called by a Transaction object going out of
   * scope.
   * This method should only be called by a Transaction object's
   * ::commitOrRelease() method.
   *
   * This method should only be called internally by a database driver.
   *
+4 −2
Original line number Diff line number Diff line
@@ -95,13 +95,14 @@ public function insertLogEntry(TestRun $test_run, array $entry): bool {
   * {@inheritdoc}
   */
  public function removeResults(TestRun $test_run): int {
    $this->connection->startTransaction('delete_test_run');
    $transaction = $this->connection->startTransaction('delete_test_run');
    $this->connection->delete('simpletest')
      ->condition('test_id', $test_run->id())
      ->execute();
    $count = $this->connection->delete('simpletest_test_id')
      ->condition('test_id', $test_run->id())
      ->execute();
    $transaction->commitOrRelease();
    return $count;
  }

@@ -169,9 +170,10 @@ public function validateTestingResultsEnvironment(): bool {
   */
  public function cleanUp(): int {
    // Clear test results.
    $this->connection->startTransaction('delete_simpletest');
    $transaction = $this->connection->startTransaction('delete_simpletest');
    $this->connection->delete('simpletest')->execute();
    $count = $this->connection->delete('simpletest_test_id')->execute();
    $transaction->commitOrRelease();
    return $count;
  }

+0 −6
Original line number Diff line number Diff line
@@ -432,9 +432,6 @@ public function testTransactionWithDdlStatement(): void {

  /**
   * Tests rollback after a DDL statement when no transactional DDL supported.
   *
   * @todo In drupal:12.0.0, rollBack will throw a
   *   TransactionOutOfOrderException. Adjust the test accordingly.
   */
  public function testRollbackAfterDdlStatementForNonTransactionalDdlDatabase(): void {
    if ($this->connection->supportsTransactionalDDL()) {
@@ -919,9 +916,6 @@ public function testRootTransactionEndCallbackCalledAfterDdlAndRollbackForTransa
   * transaction including DDL statements is not possible, since a commit
   * happened already. We cannot decide what should be the status of the
   * callback, an exception is thrown.
   *
   * @todo In drupal:12.0.0, rollBack will throw a
   *   TransactionOutOfOrderException. Adjust the test accordingly.
   */
  public function testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase(): void {
    if ($this->connection->supportsTransactionalDDL()) {
Loading