Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

PHPLIB-373 Use DatabaseTransactionsManager in ManagesTransactions trait #3443

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
paulinevos wants to merge 1 commit into mongodb:5.5
base: 5.5
Choose a base branch
Loading
from paulinevos:373_after-commit

Conversation

Copy link
Collaborator

@paulinevos paulinevos commented Sep 4, 2025

DatabaseTransactionsManager was introduced in Laravel to keep track of things like pending transactions and fire transaction events, so that e.g. dispatches could hook into transaction state.

Our trait was not using it yet, resulting in missing afterCommit behavior (see PHPLIB-373 and #3328)

Checklist

  • Add tests and ensure they pass

@@ -88,6 +92,8 @@ public function transaction(Closure $callback, $attempts = 1, array $options = [
$callbackResult = null;
$throwable = null;

$this->transactionsManager?->begin($this->getName(), 0);
Copy link
Collaborator Author

@paulinevos paulinevos Sep 4, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately have to call this stuff here manually again, unlike what the Laravel trait does (which is to call its own beginTransaction method here), as we are using the with_transaction helper function and passing the callback. The helper function, in turn, calls WithTransaction::execute which handles starting and committing the actual transaction in the session.

Calling beginTransaction here instead of calling DatabaseTransactionsManager::begin directly would therefore result in attempting to start the transaction twice.

Same goes for the commit call

jmikola reacted with thumbs up emoji
Copy link
Member

@jmikola jmikola Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per my earlier comment in beginTransaction(), should you actually pass 1 as the transaction level? Also, if you can't call beginTransaction() yourself perhaps you'd also want to manually assign $this->transactions = 1 immediately before as well:

$this->transactions = 1;
$this->transactionsManager?->begin($this->getName(), $this->transactions);

Since you're deviating from the base implementation, I think a block comment explaining why this inline duplication is necessary would help.

@GromNaN GromNaN added this to the 5.5 milestone Sep 4, 2025
@GromNaN GromNaN changed the base branch from 5.x to 5.5 September 4, 2025 14:50
`DatabaseTransactionsManager` was introduced in Laravel to keep track of
things like pending transactions and fire transaction events, so that
e.g. dispatches could hook into transaction state.
Our trait was not using it yet, resulting in missing `afterCommit`
behavior (see PHPLIB-373)
@@ -113,6 +119,8 @@ public function transaction(Closure $callback, $attempts = 1, array $options = [
throw $throwable;
}

$this->transactionsManager?->commit($this->getName(), 0, 0);
Copy link
Collaborator Author

@paulinevos paulinevos Sep 4, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't sure if if it was worth not hard-coding this for future proofing reasons in case we ever start supporting nested transactions? 😬

Copy link
Member

@jmikola jmikola Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Echoing my comments above about keeping this in sync with any changes to the commit() method and ensuring that $this->transactions also gets set back to 0. I'd suggest using the same temporary variable for clarity:

$levelBeingCommitted = $this->transactions;
$this->transactions = 0;

@@ -55,6 +55,8 @@ private function getSessionOrThrow(): Session
*/
public function beginTransaction(array $options = []): void
{
$this->transactionsManager?->begin($this->getName(), 0);
Copy link
Member

@jmikola jmikola Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted that the second parameter here is int $level per DatabaseTransactionsManager::begin().

In the base ManagesTransactions::begin() implementation, the level is incremented before invoking DatabaseTransactionsManager::begin(). I'm not certain, but I expect that 1 would be the smallest value passed in that case. Here, you're passing 0 but setting $this->transactions to 1 a few lines below.

Also, it may be a subtle change, but it looks like the base implementation invokes PDO::commit() before DatabaseTransactionsManager::begin(). Therefore, would it make more sense to invoke Session::startTransaction() first here?

Consider:

$this->getSessionOrCreate()->startTransaction($options);
$this->transactions = 1;
$this->transactionsManager?->begin($this->getName(), $this->transactions);

@@ -65,6 +67,8 @@ public function beginTransaction(array $options = []): void
public function commit(): void
{
$this->getSessionOrThrow()->commitTransaction();

$this->transactionsManager?->commit($this->getName(), 0, 0);
$this->transactions = 0;
Copy link
Member

@jmikola jmikola Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flow here does match [ManagesTransactions::commit()](https://github.com/laravel/framework/blob/v12.28.1/src/Illuminate/Database/Concerns/ManagesTransactions.php#L203] in that you invoke the driver's commit method first (analogous to PDO::commit()).

DatabaseTransactionsManager::commit() might expect two different values for int $levelBeingCommitted and int $newTransactionLevel, though. Consider the following in the base method:

[$levelBeingCommitted, $this->transactions] = [
 $this->transactions,
 max(0, $this->transactions - 1),
];

Here, you pass 0 as both the committed and new level, which is a subtle behavioral difference. While this could be replaced with 1, 0 literals, I think the code would be more self-documenting if copied the above assignment or instead did something like:

$levelBeingCommitted = $this->transactions;
$this->transactions = 0;

Either way, a comment to remind readers that MongoDB only supports a single transaction level might be helpful (if not here then perhaps in begin() where you manually assign 1 instead of incrementing).

Copy link
Collaborator Author

@paulinevos paulinevos Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I figured it would be appropriate to pas 0 here twice as we have no levels, and the levels wouldn't dip below 0. But it did feel a bit icky to me so I might do what you've suggested. Also just in case we somehow do start supporting nested transactions many years down the line, this won't become one of those things causing weird behavior because we forgot about these hardcoded values.

Copy link
Member

@jmikola jmikola Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the current transaction level gets passed to the event listeners, but that was actually what prompted my comment (not so much the possibility of nested transactions in MongoDB 12.0—don't quote me on that). Something downstream of these events might be surprised to see a transaction get committed at level zero.

paulinevos reacted with thumbs up emoji
Copy link
Collaborator Author

@paulinevos paulinevos Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

@@ -88,6 +92,8 @@ public function transaction(Closure $callback, $attempts = 1, array $options = [
$callbackResult = null;
$throwable = null;

$this->transactionsManager?->begin($this->getName(), 0);
Copy link
Member

@jmikola jmikola Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per my earlier comment in beginTransaction(), should you actually pass 1 as the transaction level? Also, if you can't call beginTransaction() yourself perhaps you'd also want to manually assign $this->transactions = 1 immediately before as well:

$this->transactions = 1;
$this->transactionsManager?->begin($this->getName(), $this->transactions);

Since you're deviating from the base implementation, I think a block comment explaining why this inline duplication is necessary would help.

@@ -113,6 +119,8 @@ public function transaction(Closure $callback, $attempts = 1, array $options = [
throw $throwable;
}

$this->transactionsManager?->commit($this->getName(), 0, 0);
Copy link
Member

@jmikola jmikola Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Echoing my comments above about keeping this in sync with any changes to the commit() method and ensuring that $this->transactions also gets set back to 0. I'd suggest using the same temporary variable for clarity:

$levelBeingCommitted = $this->transactions;
$this->transactions = 0;

@@ -55,6 +55,8 @@ private function getSessionOrThrow(): Session
*/
public function beginTransaction(array $options = []): void
{
$this->transactionsManager?->begin($this->getName(), 0);
Copy link
Member

@GromNaN GromNaN Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at Laravel's ManagesTransactions, the call to transactionsManager?->begin is done after the transaction is started.

https://github.com/laravel/framework/blob/d09f1345e25d8d04c67ffa4cdd3656f5ebbce57a/src/Illuminate/Database/Concerns/ManagesTransactions.php#L124-L136

Copy link
Collaborator Author

@paulinevos paulinevos Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That can work for this method, but not for the transaction method, as WithTransaction::execute() both starts the transaction and calls the callback, which shouldn't happen (and is what this call handles)

So I'm struggling to think if we should keep the order consistent on our library side or have one method consistent with Laravel and the other not...

@@ -55,6 +55,8 @@ private function getSessionOrThrow(): Session
*/
public function beginTransaction(array $options = []): void
{
$this->transactionsManager?->begin($this->getName(), 0);

$this->getSessionOrCreate()->startTransaction($options);
$this->transactions = 1;
}
Copy link
Member

@GromNaN GromNaN Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the $this->fireConnectionEvent('beganTransaction'); also be called at like in Laravel: https://github.com/laravel/framework/blob/d09f1345e25d8d04c67ffa4cdd3656f5ebbce57a/src/Illuminate/Database/Concerns/ManagesTransactions.php#L138

That was added in Laravel 5.4, I don't know which feature it is.

jmikola reacted with thumbs up emoji
Copy link
Collaborator Author

@paulinevos paulinevos Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, forgot that one!

Copy link
Member

@jmikola jmikola Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that there are a few other fireConnectionEvent calls for committing, committed, and rollingBack. Since we create our own callback in transaction(), which wraps the user's callback, I think you'll be able to work this into the code path that uses with_transaction as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Reviewers

@jmikola jmikola jmikola left review comments

@GromNaN GromNaN GromNaN left review comments

At least 1 approving review is required to merge this pull request.

Assignees
No one assigned
Labels
Projects
None yet
Milestone
5.5
Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /