Skip to content
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

[10.x] Fix how nested transaction callbacks are handled #48859

Merged
Merged
4 changes: 4 additions & 0 deletions src/Illuminate/Database/Concerns/ManagesTransactions.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public function transaction(Closure $callback, $attempts = 1)
$this->getPdo()->commit();
}

$this->transactionsManager?->stageTransactions($this->getName());

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

if ($this->afterCommitCallbacksShouldBeExecuted()) {
Expand Down Expand Up @@ -194,6 +196,8 @@ public function commit()
$this->getPdo()->commit();
}

$this->transactionsManager?->stageTransactions($this->getName());

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

if ($this->afterCommitCallbacksShouldBeExecuted()) {
Expand Down
59 changes: 48 additions & 11 deletions src/Illuminate/Database/DatabaseTransactionsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@

namespace Illuminate\Database;

use Illuminate\Support\Collection;

class DatabaseTransactionsManager
{
/**
* All of the recorded transactions.
* All of the committed transactions.
*
* @var \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
*/
protected $committedTransactions;

/**
* All of the pending transactions.
*
* @var \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
*/
protected $transactions;
protected $pendingTransactions;

/**
* Create a new database transactions manager instance.
Expand All @@ -18,7 +27,8 @@ class DatabaseTransactionsManager
*/
public function __construct()
{
$this->transactions = collect();
$this->committedTransactions = new Collection;
$this->pendingTransactions = new Collection;
}

/**
Expand All @@ -30,7 +40,7 @@ public function __construct()
*/
public function begin($connection, $level)
{
$this->transactions->push(
$this->pendingTransactions->push(
new DatabaseTransactionRecord($connection, $level)
);
}
Expand All @@ -44,7 +54,7 @@ public function begin($connection, $level)
*/
public function rollback($connection, $level)
{
$this->transactions = $this->transactions->reject(
$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection == $connection && $transaction->level > $level
)->values();
}
Expand All @@ -57,11 +67,11 @@ public function rollback($connection, $level)
*/
public function commit($connection)
{
[$forThisConnection, $forOtherConnections] = $this->transactions->partition(
[$forThisConnection, $forOtherConnections] = $this->committedTransactions->partition(
fn ($transaction) => $transaction->connection == $connection
);

$this->transactions = $forOtherConnections->values();
$this->committedTransactions = $forOtherConnections->values();

$forThisConnection->map->executeCallbacks();
}
Expand All @@ -81,14 +91,31 @@ public function addCallback($callback)
$callback();
}

/**
* Move all the pending transactions to a committed state.
*
* @param string $connection
* @return void
*/
public function stageTransactions($connection)
{
$this->committedTransactions = $this->committedTransactions->merge(
$this->pendingTransactions->filter(fn ($transaction) => $transaction->connection === $connection)
);

$this->pendingTransactions = $this->pendingTransactions->reject(
fn ($transaction) => $transaction->connection === $connection
);
}

/**
* Get the transactions that are applicable to callbacks.
*
* @return \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
*/
public function callbackApplicableTransactions()
{
return $this->transactions;
return $this->pendingTransactions;
}

/**
Expand All @@ -103,12 +130,22 @@ public function afterCommitCallbacksShouldBeExecuted($level)
}

/**
* Get all the transactions.
* Get all of the pending transactions.
*
* @return \Illuminate\Support\Collection
*/
public function getPendingTransactions()
{
return $this->pendingTransactions;
}

/**
* Get all of the committed transactions.
*
* @return \Illuminate\Support\Collection
*/
public function getTransactions()
public function getCommittedTransactions()
{
return $this->transactions;
return $this->committedTransactions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function addCallback($callback)
return $callback();
}

$this->transactions->last()->addCallback($callback);
$this->pendingTransactions->last()->addCallback($callback);
}

/**
Expand All @@ -31,7 +31,7 @@ public function addCallback($callback)
*/
public function callbackApplicableTransactions()
{
return $this->transactions->skip(1)->values();
return $this->pendingTransactions->skip(1)->values();
}

/**
Expand Down
76 changes: 55 additions & 21 deletions tests/Database/DatabaseTransactionsManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ public function testBeginningTransactions()
$manager->begin('default', 2);
$manager->begin('admin', 1);

$this->assertCount(3, $manager->getTransactions());
$this->assertSame('default', $manager->getTransactions()[0]->connection);
$this->assertEquals(1, $manager->getTransactions()[0]->level);
$this->assertSame('default', $manager->getTransactions()[1]->connection);
$this->assertEquals(2, $manager->getTransactions()[1]->level);
$this->assertSame('admin', $manager->getTransactions()[2]->connection);
$this->assertEquals(1, $manager->getTransactions()[2]->level);
$this->assertCount(3, $manager->getPendingTransactions());
$this->assertSame('default', $manager->getPendingTransactions()[0]->connection);
$this->assertEquals(1, $manager->getPendingTransactions()[0]->level);
$this->assertSame('default', $manager->getPendingTransactions()[1]->connection);
$this->assertEquals(2, $manager->getPendingTransactions()[1]->level);
$this->assertSame('admin', $manager->getPendingTransactions()[2]->connection);
$this->assertEquals(1, $manager->getPendingTransactions()[2]->level);
}

public function testRollingBackTransactions()
Expand All @@ -34,13 +34,13 @@ public function testRollingBackTransactions()

$manager->rollback('default', 1);

$this->assertCount(2, $manager->getTransactions());
$this->assertCount(2, $manager->getPendingTransactions());

$this->assertSame('default', $manager->getTransactions()[0]->connection);
$this->assertEquals(1, $manager->getTransactions()[0]->level);
$this->assertSame('default', $manager->getPendingTransactions()[0]->connection);
$this->assertEquals(1, $manager->getPendingTransactions()[0]->level);

$this->assertSame('admin', $manager->getTransactions()[1]->connection);
$this->assertEquals(1, $manager->getTransactions()[1]->level);
$this->assertSame('admin', $manager->getPendingTransactions()[1]->connection);
$this->assertEquals(1, $manager->getPendingTransactions()[1]->level);
}

public function testRollingBackTransactionsAllTheWay()
Expand All @@ -53,10 +53,10 @@ public function testRollingBackTransactionsAllTheWay()

$manager->rollback('default', 0);

$this->assertCount(1, $manager->getTransactions());
$this->assertCount(1, $manager->getPendingTransactions());

$this->assertSame('admin', $manager->getTransactions()[0]->connection);
$this->assertEquals(1, $manager->getTransactions()[0]->level);
$this->assertSame('admin', $manager->getPendingTransactions()[0]->connection);
$this->assertEquals(1, $manager->getPendingTransactions()[0]->level);
}

public function testCommittingTransactions()
Expand All @@ -67,12 +67,15 @@ public function testCommittingTransactions()
$manager->begin('default', 2);
$manager->begin('admin', 1);

$manager->stageTransactions('default');
$manager->stageTransactions('admin');
$manager->commit('default');

$this->assertCount(1, $manager->getTransactions());
$this->assertCount(0, $manager->getPendingTransactions());
$this->assertCount(1, $manager->getCommittedTransactions());

$this->assertSame('admin', $manager->getTransactions()[0]->connection);
$this->assertEquals(1, $manager->getTransactions()[0]->level);
$this->assertSame('admin', $manager->getCommittedTransactions()[0]->connection);
$this->assertEquals(1, $manager->getCommittedTransactions()[0]->level);
}

public function testCallbacksAreAddedToTheCurrentTransaction()
Expand All @@ -93,9 +96,9 @@ public function testCallbacksAreAddedToTheCurrentTransaction()
$manager->addCallback(function () use (&$callbacks) {
});

$this->assertCount(1, $manager->getTransactions()[0]->getCallbacks());
$this->assertCount(0, $manager->getTransactions()[1]->getCallbacks());
$this->assertCount(1, $manager->getTransactions()[2]->getCallbacks());
$this->assertCount(1, $manager->getPendingTransactions()[0]->getCallbacks());
$this->assertCount(0, $manager->getPendingTransactions()[1]->getCallbacks());
$this->assertCount(1, $manager->getPendingTransactions()[2]->getCallbacks());
}

public function testCommittingTransactionsExecutesCallbacks()
Expand All @@ -118,6 +121,7 @@ public function testCommittingTransactionsExecutesCallbacks()

$manager->begin('admin', 1);

$manager->stageTransactions('default');
$manager->commit('default');

$this->assertCount(2, $callbacks);
Expand All @@ -144,6 +148,7 @@ public function testCommittingExecutesOnlyCallbacksOfTheConnection()
$callbacks[] = ['admin', 1];
});

$manager->stageTransactions('default');
$manager->commit('default');

$this->assertCount(1, $callbacks);
Expand All @@ -163,4 +168,33 @@ public function testCallbackIsExecutedIfNoTransactions()
$this->assertCount(1, $callbacks);
$this->assertEquals(['default', 1], $callbacks[0]);
}

public function testStageTransactions()
{
$manager = (new DatabaseTransactionsManager);

$manager->begin('default', 1);
$manager->begin('admin', 1);

$this->assertCount(2, $manager->getPendingTransactions());

$pendingTransactions = $manager->getPendingTransactions();

$this->assertEquals(1, $pendingTransactions[0]->level);
$this->assertEquals('default', $pendingTransactions[0]->connection);
$this->assertEquals(1, $pendingTransactions[1]->level);
$this->assertEquals('admin', $pendingTransactions[1]->connection);

$manager->stageTransactions('default');

$this->assertCount(1, $manager->getPendingTransactions());
$this->assertCount(1, $manager->getCommittedTransactions());
$this->assertEquals('default', $manager->getCommittedTransactions()[0]->connection);

$manager->stageTransactions('admin');

$this->assertCount(0, $manager->getPendingTransactions());
$this->assertCount(2, $manager->getCommittedTransactions());
$this->assertEquals('admin', $manager->getCommittedTransactions()[1]->connection);
}
}
5 changes: 5 additions & 0 deletions tests/Database/DatabaseTransactionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public function testTransactionIsRecordedAndCommitted()
{
$transactionManager = m::mock(new DatabaseTransactionsManager);
$transactionManager->shouldReceive('begin')->once()->with('default', 1);
$transactionManager->shouldReceive('stageTransactions')->once()->with('default');
$transactionManager->shouldReceive('commit')->once()->with('default');

$this->connection()->setTransactionManager($transactionManager);
Expand All @@ -83,6 +84,7 @@ public function testTransactionIsRecordedAndCommittedUsingTheSeparateMethods()
{
$transactionManager = m::mock(new DatabaseTransactionsManager);
$transactionManager->shouldReceive('begin')->once()->with('default', 1);
$transactionManager->shouldReceive('stageTransactions')->once()->with('default');
$transactionManager->shouldReceive('commit')->once()->with('default');

$this->connection()->setTransactionManager($transactionManager);
Expand All @@ -103,6 +105,7 @@ public function testNestedTransactionIsRecordedAndCommitted()
$transactionManager = m::mock(new DatabaseTransactionsManager);
$transactionManager->shouldReceive('begin')->once()->with('default', 1);
$transactionManager->shouldReceive('begin')->once()->with('default', 2);
$transactionManager->shouldReceive('stageTransactions')->twice()->with('default');
$transactionManager->shouldReceive('commit')->once()->with('default');

$this->connection()->setTransactionManager($transactionManager);
Expand Down Expand Up @@ -130,6 +133,8 @@ public function testNestedTransactionIsRecordeForDifferentConnectionsdAndCommitt
$transactionManager->shouldReceive('begin')->once()->with('default', 1);
$transactionManager->shouldReceive('begin')->once()->with('second_connection', 1);
$transactionManager->shouldReceive('begin')->once()->with('second_connection', 2);
$transactionManager->shouldReceive('stageTransactions')->once()->with('default');
$transactionManager->shouldReceive('stageTransactions')->twice()->with('second_connection');
$transactionManager->shouldReceive('commit')->once()->with('default');
$transactionManager->shouldReceive('commit')->once()->with('second_connection');

Expand Down
Loading