Skip to content
Snippets Groups Projects
Commit 6950cac3 authored by catch's avatar catch
Browse files

Issue #3384960 by mondrake, daffie, longwave: Strengthen TransactionManager

parent e0eb2312
No related branches found
No related tags found
42 merge requests!54479.5.x SF update,!5014Issue #3071143: Table Render Array Example Is Incorrect,!4868Issue #1428520: Improve menu parent link selection,!3878Removed unused condition head title for views,!38582585169-10.1.x,!3818Issue #2140179: $entity->original gets stale between updates,!3742Issue #3328429: Create item list field formatter for displaying ordered and unordered lists,!3731Claro: role=button on status report items,!3668Resolve #3347842 "Deprecate the trusted",!3651Issue #3347736: Create new SDC component for Olivero (header-search),!3546refactored dialog.pcss file,!3531Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!3502Issue #3335308: Confusing behavior with FormState::setFormState and FormState::setMethod,!3452Issue #3332701: Refactor Claro's tablesort-indicator stylesheet,!3451Issue #2410579: Allows setting the current language programmatically.,!3355Issue #3209129: Scrolling problems when adding a block via layout builder,!3226Issue #2987537: Custom menu link entity type should not declare "bundle" entity key,!3154Fixes #2987987 - CSRF token validation broken on routes with optional parameters.,!3147Issue #3328457: Replace most substr($a, $i) where $i is negative with str_ends_with(),!3146Issue #3328456: Replace substr($a, 0, $i) with str_starts_with(),!3133core/modules/system/css/components/hidden.module.css,!31312878513-10.1.x,!2964Issue #2865710 : Dependencies from only one instance of a widget are used in display modes,!2812Issue #3312049: [Followup] Fix Drupal.Commenting.FunctionComment.MissingReturnType returns for NULL,!2614Issue #2981326: Replace non-test usages of \Drupal::logger() with IoC injection,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2334Issue #3228209: Add hasRole() method to AccountInterface,!2062Issue #3246454: Add weekly granularity to views date sort,!1591Issue #3199697: Add JSON:API Translation experimental module,!1255Issue #3238922: Refactor (if feasible) uses of the jQuery serialize function to use vanillaJS,!1105Issue #3025039: New non translatable field on translatable content throws error,!1073issue #3191727: Focus states on mobile second level navigation items fixed,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!877Issue #2708101: Default value for link text is not saved,!844Resolve #3036010 "Updaters",!673Issue #3214208: FinishResponseSubscriber could create duplicate headers,!617Issue #3043725: Provide a Entity Handler for user cancelation,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493,!485Sets the autocomplete attribute for username/password input field on login form.,!30Issue #3182188: Updates composer usage to point at ./vendor/bin/composer
Pipeline #20592 failed
Pipeline: drupal

#20596

    ...@@ -1480,7 +1480,7 @@ public function startTransaction($name = '') { ...@@ -1480,7 +1480,7 @@ public function startTransaction($name = '') {
    public function rollBack($savepoint_name = 'drupal_transaction') { public function rollBack($savepoint_name = 'drupal_transaction') {
    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Do not rollback the connection, roll back the Transaction objects instead. See https://www.drupal.org/node/3381002', E_USER_DEPRECATED); @trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Do not rollback the connection, roll back the Transaction objects instead. See https://www.drupal.org/node/3381002', E_USER_DEPRECATED);
    if ($this->transactionManager()) { if ($this->transactionManager()) {
    $this->transactionManager()->rollback($savepoint_name); $this->transactionManager()->rollback($savepoint_name, 'bc-force-rollback');
    return; return;
    } }
    if (!$this->inTransaction()) { if (!$this->inTransaction()) {
    ......
    ...@@ -34,6 +34,11 @@ class Transaction { ...@@ -34,6 +34,11 @@ class Transaction {
    * A boolean value to indicate whether this transaction has been rolled back. * A boolean value to indicate whether this transaction has been rolled back.
    * *
    * @var bool * @var bool
    *
    * @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. There is
    * no replacement.
    *
    * @see https://www.drupal.org/node/3381002
    */ */
    protected $rolledBack = FALSE; protected $rolledBack = FALSE;
    ...@@ -47,7 +52,11 @@ class Transaction { ...@@ -47,7 +52,11 @@ class Transaction {
    */ */
    protected $name; protected $name;
    public function __construct(Connection $connection, $name = NULL) { public function __construct(
    Connection $connection,
    $name = NULL,
    protected readonly string $id = '',
    ) {
    if ($connection->transactionManager()) { if ($connection->transactionManager()) {
    $this->connection = $connection; $this->connection = $connection;
    $this->name = $name; $this->name = $name;
    ...@@ -76,11 +85,12 @@ public function __construct(Connection $connection, $name = NULL) { ...@@ -76,11 +85,12 @@ public function __construct(Connection $connection, $name = NULL) {
    public function __destruct() { public function __destruct() {
    if ($this->connection->transactionManager()) { if ($this->connection->transactionManager()) {
    $this->connection->transactionManager()->unpile($this->name); $this->connection->transactionManager()->unpile($this->name, $this->id);
    return; return;
    } }
    // Start of BC layer. // Start of BC layer.
    // If we rolled back then the transaction would have already been popped. // If we rolled back then the transaction would have already been popped.
    // @phpstan-ignore-next-line
    if (!$this->rolledBack) { if (!$this->rolledBack) {
    // @phpstan-ignore-next-line // @phpstan-ignore-next-line
    $this->connection->popTransaction($this->name); $this->connection->popTransaction($this->name);
    ...@@ -107,10 +117,11 @@ public function name() { ...@@ -107,10 +117,11 @@ public function name() {
    */ */
    public function rollBack() { public function rollBack() {
    if ($this->connection->transactionManager()) { if ($this->connection->transactionManager()) {
    $this->connection->transactionManager()->rollback($this->name); $this->connection->transactionManager()->rollback($this->name, $this->id);
    return; return;
    } }
    // Start of BC layer. // Start of BC layer.
    // @phpstan-ignore-next-line
    $this->rolledBack = TRUE; $this->rolledBack = TRUE;
    // @phpstan-ignore-next-line // @phpstan-ignore-next-line
    $this->connection->rollBack($this->name); $this->connection->rollBack($this->name);
    ......
    <?php
    declare(strict_types=1);
    namespace Drupal\Core\Database\Transaction;
    /**
    * A value object for items on the transaction stack.
    */
    final class StackItem {
    /**
    * Constructor.
    *
    * @param string $name
    * The name of the transaction.
    * @param StackItemType $type
    * The stack item type.
    */
    public function __construct(
    public readonly string $name,
    public readonly StackItemType $type,
    ) {
    }
    }
    ...@@ -27,21 +27,31 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { ...@@ -27,21 +27,31 @@ abstract class TransactionManagerBase implements TransactionManagerInterface {
    /** /**
    * The stack of Drupal transactions currently active. * The stack of Drupal transactions currently active.
    * *
    * This is not a real LIFO (Last In, First Out) stack, where we would only * This property is keeping track of the Transaction objects started and
    * remove the layers according to the order they were introduced. For commits * ended as a LIFO (Last In, First Out) stack.
    * the layer order is enforced, while for rollbacks the API allows to
    * rollback to savepoints before the last one.
    * *
    * @var array<string,StackItemType> * The database API allows to begin transactions, add an arbitrary number of
    */ * additional savepoints, and release any savepoint in the sequence. When
    private array $stack = []; * this happens, the database will implicitly release all the savepoints
    * created after the one released. Given Drupal implementation of the
    /** * Transaction objects, we cannot force descoping of the corresponding
    * A list of Drupal transactions rolled back but not yet unpiled. * Transaction savepoint objects from the manager, because they live in the
    * scope of the calling code. This stack ensures that when an outlived
    * Transaction object gets out of scope, it will not try to release on the
    * database a savepoint that no longer exists.
    *
    * Differently, rollbacks are strictly being checked for LIFO order: if a
    * rollback is requested against a savepoint that is not the last created,
    * the manager will throw a TransactionOutOfOrderException.
    *
    * The array key is the transaction's unique id, its value a StackItem.
    * *
    * @var array<string,true> * @var array<string,StackItem>
    *
    * @todo in https://www.drupal.org/project/drupal/issues/3384995, complete
    * the LIFO logic extending it to the root transaction too.
    */ */
    private array $rollbacks = []; private array $stack = [];
    /** /**
    * A list of post-transaction callbacks. * A list of post-transaction callbacks.
    ...@@ -91,7 +101,7 @@ public function stackDepth(): int { ...@@ -91,7 +101,7 @@ public function stackDepth(): int {
    * $stack property. * $stack property.
    * *
    * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidReturn * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidReturn
    * @return array<string,StackItemType> * @return array<string,StackItem>
    * The elements of the transaction stack. * The elements of the transaction stack.
    */ */
    protected function stack(): array { protected function stack(): array {
    ...@@ -114,13 +124,13 @@ protected function resetStack(): void { ...@@ -114,13 +124,13 @@ protected function resetStack(): void {
    * Drivers should not override this method unless they also override the * Drivers should not override this method unless they also override the
    * $stack property. * $stack property.
    * *
    * @param string $name * @param string $id
    * The name of the transaction. * The id of the transaction.
    * @param \Drupal\Core\Database\Transaction\StackItemType $type * @param \Drupal\Core\Database\Transaction\StackItem $item
    * The stack item type. * The stack item.
    */ */
    protected function addStackItem(string $name, StackItemType $type): void { protected function addStackItem(string $id, StackItem $item): void {
    $this->stack[$name] = $type; $this->stack[$id] = $item;
    } }
    /** /**
    ...@@ -129,11 +139,11 @@ protected function addStackItem(string $name, StackItemType $type): void { ...@@ -129,11 +139,11 @@ protected function addStackItem(string $name, StackItemType $type): void {
    * Drivers should not override this method unless they also override the * Drivers should not override this method unless they also override the
    * $stack property. * $stack property.
    * *
    * @param string $name * @param string $id
    * The name of the transaction. * The id of the transaction.
    */ */
    protected function removeStackItem(string $name): void { protected function removeStackItem(string $id): void {
    unset($this->stack[$name]); unset($this->stack[$id]);
    } }
    /** /**
    ...@@ -176,23 +186,30 @@ public function push(string $name = ''): Transaction { ...@@ -176,23 +186,30 @@ public function push(string $name = ''): Transaction {
    $type = StackItemType::Savepoint; $type = StackItemType::Savepoint;
    } }
    // Push the transaction on the stack, increasing its depth. // Define an unique id for the transaction.
    $this->addStackItem($name, $type); $id = uniqid();
    // Add an item on the stack, increasing its depth.
    $this->addStackItem($id, new StackItem($name, $type));
    return new Transaction($this->connection, $name); // Actually return a new Transaction object.
    return new Transaction($this->connection, $name, $id);
    } }
    /** /**
    * {@inheritdoc} * {@inheritdoc}
    */ */
    public function unpile(string $name): void { public function unpile(string $name, string $id): void {
    // If an already rolled back Drupal transaction, do nothing on the client // If the $id does not correspond to the one in the stack for that $name,
    // connection, just cleanup the list of transactions rolled back. // we are facing an orphaned Transaction object (for example in case of a
    if (isset($this->rollbacks[$name])) { // DDL statement breaking an active transaction), so there is nothing more
    unset($this->rollbacks[$name]); // to do.
    if (!isset($this->stack()[$id]) || $this->stack()[$id]->name !== $name) {
    return; return;
    } }
    // If unpiling a savepoint, but that does not exist on the stack, the stack
    // got corrupted.
    if ($name !== 'drupal_transaction' && !$this->has($name)) { if ($name !== 'drupal_transaction' && !$this->has($name)) {
    throw new TransactionOutOfOrderException(); throw new TransactionOutOfOrderException();
    } }
    ...@@ -201,14 +218,20 @@ public function unpile(string $name): void { ...@@ -201,14 +218,20 @@ public function unpile(string $name): void {
    // is not a root one. // is not a root one.
    if ( if (
    $this->has($name) $this->has($name)
    && $this->stack()[$name] === StackItemType::Savepoint && $this->stack()[$id]->type === StackItemType::Savepoint
    && $this->getConnectionTransactionState() === ClientConnectionTransactionState::Active && $this->getConnectionTransactionState() === ClientConnectionTransactionState::Active
    ) { ) {
    // If we are not releasing the last savepoint but an earlier one, all
    // subsequent savepoints will have been released as well. The stack must
    // be diminished accordingly.
    while (($i = array_key_last($this->stack())) != $id) {
    $this->removeStackItem((string) $i);
    }
    $this->releaseClientSavepoint($name); $this->releaseClientSavepoint($name);
    } }
    // Remove the transaction from the stack. // Remove the transaction from the stack.
    $this->removeStackItem($name); $this->removeStackItem($id);
    // If this was the last Drupal transaction open, we can commit the client // If this was the last Drupal transaction open, we can commit the client
    // transaction. // transaction.
    ...@@ -223,24 +246,47 @@ public function unpile(string $name): void { ...@@ -223,24 +246,47 @@ public function unpile(string $name): void {
    /** /**
    * {@inheritdoc} * {@inheritdoc}
    */ */
    public function rollback(string $name): void { public function rollback(string $name, string $id): void {
    // @todo remove in drupal:11.0.0.
    // Start of BC layer.
    if ($id === 'bc-force-rollback') {
    foreach ($this->stack() as $stackId => $item) {
    if ($item->name === $name) {
    $id = $stackId;
    break;
    }
    }
    if ($id === 'bc-force-rollback') {
    throw new TransactionOutOfOrderException();
    }
    }
    // End of BC layer.
    // If the $id does not correspond to the one in the stack for that $name,
    // we are facing an orphaned Transaction object (for example in case of a
    // DDL statement breaking an active transaction), so there is nothing more
    // to do.
    if (!isset($this->stack()[$id]) || $this->stack()[$id]->name !== $name) {
    return;
    }
    if (!$this->inTransaction()) { if (!$this->inTransaction()) {
    throw new TransactionNoActiveException(); throw new TransactionNoActiveException();
    } }
    // Do the client-level processing. // Do the client-level processing.
    match ($this->stack()[$name]) { match ($this->stack()[$id]->type) {
    StackItemType::Root => $this->processRootRollback(), StackItemType::Root => $this->processRootRollback(),
    StackItemType::Savepoint => $this->rollbackClientSavepoint($name), StackItemType::Savepoint => $this->rollbackClientSavepoint($name),
    }; };
    // Rolled back item should match the last one in stack. // Rolled back item should match the last one in stack.
    if ($name !== array_key_last($this->stack())) { if ($id != array_key_last($this->stack())) {
    throw new TransactionOutOfOrderException(); throw new TransactionOutOfOrderException();
    } }
    $this->rollbacks[$name] = TRUE; // Remove the transaction from the stack.
    $this->removeStackItem($name); $this->removeStackItem($id);
    // If this was the last Drupal transaction open, we can commit the client // If this was the last Drupal transaction open, we can commit the client
    // transaction. // transaction.
    ...@@ -263,7 +309,12 @@ public function addPostTransactionCallback(callable $callback): void { ...@@ -263,7 +309,12 @@ public function addPostTransactionCallback(callable $callback): void {
    * {@inheritdoc} * {@inheritdoc}
    */ */
    public function has(string $name): bool { public function has(string $name): bool {
    return isset($this->stack()[$name]); foreach ($this->stack() as $item) {
    if ($item->name === $name) {
    return TRUE;
    }
    }
    return FALSE;
    } }
    /** /**
    ......
    ...@@ -36,6 +36,8 @@ public function has(string $name): bool; ...@@ -36,6 +36,8 @@ public function has(string $name): bool;
    * This begins a client connection transaction if there is not one active, * This begins a client connection transaction if there is not one active,
    * or adds a savepoint to the active one. * or adds a savepoint to the active one.
    * *
    * This method should only be called internally by a database driver.
    *
    * @param string $name * @param string $name
    * (optional) The name of the savepoint. * (optional) The name of the savepoint.
    * *
    ...@@ -54,15 +56,19 @@ public function push(string $name = ''): Transaction; ...@@ -54,15 +56,19 @@ public function push(string $name = ''): Transaction;
    * This method should only be called by a Transaction object going out of * This method should only be called by a Transaction object going out of
    * scope. * scope.
    * *
    * This method should only be called internally by a database driver.
    *
    * @param string $name * @param string $name
    * (optional) The name of the savepoint. * The name of the transaction.
    * @param string $id
    * The id of the transaction.
    * *
    * @throws \Drupal\Core\Database\TransactionOutOfOrderException * @throws \Drupal\Core\Database\TransactionOutOfOrderException
    * If a Drupal Transaction with the specified name does not exist. * If a Drupal Transaction with the specified name does not exist.
    * @throws \Drupal\Core\Database\TransactionCommitFailedException * @throws \Drupal\Core\Database\TransactionCommitFailedException
    * If the commit of the root transaction failed. * If the commit of the root transaction failed.
    */ */
    public function unpile(string $name): void; public function unpile(string $name, string $id): void;
    /** /**
    * Rolls back a Drupal transaction. * Rolls back a Drupal transaction.
    ...@@ -72,8 +78,12 @@ public function unpile(string $name): void; ...@@ -72,8 +78,12 @@ public function unpile(string $name): void;
    * to rolling back the client connection (or to committing it in the edge * to rolling back the client connection (or to committing it in the edge
    * case when the root was unpiled earlier). * case when the root was unpiled earlier).
    * *
    * This method should only be called internally by a database driver.
    *
    * @param string $name * @param string $name
    * (optional) The name of the savepoint. * The name of the transaction.
    * @param string $id
    * The id of the transaction.
    * *
    * @throws \Drupal\Core\Database\TransactionNoActiveException * @throws \Drupal\Core\Database\TransactionNoActiveException
    * If there is no active client connection. * If there is no active client connection.
    ...@@ -83,7 +93,7 @@ public function unpile(string $name): void; ...@@ -83,7 +93,7 @@ public function unpile(string $name): void;
    * @throws \Drupal\Core\Database\TransactionCommitFailedException * @throws \Drupal\Core\Database\TransactionCommitFailedException
    * If the commit of the root transaction failed. * If the commit of the root transaction failed.
    */ */
    public function rollback(string $name): void; public function rollback(string $name, string $id): void;
    /** /**
    * Adds a root transaction end callback. * Adds a root transaction end callback.
    ......
    ...@@ -306,6 +306,7 @@ desaturate ...@@ -306,6 +306,7 @@ desaturate
    desaturated desaturated
    desaturates desaturates
    desaturating desaturating
    descoping
    descripcion descripcion
    destructable destructable
    deutsch deutsch
    ......
    ...@@ -576,6 +576,30 @@ public function testQueryFailureInTransaction() { ...@@ -576,6 +576,30 @@ public function testQueryFailureInTransaction() {
    $this->assertEquals('24', $saved_age); $this->assertEquals('24', $saved_age);
    } }
    /**
    * Tests releasing a savepoint before last is safe.
    */
    public function testReleaseIntermediateSavepoint(): void {
    $transaction = $this->connection->startTransaction();
    $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
    $savepoint1 = $this->connection->startTransaction();
    $this->assertSame(2, $this->connection->transactionManager()->stackDepth());
    $savepoint2 = $this->connection->startTransaction();
    $this->assertSame(3, $this->connection->transactionManager()->stackDepth());
    $savepoint3 = $this->connection->startTransaction();
    $this->assertSame(4, $this->connection->transactionManager()->stackDepth());
    $savepoint4 = $this->connection->startTransaction();
    $this->assertSame(5, $this->connection->transactionManager()->stackDepth());
    $this->insertRow('row');
    unset($savepoint2);
    $this->assertSame(2, $this->connection->transactionManager()->stackDepth());
    $this->assertRowPresent('row');
    unset($savepoint1);
    unset($transaction);
    $this->assertFalse($this->connection->inTransaction());
    $this->assertRowPresent('row');
    }
    /** /**
    * Tests for transaction names. * Tests for transaction names.
    */ */
    ......
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Finish editing this message first!
    Please register or to comment