Skip to content

Commit 95e924d

Browse files
mateusjateneecrynobonetaylorotwell
authored
[10.x] Fix how nested transaction callbacks are handled (#48859)
* failing test * naive solution * Add comment * Revert "naive solution" This reverts commit ef46049. * wip * Fix DatabaseTransactionsManager tests * fix null object * Fix Testing DatabaseTransactionsManager * make styleci happy * additional test * Pass connection name to stageTransactions * Add unit test * add tests for Testing DatabaseTransactionsManager * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * wip Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> * formatting --------- Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com> Co-authored-by: Mior Muhammad Zaki <crynobone@gmail.com> Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent 7faedf1 commit 95e924d

File tree

7 files changed

+306
-34
lines changed

7 files changed

+306
-34
lines changed

src/Illuminate/Database/Concerns/ManagesTransactions.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public function transaction(Closure $callback, $attempts = 1)
4747
$this->getPdo()->commit();
4848
}
4949

50+
$this->transactionsManager?->stageTransactions($this->getName());
51+
5052
$this->transactions = max(0, $this->transactions - 1);
5153

5254
if ($this->afterCommitCallbacksShouldBeExecuted()) {
@@ -194,6 +196,8 @@ public function commit()
194196
$this->getPdo()->commit();
195197
}
196198

199+
$this->transactionsManager?->stageTransactions($this->getName());
200+
197201
$this->transactions = max(0, $this->transactions - 1);
198202

199203
if ($this->afterCommitCallbacksShouldBeExecuted()) {

src/Illuminate/Database/DatabaseTransactionsManager.php

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@
22

33
namespace Illuminate\Database;
44

5+
use Illuminate\Support\Collection;
6+
57
class DatabaseTransactionsManager
68
{
79
/**
8-
* All of the recorded transactions.
10+
* All of the committed transactions.
11+
*
12+
* @var \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
13+
*/
14+
protected $committedTransactions;
15+
16+
/**
17+
* All of the pending transactions.
918
*
1019
* @var \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
1120
*/
12-
protected $transactions;
21+
protected $pendingTransactions;
1322

1423
/**
1524
* Create a new database transactions manager instance.
@@ -18,7 +27,8 @@ class DatabaseTransactionsManager
1827
*/
1928
public function __construct()
2029
{
21-
$this->transactions = collect();
30+
$this->committedTransactions = new Collection;
31+
$this->pendingTransactions = new Collection;
2232
}
2333

2434
/**
@@ -30,7 +40,7 @@ public function __construct()
3040
*/
3141
public function begin($connection, $level)
3242
{
33-
$this->transactions->push(
43+
$this->pendingTransactions->push(
3444
new DatabaseTransactionRecord($connection, $level)
3545
);
3646
}
@@ -44,7 +54,7 @@ public function begin($connection, $level)
4454
*/
4555
public function rollback($connection, $level)
4656
{
47-
$this->transactions = $this->transactions->reject(
57+
$this->pendingTransactions = $this->pendingTransactions->reject(
4858
fn ($transaction) => $transaction->connection == $connection && $transaction->level > $level
4959
)->values();
5060
}
@@ -57,11 +67,11 @@ public function rollback($connection, $level)
5767
*/
5868
public function commit($connection)
5969
{
60-
[$forThisConnection, $forOtherConnections] = $this->transactions->partition(
70+
[$forThisConnection, $forOtherConnections] = $this->committedTransactions->partition(
6171
fn ($transaction) => $transaction->connection == $connection
6272
);
6373

64-
$this->transactions = $forOtherConnections->values();
74+
$this->committedTransactions = $forOtherConnections->values();
6575

6676
$forThisConnection->map->executeCallbacks();
6777
}
@@ -81,14 +91,31 @@ public function addCallback($callback)
8191
$callback();
8292
}
8393

94+
/**
95+
* Move all the pending transactions to a committed state.
96+
*
97+
* @param string $connection
98+
* @return void
99+
*/
100+
public function stageTransactions($connection)
101+
{
102+
$this->committedTransactions = $this->committedTransactions->merge(
103+
$this->pendingTransactions->filter(fn ($transaction) => $transaction->connection === $connection)
104+
);
105+
106+
$this->pendingTransactions = $this->pendingTransactions->reject(
107+
fn ($transaction) => $transaction->connection === $connection
108+
);
109+
}
110+
84111
/**
85112
* Get the transactions that are applicable to callbacks.
86113
*
87114
* @return \Illuminate\Support\Collection<int, \Illuminate\Database\DatabaseTransactionRecord>
88115
*/
89116
public function callbackApplicableTransactions()
90117
{
91-
return $this->transactions;
118+
return $this->pendingTransactions;
92119
}
93120

94121
/**
@@ -103,12 +130,22 @@ public function afterCommitCallbacksShouldBeExecuted($level)
103130
}
104131

105132
/**
106-
* Get all the transactions.
133+
* Get all of the pending transactions.
134+
*
135+
* @return \Illuminate\Support\Collection
136+
*/
137+
public function getPendingTransactions()
138+
{
139+
return $this->pendingTransactions;
140+
}
141+
142+
/**
143+
* Get all of the committed transactions.
107144
*
108145
* @return \Illuminate\Support\Collection
109146
*/
110-
public function getTransactions()
147+
public function getCommittedTransactions()
111148
{
112-
return $this->transactions;
149+
return $this->committedTransactions;
113150
}
114151
}

src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function addCallback($callback)
2121
return $callback();
2222
}
2323

24-
$this->transactions->last()->addCallback($callback);
24+
$this->pendingTransactions->last()->addCallback($callback);
2525
}
2626

2727
/**
@@ -31,7 +31,7 @@ public function addCallback($callback)
3131
*/
3232
public function callbackApplicableTransactions()
3333
{
34-
return $this->transactions->skip(1)->values();
34+
return $this->pendingTransactions->skip(1)->values();
3535
}
3636

3737
/**

tests/Database/DatabaseTransactionsManagerTest.php

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ public function testBeginningTransactions()
1515
$manager->begin('default', 2);
1616
$manager->begin('admin', 1);
1717

18-
$this->assertCount(3, $manager->getTransactions());
19-
$this->assertSame('default', $manager->getTransactions()[0]->connection);
20-
$this->assertEquals(1, $manager->getTransactions()[0]->level);
21-
$this->assertSame('default', $manager->getTransactions()[1]->connection);
22-
$this->assertEquals(2, $manager->getTransactions()[1]->level);
23-
$this->assertSame('admin', $manager->getTransactions()[2]->connection);
24-
$this->assertEquals(1, $manager->getTransactions()[2]->level);
18+
$this->assertCount(3, $manager->getPendingTransactions());
19+
$this->assertSame('default', $manager->getPendingTransactions()[0]->connection);
20+
$this->assertEquals(1, $manager->getPendingTransactions()[0]->level);
21+
$this->assertSame('default', $manager->getPendingTransactions()[1]->connection);
22+
$this->assertEquals(2, $manager->getPendingTransactions()[1]->level);
23+
$this->assertSame('admin', $manager->getPendingTransactions()[2]->connection);
24+
$this->assertEquals(1, $manager->getPendingTransactions()[2]->level);
2525
}
2626

2727
public function testRollingBackTransactions()
@@ -34,13 +34,13 @@ public function testRollingBackTransactions()
3434

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

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

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

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

4646
public function testRollingBackTransactionsAllTheWay()
@@ -53,10 +53,10 @@ public function testRollingBackTransactionsAllTheWay()
5353

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

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

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

6262
public function testCommittingTransactions()
@@ -67,12 +67,15 @@ public function testCommittingTransactions()
6767
$manager->begin('default', 2);
6868
$manager->begin('admin', 1);
6969

70+
$manager->stageTransactions('default');
71+
$manager->stageTransactions('admin');
7072
$manager->commit('default');
7173

72-
$this->assertCount(1, $manager->getTransactions());
74+
$this->assertCount(0, $manager->getPendingTransactions());
75+
$this->assertCount(1, $manager->getCommittedTransactions());
7376

74-
$this->assertSame('admin', $manager->getTransactions()[0]->connection);
75-
$this->assertEquals(1, $manager->getTransactions()[0]->level);
77+
$this->assertSame('admin', $manager->getCommittedTransactions()[0]->connection);
78+
$this->assertEquals(1, $manager->getCommittedTransactions()[0]->level);
7679
}
7780

7881
public function testCallbacksAreAddedToTheCurrentTransaction()
@@ -93,9 +96,9 @@ public function testCallbacksAreAddedToTheCurrentTransaction()
9396
$manager->addCallback(function () use (&$callbacks) {
9497
});
9598

96-
$this->assertCount(1, $manager->getTransactions()[0]->getCallbacks());
97-
$this->assertCount(0, $manager->getTransactions()[1]->getCallbacks());
98-
$this->assertCount(1, $manager->getTransactions()[2]->getCallbacks());
99+
$this->assertCount(1, $manager->getPendingTransactions()[0]->getCallbacks());
100+
$this->assertCount(0, $manager->getPendingTransactions()[1]->getCallbacks());
101+
$this->assertCount(1, $manager->getPendingTransactions()[2]->getCallbacks());
99102
}
100103

101104
public function testCommittingTransactionsExecutesCallbacks()
@@ -118,6 +121,7 @@ public function testCommittingTransactionsExecutesCallbacks()
118121

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

124+
$manager->stageTransactions('default');
121125
$manager->commit('default');
122126

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

151+
$manager->stageTransactions('default');
147152
$manager->commit('default');
148153

149154
$this->assertCount(1, $callbacks);
@@ -163,4 +168,33 @@ public function testCallbackIsExecutedIfNoTransactions()
163168
$this->assertCount(1, $callbacks);
164169
$this->assertEquals(['default', 1], $callbacks[0]);
165170
}
171+
172+
public function testStageTransactions()
173+
{
174+
$manager = (new DatabaseTransactionsManager);
175+
176+
$manager->begin('default', 1);
177+
$manager->begin('admin', 1);
178+
179+
$this->assertCount(2, $manager->getPendingTransactions());
180+
181+
$pendingTransactions = $manager->getPendingTransactions();
182+
183+
$this->assertEquals(1, $pendingTransactions[0]->level);
184+
$this->assertEquals('default', $pendingTransactions[0]->connection);
185+
$this->assertEquals(1, $pendingTransactions[1]->level);
186+
$this->assertEquals('admin', $pendingTransactions[1]->connection);
187+
188+
$manager->stageTransactions('default');
189+
190+
$this->assertCount(1, $manager->getPendingTransactions());
191+
$this->assertCount(1, $manager->getCommittedTransactions());
192+
$this->assertEquals('default', $manager->getCommittedTransactions()[0]->connection);
193+
194+
$manager->stageTransactions('admin');
195+
196+
$this->assertCount(0, $manager->getPendingTransactions());
197+
$this->assertCount(2, $manager->getCommittedTransactions());
198+
$this->assertEquals('admin', $manager->getCommittedTransactions()[1]->connection);
199+
}
166200
}

tests/Database/DatabaseTransactionsTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public function testTransactionIsRecordedAndCommitted()
6464
{
6565
$transactionManager = m::mock(new DatabaseTransactionsManager);
6666
$transactionManager->shouldReceive('begin')->once()->with('default', 1);
67+
$transactionManager->shouldReceive('stageTransactions')->once()->with('default');
6768
$transactionManager->shouldReceive('commit')->once()->with('default');
6869

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

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

108111
$this->connection()->setTransactionManager($transactionManager);
@@ -130,6 +133,8 @@ public function testNestedTransactionIsRecordeForDifferentConnectionsdAndCommitt
130133
$transactionManager->shouldReceive('begin')->once()->with('default', 1);
131134
$transactionManager->shouldReceive('begin')->once()->with('second_connection', 1);
132135
$transactionManager->shouldReceive('begin')->once()->with('second_connection', 2);
136+
$transactionManager->shouldReceive('stageTransactions')->once()->with('default');
137+
$transactionManager->shouldReceive('stageTransactions')->twice()->with('second_connection');
133138
$transactionManager->shouldReceive('commit')->once()->with('default');
134139
$transactionManager->shouldReceive('commit')->once()->with('second_connection');
135140

0 commit comments

Comments
 (0)