Skip to content

Commit 8bff1ef

Browse files
authored
feat(database): add transaction retry attempts (#10197)
1 parent 170b89a commit 8bff1ef

8 files changed

Lines changed: 495 additions & 38 deletions

File tree

system/Database/BaseConnection.php

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use CodeIgniter\Database\Exceptions\RetryableTransactionException;
1919
use CodeIgniter\Database\Exceptions\UniqueConstraintViolationException;
2020
use CodeIgniter\Events\Events;
21+
use CodeIgniter\Exceptions\InvalidArgumentException;
2122
use CodeIgniter\I18n\Time;
2223
use Exception;
2324
use ReflectionClass;
@@ -228,6 +229,11 @@ abstract class BaseConnection implements ConnectionInterface
228229
*/
229230
protected ?DatabaseException $lastException = null;
230231

232+
/**
233+
* The first database exception that caused the current transaction to fail.
234+
*/
235+
protected ?DatabaseException $transFailureException = null;
236+
231237
/**
232238
* Connection ID
233239
*
@@ -860,7 +866,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s
860866
$query->setDuration($startTime, $startTime);
861867

862868
// This will trigger a rollback if transactions are being used
863-
$this->handleTransStatus();
869+
$this->handleTransStatus($exception ?? $this->lastException);
864870

865871
if (
866872
$this->DBDebug
@@ -1082,44 +1088,67 @@ public function afterRollback(callable $callback): static
10821088
* @template TReturn
10831089
*
10841090
* @param callable(self): TReturn $callback
1091+
* @param positive-int $attempts
10851092
*
10861093
* @return false|TReturn
10871094
*/
1088-
public function transaction(callable $callback): mixed
1095+
public function transaction(callable $callback, int $attempts = 1): mixed
10891096
{
1097+
if ($attempts < 1) {
1098+
throw new InvalidArgumentException('Transaction attempts must be a positive integer.');
1099+
}
1100+
10901101
if (! $this->transEnabled) {
10911102
return $callback($this);
10921103
}
10931104

1094-
if (! $this->transBegin()) {
1095-
return false;
1096-
}
1105+
$attempts = $this->transDepth === 0 ? $attempts : 1;
1106+
1107+
for ($attempt = 1; $attempt <= $attempts; $attempt++) {
1108+
if (! $this->transBegin()) {
1109+
return false;
1110+
}
10971111

1098-
try {
1099-
$result = $callback($this);
1100-
} catch (Throwable $e) {
11011112
try {
1102-
$this->transRollback();
1103-
} catch (Throwable $rollbackException) {
1104-
log_message('error', 'Database: Transaction callback threw an exception before rollback failed: ' . $e);
1113+
$result = $callback($this);
1114+
} catch (Throwable $e) {
1115+
try {
1116+
$this->transRollback();
1117+
} catch (Throwable $rollbackException) {
1118+
log_message('error', 'Database: Transaction callback threw an exception before rollback failed: ' . $e);
1119+
1120+
throw $rollbackException;
1121+
} finally {
1122+
if ($this->transDepth > 0) {
1123+
$this->transStatus = false;
1124+
} elseif ($this->transStrict === false) {
1125+
$this->transStatus = true;
1126+
}
1127+
}
11051128

1106-
throw $rollbackException;
1107-
} finally {
1108-
if ($this->transDepth > 0) {
1109-
$this->transStatus = false;
1110-
} elseif ($this->transStrict === false) {
1111-
$this->transStatus = true;
1129+
if ($this->transDepth === 0 && $e instanceof RetryableTransactionException && $attempt < $attempts) {
1130+
$this->prepareTransactionRetry();
1131+
1132+
continue;
11121133
}
1134+
1135+
throw $e;
11131136
}
11141137

1115-
throw $e;
1116-
}
1138+
if (! $this->transComplete()) {
1139+
if ($this->transDepth === 0 && $this->transFailureException instanceof RetryableTransactionException && $attempt < $attempts) {
1140+
$this->prepareTransactionRetry();
11171141

1118-
if (! $this->transComplete()) {
1119-
return false;
1142+
continue;
1143+
}
1144+
1145+
return false;
1146+
}
1147+
1148+
return $result;
11201149
}
11211150

1122-
return $result;
1151+
return false;
11231152
}
11241153

11251154
/**
@@ -1145,7 +1174,8 @@ public function transBegin(bool $testMode = false): bool
11451174
// Reset the transaction failure flag.
11461175
// If the $testMode flag is set to TRUE transactions will be rolled back
11471176
// even if the queries produce a successful result.
1148-
$this->transFailure = $testMode;
1177+
$this->transFailure = $testMode;
1178+
$this->transFailureException = null;
11491179

11501180
if ($this->_transBegin()) {
11511181
$this->transDepth++;
@@ -1219,13 +1249,24 @@ public function resetTransStatus(): static
12191249
*
12201250
* @internal This method is for internal database component use only
12211251
*/
1222-
public function handleTransStatus(): void
1252+
public function handleTransStatus(?DatabaseException $exception = null): void
12231253
{
12241254
if ($this->transDepth !== 0) {
12251255
$this->transStatus = false;
1256+
$this->transFailureException ??= $exception;
12261257
}
12271258
}
12281259

1260+
/**
1261+
* Reset transaction state that should not leak into a retry attempt.
1262+
*/
1263+
protected function prepareTransactionRetry(): void
1264+
{
1265+
$this->transStatus = true;
1266+
$this->transFailureException = null;
1267+
$this->lastException = null;
1268+
}
1269+
12291270
/**
12301271
* Run and clear callbacks registered for a successful transaction commit.
12311272
*/

system/Database/BasePreparedQuery.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,11 @@ public function execute(...$data)
141141
if ($result === false) {
142142
$query->setDuration($startTime, $startTime);
143143

144-
// This will trigger a rollback if transactions are being used
145-
$this->db->handleTransStatus();
146-
147144
$databaseException = $this->createDatabaseException($exception);
148145

146+
// This will trigger a rollback if transactions are being used
147+
$this->db->handleTransStatus($databaseException);
148+
149149
if ($this->db->DBDebug) {
150150
// We call this function in order to roll-back queries
151151
// if transactions are enabled. If we don't call this here

system/Database/ConnectionInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,11 @@ public function afterRollback(callable $callback): static;
144144
* @template TReturn
145145
*
146146
* @param callable(self): TReturn $callback
147+
* @param positive-int $attempts
147148
*
148149
* @return false|TReturn
149150
*/
150-
public function transaction(callable $callback): mixed;
151+
public function transaction(callable $callback, int $attempts = 1): mixed;
151152

152153
/**
153154
* Returns an instance of the query builder for this connection.

0 commit comments

Comments
 (0)