Skip to content

Commit 669b9c1

Browse files
committed
release
2 parents 0423600 + 9a0ee11 commit 669b9c1

File tree

3 files changed

+347
-18
lines changed

3 files changed

+347
-18
lines changed

src/DBTransactionRetryHelper.php

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,31 @@
22

33
namespace MysqlDeadlocks\RetryHelper;
44

5+
use Closure;
56
use Random\RandomException;
67
use Throwable;
78
use Illuminate\Support\Facades\DB;
89
use Illuminate\Database\QueryException;
9-
use Illuminate\Support\Facades\Log;
10-
use Illuminate\Support\Facades\App;
1110

1211
class DBTransactionRetryHelper
1312
{
1413
/**
1514
* Perform a database transaction with retry logic in case of deadlocks.
1615
*
17-
* @param callable $callback The transaction logic to execute.
16+
* @param Closure $callback The transaction logic to execute.
1817
* @param int $maxRetries Number of times to retry on deadlock.
1918
* @param int $retryDelay Delay between retries in seconds.
2019
* @param string $logFileName The log file name
20+
* @param string $trxLabel The transaction label
2121
* @return mixed
22-
* @throws QueryException
22+
* @throws RandomException
2323
* @throws Throwable
2424
*/
25-
public static function transactionWithRetry(callable $callback, int $maxRetries = 3, int $retryDelay = 2, string $logFileName = 'mysql-deadlocks'): mixed
25+
public static function transactionWithRetry(Closure $callback, int $maxRetries = 3, int $retryDelay = 2, string $logFileName = 'database/mysql-deadlocks', string $trxLabel = ''): mixed
2626
{
2727
$attempt = 0;
2828
$log = [];
29+
$isDeadlock = false;
2930

3031
while ($attempt < $maxRetries) {
3132
$throwable = null;
@@ -44,7 +45,7 @@ public static function transactionWithRetry(callable $callback, int $maxRetries
4445

4546
if ($isDeadlock) {
4647
$attempt++;
47-
$log[] = static::buildLogContext($e, $attempt);
48+
$log[] = static::buildLogContext($e, $attempt, $maxRetries, $trxLabel);
4849

4950
if ($attempt >= $maxRetries) {
5051
$throwable = $e;
@@ -65,8 +66,13 @@ public static function transactionWithRetry(callable $callback, int $maxRetries
6566
generateLog($log[count($log) - 1], $logFileName, 'warning');
6667
}
6768
} elseif (!is_null($throwable)) {
69+
// Ensure non-deadlock exceptions are logged
6870
if (count($log) > 0) {
6971
generateLog($log[count($log) - 1], $logFileName);
72+
} else if (!$isDeadlock && $throwable instanceof QueryException) {
73+
// Log non-deadlock QueryException immediately
74+
$context = static::buildLogContext($throwable, $attempt, $maxRetries, $trxLabel);
75+
generateLog($context, $logFileName);
7076
}
7177
throw $throwable;
7278
}
@@ -89,8 +95,21 @@ protected static function isDeadlockOrSerializationError(QueryException $e): boo
8995
;
9096
}
9197

92-
protected static function buildLogContext(QueryException $e, int $attempt): array
98+
protected static function buildLogContext(QueryException $e, int $attempt, int $maxRetries, string $trxLabel): array
9399
{
100+
// Extract sql & bindings safely
101+
$sql = method_exists($e, 'getSql') ? $e->getSql() : null;
102+
$bindings = method_exists($e, 'getBindings') ? $e->getBindings() : [];
103+
104+
// Try to read connection name
105+
$connectionName = null;
106+
try {
107+
$connection = DB::connection();
108+
$connectionName = $connection?->getName();
109+
} catch (Throwable) {
110+
// ignore
111+
}
112+
94113
$requestData = [
95114
'url' => null,
96115
'method' => null,
@@ -99,25 +118,32 @@ protected static function buildLogContext(QueryException $e, int $attempt): arra
99118
];
100119

101120
try {
102-
// Only access request() when available (HTTP context)
103121
if (function_exists('request') && app()->bound('request')) {
104122
$req = request();
105123
$requestData['url'] = method_exists($req, 'getUri') ? $req->getUri() : null;
106124
$requestData['method'] = method_exists($req, 'getMethod') ? $req->getMethod() : null;
107-
$requestData['token'] = method_exists($req, 'header') ? ($req->header('authorization') ?? null) : null;
125+
if (method_exists($req, 'header')) {
126+
$auth = $req->header('authorization');
127+
$requestData['authHeaderLen'] = $auth ? strlen($auth) : null;
128+
}
108129
$requestData['userId'] = method_exists($req, 'user') && $req->user() ? ($req->user()->id ?? null) : null;
109130
}
110131
} catch (Throwable) {
111-
// ignore request context errors for CLI/queue
132+
// ignore
112133
}
113134

114-
return array_merge([
135+
return array_merge($requestData, [
115136
'attempt' => $attempt,
137+
'maxRetries' => $maxRetries,
138+
'trxLabel' => $trxLabel,
139+
'Exception' => get_class($e),
140+
'message' => $e->getMessage(),
141+
'sql' => $sql,
142+
'bindings' => static::stringifyBindings($bindings),
116143
'errorInfo' => $e->errorInfo,
117-
'ExceptionName' => get_class($e),
118-
'QueryException' => $e->getMessage(),
119-
'trace' => getDebugBacktraceArray() ?? null,
120-
], $requestData);
144+
'connection' => $connectionName,
145+
'trace' => static::safeTrace(),
146+
]);
121147
}
122148

123149
/**
@@ -132,4 +158,49 @@ protected static function backoffDelay(int $baseDelay, int $attempt): int
132158
$max = $delay + $jitter;
133159
return random_int($min, $max);
134160
}
161+
162+
protected static function stringifyBindings(array $bindings): array
163+
{
164+
return array_map(function ($b) {
165+
if ($b instanceof \DateTimeInterface) {
166+
return $b->format('Y-m-d H:i:s.u');
167+
}
168+
if (is_object($b)) {
169+
return '[object ' . get_class($b) . ']';
170+
}
171+
if (is_resource($b)) {
172+
return '[resource]';
173+
}
174+
if (is_string($b)) {
175+
// Trim very long strings to avoid log bloat
176+
return mb_strlen($b) > 500 ? (mb_substr($b, 0, 500) . '…[+trimmed]') : $b;
177+
}
178+
if (is_array($b)) {
179+
// Compact arrays
180+
$json = @json_encode($b, JSON_UNESCAPED_UNICODE);
181+
182+
return $json !== false
183+
? (mb_strlen($json) > 500 ? (mb_substr($json, 0, 500) . '…[+trimmed]') : $json)
184+
: '[array]';
185+
}
186+
187+
return $b;
188+
}, $bindings);
189+
}
190+
191+
protected static function safeTrace(): array
192+
{
193+
try {
194+
return collect(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 15))
195+
->map(fn($f) => [
196+
'file' => $f['file'] ?? null,
197+
'line' => $f['line'] ?? null,
198+
'function' => $f['function'] ?? null,
199+
'class' => $f['class'] ?? null,
200+
'type' => $f['type'] ?? null,
201+
])->all();
202+
} catch (Throwable) {
203+
return [];
204+
}
205+
}
135206
}

0 commit comments

Comments
 (0)