Problem/Motivation

Right now, a Transaction object in Drupal leads to a COMMIT or a RELEASE SAVEPOINT only when it goes out of scope. In practice, it's an autocommit.

We have code like

    try {
      $transaction = $this->connection->startTransaction();
      foreach ($this->insertValues as $insert_values) {
        $stmt->execute($insert_values, $this->queryOptions);
        ...
      }
    }
    catch (\Exception $e) {
      if (isset($transaction)) {
        // One of the INSERTs failed, rollback the whole batch.
        $transaction->rollBack();
      }
      // Rethrow the exception for the calling code.
      throw $e;
    }

or in other cases something like

    try {
      $transaction = $this->connection->startTransaction();
      foreach ($this->insertValues as $insert_values) {
        $stmt->execute($insert_values, $this->queryOptions);
        ...
      }
      unset($transaction);
    }
    catch (\Exception $e) {
      ....
    }

It works, but I personally find unset($transaction) counterintuitive as to be implying a COMMIT on the db.

See also #1025314: Transactions should be allowed to be committed explicitly for a lot of rationale on why this is not optimal.

Proposed resolution

I propose here to add the possibility to explicitly commit (or release savepoint) a Transaction object, as an opt-in vs. current behavior.

For this purpose, we add a ::commitOrRelease() method to the Transaction object. IMHO we should not name the method ::commit() because we cannot know if the Transaction object is representing the root transaction or a savepoint: so it could well be that a ::commit() method is actually not committing anything on the database. ::commitOrRelease, indicates that the transaction control is returned to the parent level in a nested transaction scenario like Drupal's - e.g. a savepoint transaction object returns control to its parent 'root' transaction (that can still be rolled back entirely if necessary); a 'root' transaction returns control to the database by committing(=persisting the data changes) the db transaction, etc.

We would get into something like

    $transaction = $this->connection->startTransaction();
    try {
      foreach ($this->insertValues as $insert_values) {
        $stmt->execute($insert_values, $this->queryOptions);
        ...
      }
      $transaction->commitOrRelease();
    }
    catch (\Exception $e) {
      // One of the INSERTs failed, rollback the whole batch and rethrow the exception for the calling code.
      $transaction->rollBack();
      throw $e;
    }

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

CommentFileSizeAuthor
#27 3398767-nr-bot.txt3.07 KBneeds-review-queue-bot

Issue fork drupal-3398767

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mondrake created an issue. See original summary.

mondrake’s picture

Title: Allow explicit COMMIT in Transaction objects » RFC: Allow explicit COMMIT in Transaction objects
daffie’s picture

+1 for me.

I really dislike unset($transaction); for the implicit commit.

mondrake’s picture

Issue summary: View changes
mondrake’s picture

Title: RFC: Allow explicit COMMIT in Transaction objects » Allow explicit COMMIT in Transaction objects

OK, let's try this then.

mondrake’s picture

Status: Active » Needs review

smustgrave made their first commit to this issue’s fork.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs change record

Rebased to run the pipeline and it was all green

Ran the test-only feature and they all seemed to still pass shouldn't the base test file fail? https://git.drupalcode.org/issue/drupal-3398767/-/jobs/374406

Seems like something that could use a change record right?

mondrake’s picture

Status: Needs work » Needs review

Ran the test-only feature and they all seemed to still pass shouldn't the base test file fail?

It should in this case, but I don’t think #3395977-4: Test-only changes reverts changes to test modules was addressed and here the change was to a test base class that got reverted too?

mondrake’s picture

Let’s wait for more comments before jumping into CR writing

mondrake’s picture

Title: Allow explicit COMMIT in Transaction objects » [PP-1] Allow explicit COMMIT in Transaction objects
Status: Needs review » Postponed
mondrake’s picture

Title: [PP-1] Allow explicit COMMIT in Transaction objects » Allow explicit COMMIT in Transaction objects
Status: Postponed » Needs work
mondrake’s picture

Status: Needs work » Postponed
Related issues: +#3384999: Introduce a Schema::executeDdlStatement method

Postponed on #3384999: Introduce a Schema::executeDdlStatement method. That will give us the possibility to know about an autocommit due to lack of transactional DDL support having happened BEFORE actually attempting a commit on the DB, and appropriate test coverage, too.

Also we probably need to split the cleanup of DriverSpecificTransactionTestBase - there's a lot of bolierplate that was accumulated by copy/pasting test methods that can be removed.

mondrake’s picture

Title: Allow explicit COMMIT in Transaction objects » [PP-2] Allow explicit COMMIT in Transaction objects
Related issues: +#3407979: Cleanup DriverSpecificTransactionTestBase
mondrake’s picture

Title: [PP-2] Allow explicit COMMIT in Transaction objects » [PP-1] Allow explicit COMMIT in Transaction objects
mondrake’s picture

Note to self: when this is unblocked, do #3386263-10: [ignore] testing issue first thing.

moshe weitzman’s picture

We now have \Drupal\Core\Database\Database::commitAllOnShutdown. Should we keep this issue open or change it somehow?

mondrake’s picture

#18 this issue is about moving AWAY from ::commitAllOnShutdown() (and in general from committing during object destruction which has drawbacks).

IMHO, ::commitAllOnShutdown() should only be transitional towards such goal.

mondrake’s picture

rebased

mondrake’s picture

Title: [PP-1] Allow explicit COMMIT in Transaction objects » Allow explicit COMMIT in Transaction objects
Assigned: Unassigned » mondrake
Status: Postponed » Needs work
mondrake’s picture

Title: Allow explicit COMMIT in Transaction objects » Allow returning explicitly to the prior nesting level in transactions (aka allow explicit COMMIT in Transaction objects)
Issue summary: View changes

Updated title and IS

mondrake’s picture

mondrake’s picture

Assigned: mondrake » Unassigned
Priority: Normal » Major
Status: Needs work » Needs review

Green on all three core supported databases! Ready for review.

I am bumping to Major as it's known there are problems with the current handling of autocommit in HEAD.

mondrake’s picture

Assigned: Unassigned » mondrake
Status: Needs review » Needs work
mondrake’s picture

Assigned: mondrake » Unassigned
Status: Needs work » Needs review
needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new3.07 KB

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

mondrake’s picture

Status: Needs work » Needs review

rebased

mondrake’s picture

daffie’s picture

Status: Needs review » Needs work
mondrake’s picture

Thanks for review @daffie!

In general, I just c/p DriverSpecificTransactionTestBase into the new TransactionYieldTest, to try and keep the two test sets (one testing the commit-on-destruct behavior, one testing the commit-on-yield one) in sync, doing as few changes as possible. Anyway, I will look into your input and adjust as much as possible.

mondrake’s picture

Status: Needs work » Needs review
daffie’s picture

Status: Needs review » Needs work
Issue tags: -Needs subsystem maintainer review

All the code changes look good to me.
The CI pipeline is green for all 3 databases.
For the new method we shall need a CR.

mondrake’s picture

Status: Needs work » Needs review

Added CR https://www.drupal.org/node/3512006

Thanks @daffie!

mondrake’s picture

Issue tags: -Needs change record
daffie’s picture

Status: Needs review » Reviewed & tested by the community

The CR looks good to me.
All code changes look good to me.
The CI pipeline is green for all 3 databases.
For me it is RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

Added some comments to the MR.

I think we should consider having a commit method for situations where you know that you've created the root transaction. I think we should also consider having a startRootTransaction() method to support this. So you can have very explicit code. Yield is a tricky word to use as it already has quite a bit of meaning in PHP and normally means to yield a value as part of an iteration.

mondrake’s picture

Thanks for review @alexpott.

Transaction does not have an interface for now.

We could add an additional Connection::startSavepoint() method, and Transaction::commit() + Transaction::releaseSavepoint() methods. That will somehow force how transactions are managed in code, though, in the sense that devs will have to be aware of whether they are working within the root transaction or within a savepoint. If some code starts a root transaction, you would not be able to encapsulate that code in a higher method if that will try to start a root transaction as well.

The beauty of the MR here is that you do not really care - you always call Connection::startTransaction and then it's the manager deciding what to do depending on the stack depth.

I am not going to make changes until we have agreed on a plan forward.

mondrake’s picture

Status: Needs work » Needs review

NR for the proposal in #38.

alexpott’s picture

I am not going to make changes until we have agreed on a plan forward.

Yeah 100%.

One thing I'm thinking about is rollback... Atm we call rollback on a transaction and that will go back to your previous savepoint or the end the root transaction without a commit - right? So given rollback already does 2 different things... why don't we rename yield to commit() and document that it will commit the root transaction or release the savepoint depending on transaction depth as we already have this behaviour with rollback... what do you think. My concern is the yield is an odd word wrt to databases and transactions.

mondrake’s picture

Apologies if this sounds a bit scholarly... not my intention.

One thing I'm thinking about is rollback... Atm we call rollback on a transaction and that will go back to your previous savepoint or the end the root transaction without a commit - right? So given rollback already does 2 different things...

Right. But IMHO we should not assimilate the combination commit/release savepoint to the rollback. Commit permanently saves data to the DBMS, release savepoint doesn't. Rollback, doesn't matter whether to a savepoint or to the begin of the transactions, just returns to a temporary state within the transaction, it does not persist anything. Commit is very a specific, unambiguous action.

why don't we rename yield to commit() and document that it will commit the root transaction or release the savepoint depending on transaction depth as we already have this behaviour with rollback...

In an earlier version of the MR, it was like that. Then, I pondered the above and thought that a 'commit' method that does not really persist anything may be seen as a WTF - anyone not aware of all the details/not reading the docs would call 'commit' on a savepoint and then complain data are not saved.

For me if we add a Transaction::commit() method we should not be allowing ambiguity: it must persist the data as everyone would expect.

So, how about this next proposal:

  • once instantiating a new Transaction object, store the transaction type in it
  • add a Transaction::commit() and a Transaction::releaseSavepoint() methods, to be used respectively for... what they respectively mean
  • in case we need to let the manager decide whether to commit or release a savepoint, we could still have a method like ::yield() with an appropriate rename (for example, ::commitOrRelease()), or we could have a simple match construct in calling code like
    match ($transaction->type) {
        StackItemType::Root => $transaction->commit(),
        StackItemType::Savepoint => $transaction->releaseSavepoint(),
    }

EDIT - added proposed rename for ::yield()

daffie’s picture

I like the change to only using Transaction::commit() or Transaction::releaseSavepoint() and having no Transaction::yield(). Developer need to learn the difference between to two.
The proposed changes look good to me.

alexpott’s picture

I like the commitOrRelease name way more than yield. I like specific methods for commit and releaseSavepoint being public.My only concern is that commit not being an alias for commitOrRelease() might be problematic - because ATM DB transaction users don't need to be concerned if something wraps their code in another transaction because the abstraction layer hides this complexity away from them.

mondrake’s picture

So we just go ahead renaming yield() to commitOrRelease() without implementing commit() and releaseSavepoint()?

I’d be +1 on that as I really value not having to care about what method to use, and the manager caring about that.

alexpott’s picture

Yeah lets do #44. We can add explicit methods later if needs be.

daffie’s picture

Status: Needs review » Needs work

Let’s do #44

mondrake’s picture

Assigned: Unassigned » mondrake

On it.

mondrake’s picture

Issue summary: View changes

Updated IS with #44

catch’s picture

fwiw I was behind on this issue and only read the last few comments just now, but #44 sounds good and means we don't need to worry about implications of #43, so extra +1.

mondrake’s picture

Assigned: mondrake » Unassigned
Status: Needs work » Needs review

Done

mondrake’s picture

Updated CR with the latest approach.

daffie’s picture

Status: Needs review » Reviewed & tested by the community

All use of the method yield() has been replaced with commitOrRelease().
The IS and the CR are updated.
Back to RTBC.

mondrake’s picture

Please consider crediting contributors to #1025314: Transactions should be allowed to be committed explicitly, that will become outdated when this is committed.

mondrake’s picture

fwiw, the #3406985: Convert all transactions in core to use explicit ::commitOrRelease() follow-up MR is built on top of this MR, and passes all tests.

alexpott credited anybody.

alexpott credited c960657.

alexpott’s picture

Crediting people who contributed to #1025314: Transactions should be allowed to be committed explicitly - ie. c960657 and anybody.

Please all the peeps from this issue.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Getting this into 11.x early in the 11.3.x cycle to wrangle out any issues and work on the follow-ups.

Committed 4815dcc and pushed to 11.x. Thanks!

  • alexpott committed 4815dccf on 11.x
    Issue #3398767 by mondrake, daffie, alexpott, c960657, anybody: Allow...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

alexpott’s picture

This made it into 11.2.x-rc1 so is in the 11.2.x releases. Updating the CR.

mondrake’s picture

@alexpott this issue did, but #3406985: Convert all transactions in core to use explicit ::commitOrRelease() is still wip. So the edit of https://www.drupal.org/node/3524461 is incorrect, actually. Not reverting it since it’s still a draft.

mondrake’s picture

Changed https://www.drupal.org/node/3512006 which indeed was published pointing to 11.3.0 whereas it should refer to 11.2.0

twod’s picture

I stumbled upon https://www.drupal.org/node/3512006 today and thought I could now use ::commitOrRelease in 11.2.0 since it was published for that version.

But it's apparent from git that it's only committed to 11.x, and isn't even in 11.2.x yet. Was there a mistake somewhere?

mondrake’s picture

#64 you're right. It's not in 11.2.x, it was only committed to 11.x (yes, at the time when 11.2.0 was still in RC, but not to its branch). So it will be GA only when 11.3.0 will be released.

Changed again the CR.

Thanks!

mondrake’s picture