@@ -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}
0 commit comments