22
33namespace MysqlDeadlocks \RetryHelper ;
44
5+ use Closure ;
56use Random \RandomException ;
67use Throwable ;
78use Illuminate \Support \Facades \DB ;
89use Illuminate \Database \QueryException ;
9- use Illuminate \Support \Facades \Log ;
10- use Illuminate \Support \Facades \App ;
1110
1211class 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