Skip to content

Commit 7d65ce0

Browse files
feat: Add a callback to be called on transaction failure (#55338)
* feat: Add a callback to be called on transaction failure * formatting --------- Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent b562df0 commit 7d65ce0

File tree

4 files changed

+54
-6
lines changed

4 files changed

+54
-6
lines changed

src/Illuminate/Database/Concerns/ManagesTransactions.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ trait ManagesTransactions
1616
*
1717
* @param (\Closure(static): TReturn) $callback
1818
* @param int $attempts
19+
* @param Closure|null $onFailure
1920
* @return TReturn
2021
*
2122
* @throws \Throwable
2223
*/
23-
public function transaction(Closure $callback, $attempts = 1)
24+
public function transaction(Closure $callback, $attempts = 1, ?Closure $onFailure = null)
2425
{
2526
for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
2627
$this->beginTransaction();
@@ -37,7 +38,7 @@ public function transaction(Closure $callback, $attempts = 1)
3738
// exception back out, and let the developer handle an uncaught exception.
3839
catch (Throwable $e) {
3940
$this->handleTransactionException(
40-
$e, $currentAttempt, $attempts
41+
$e, $currentAttempt, $attempts, $onFailure
4142
);
4243

4344
continue;
@@ -78,11 +79,12 @@ public function transaction(Closure $callback, $attempts = 1)
7879
* @param \Throwable $e
7980
* @param int $currentAttempt
8081
* @param int $maxAttempts
82+
* @param Closure|null $onFailure
8183
* @return void
8284
*
8385
* @throws \Throwable
8486
*/
85-
protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts)
87+
protected function handleTransactionException(Throwable $e, $currentAttempt, $maxAttempts, ?Closure $onFailure)
8688
{
8789
// On a deadlock, MySQL rolls back the entire transaction so we can't just
8890
// retry the query. We have to throw this exception all the way out and
@@ -108,6 +110,10 @@ protected function handleTransactionException(Throwable $e, $currentAttempt, $ma
108110
return;
109111
}
110112

113+
if ($onFailure !== null) {
114+
$onFailure($e);
115+
}
116+
111117
throw $e;
112118
}
113119

src/Illuminate/Database/ConnectionInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,12 @@ public function prepareBindings(array $bindings);
131131
*
132132
* @param \Closure $callback
133133
* @param int $attempts
134+
* @param Closure|null $onFailure
134135
* @return mixed
135136
*
136137
* @throws \Throwable
137138
*/
138-
public function transaction(Closure $callback, $attempts = 1);
139+
public function transaction(Closure $callback, $attempts = 1, ?Closure $onFailure = null);
139140

140141
/**
141142
* Start a new database transaction.

src/Illuminate/Database/SqlServerConnection.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,16 @@ public function getDriverTitle()
2727
*
2828
* @param \Closure $callback
2929
* @param int $attempts
30+
* @param Closure|null $onFailure
3031
* @return mixed
3132
*
3233
* @throws \Throwable
3334
*/
34-
public function transaction(Closure $callback, $attempts = 1)
35+
public function transaction(Closure $callback, $attempts = 1, ?Closure $onFailure = null)
3536
{
3637
for ($a = 1; $a <= $attempts; $a++) {
3738
if ($this->getDriverName() === 'sqlsrv') {
38-
return parent::transaction($callback, $attempts);
39+
return parent::transaction($callback, $attempts, $onFailure);
3940
}
4041

4142
$this->getPdo()->exec('BEGIN TRAN');
@@ -55,6 +56,10 @@ public function transaction(Closure $callback, $attempts = 1)
5556
catch (Throwable $e) {
5657
$this->getPdo()->exec('ROLLBACK TRAN');
5758

59+
if ($a === $attempts && $onFailure !== null) {
60+
$onFailure($e);
61+
}
62+
5863
throw $e;
5964
}
6065

tests/Integration/Database/DatabaseTransactionsTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,42 @@ public function testTransactionsDoNotAffectDifferentConnections()
105105
$this->assertTrue($secondObject->ran);
106106
$this->assertFalse($thirdObject->ran);
107107
}
108+
109+
public function testOnErrorCallbackIsCalled()
110+
{
111+
$executed = false;
112+
try {
113+
DB::transaction(function () {
114+
throw new \Exception;
115+
}, 1, function () use (&$executed) {
116+
$executed = true;
117+
});
118+
} catch (\Throwable) {
119+
// Ignore the exception
120+
}
121+
122+
$this->assertTrue($executed);
123+
}
124+
125+
public function testOnErrorCallbackIsCalledWithDeadlockRetry()
126+
{
127+
$executed = false;
128+
$attempts = 0;
129+
130+
try {
131+
DB::transaction(function () use (&$attempts) {
132+
$attempts += 1;
133+
throw new \Exception('has been chosen as the deadlock victim');
134+
}, 3, function () use (&$executed) {
135+
$executed = true;
136+
});
137+
} catch (\Throwable) {
138+
// Ignore the exception
139+
}
140+
141+
$this->assertSame(3, $attempts);
142+
$this->assertTrue($executed);
143+
}
108144
}
109145

110146
class TestObjectForTransactions

0 commit comments

Comments
 (0)