Skip to content

Commit e7890aa

Browse files
committed
Improves DB transaction retry mechanism
Enhances the database transaction retry helper by reducing default retries and delay for quicker failure detection. Introduces detailed logging for deadlock or serialization errors, including request context for debugging. Implements exponential backoff with jitter to avoid thundering herd issues. Improves the log file path generation by adding the date at the beginning.
1 parent e5efc9d commit e7890aa

File tree

2 files changed

+75
-8
lines changed

2 files changed

+75
-8
lines changed

src/DBTransactionRetryHelper.php

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class DBTransactionRetryHelper
1919
* @throws QueryException
2020
* @throws Throwable
2121
*/
22-
public static function transactionWithRetry(callable $callback, int $maxRetries = 5, int $retryDelay = 5, string $logFileName = 'mysql-deadlocks-log'): mixed
22+
public static function transactionWithRetry(callable $callback, int $maxRetries = 3, int $retryDelay = 2, string $logFileName = 'mysql-deadlocks'): mixed
2323
{
2424
$attempt = 0;
2525
$throwable = null;
@@ -80,5 +80,64 @@ public static function transactionWithRetry(callable $callback, int $maxRetries
8080
}
8181
}
8282
}
83+
84+
// If we exit the loop without returning, throw a generic runtime exception
85+
throw new \RuntimeException('Transaction with retry exhausted after ' . $maxRetries . ' attempts.');
86+
}
87+
88+
protected static function isDeadlockOrSerializationError(QueryException $e): bool
89+
{
90+
// MySQL deadlock: driver error 1213; lock wait timeout: 1205 (often not retryable); SQLSTATE 40001 serialization failure
91+
$sqlState = $e->getCode(); // In Laravel, getCode often returns SQLSTATE (e.g., '40001')
92+
$driverErr = is_array($e->errorInfo ?? null) && isset($e->errorInfo[1]) ? $e->errorInfo[1] : null;
93+
94+
return ($sqlState === '40001')
95+
|| ($driverErr === 1213)
96+
|| ($sqlState === 1213) // in case driver bubbles numeric
97+
;
98+
}
99+
100+
protected static function buildLogContext(QueryException $e, int $attempt): array
101+
{
102+
$requestData = [
103+
'url' => null,
104+
'method' => null,
105+
'token' => null,
106+
'userId' => null,
107+
];
108+
109+
try {
110+
// Only access request() when available (HTTP context)
111+
if (function_exists('request') && app()->bound('request')) {
112+
$req = request();
113+
$requestData['url'] = method_exists($req, 'getUri') ? $req->getUri() : null;
114+
$requestData['method'] = method_exists($req, 'getMethod') ? $req->getMethod() : null;
115+
$requestData['token'] = method_exists($req, 'header') ? ($req->header('authorization') ?? null) : null;
116+
$requestData['userId'] = method_exists($req, 'user') && $req->user() ? ($req->user()->id ?? null) : null;
117+
}
118+
} catch (Throwable) {
119+
// ignore request context errors for CLI/queue
120+
}
121+
122+
return array_merge([
123+
'attempt' => $attempt,
124+
'errorInfo' => $e->errorInfo,
125+
'ExceptionName' => get_class($e),
126+
'QueryException' => $e->getMessage(),
127+
'trace' => getDebugBacktraceArray() ?? null,
128+
], $requestData);
129+
}
130+
131+
/**
132+
* @throws RandomException
133+
*/
134+
protected static function backoffDelay(int $baseDelay, int $attempt): int
135+
{
136+
// Simple exponential backoff with jitter: baseDelay * 2^(attempt-1) +/- 25%
137+
$delay = max(1, (int)round($baseDelay * pow(2, max(0, $attempt - 1))));
138+
$jitter = max(0, (int)round($delay * 0.25));
139+
$min = max(1, $delay - $jitter);
140+
$max = $delay + $jitter;
141+
return random_int($min, $max);
83142
}
84143
}

src/Helper.php

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,27 @@ function getDebugBacktraceArray(): array
2222
if (!function_exists('generateLog')) {
2323
function generateLog($var, $logFileName, $logType = 'error'): void
2424
{
25-
if (is_null($logFileName)) {
26-
$logFilePath = storage_path('logs/general_' . now()->toDateString() . '.log');
25+
$date = function_exists('now') ? now()->toDateString() : date('Y-m-d');
26+
27+
if (empty($logFileName)) {
28+
$logFilePath = storage_path('logs/' . date('Y-m-d') . 'general_' . $date . '.log');
2729
} else {
28-
$logFilePath = storage_path("logs/{$logFileName}_" . now()->toDateString() . '.log');
30+
$logFilePath = storage_path("logs/" . date('Y-m-d') . "{$logFileName}_" . $date . '.log');
2931
}
3032
$log = Log::build([
3133
'driver' => 'single',
3234
'path' => $logFilePath,
3335
]);
34-
if ($logType == 'error') {
35-
$log->error(var_export($var, true));
36-
} elseif ($logType == 'warning') {
37-
$log->warning(var_export($var, true));
36+
$payload = is_array($var) ? $var : ['message' => (string)$var];
37+
$attempts = $var['attempt'] ?? 0;
38+
$errorInfo = $var['errorInfo'][2] ?? '';
39+
40+
if ($logType === 'error') {
41+
$log->error($attempts . ' ' . $errorInfo, $payload);
42+
} elseif ($logType === 'warning') {
43+
$log->warning($attempts . ' ' . $errorInfo, $payload);
44+
} else {
45+
$log->info($attempts . ' ' . $errorInfo, $payload);
3846
}
3947
}
4048
}

0 commit comments

Comments
 (0)