= $maxAttempts) { Log::error("Transaction failed after {$attempt} attempts", [ 'error' => $e->getMessage(), 'sql_state' => $e->errorInfo[0] ?? 'unknown', 'error_code' => $e->errorInfo[1] ?? 'unknown' ]); throw $e; } // Calculate exponential backoff delay with jitter $delay = min( $baseDelay * pow(2, $attempt - 1) + rand(0, 50), $maxDelay ); Log::warning("Transaction lock error, retrying attempt {$attempt}/{$maxAttempts}", [ 'error' => $e->getMessage(), 'delay_ms' => $delay, 'sql_state' => $e->errorInfo[0] ?? 'unknown' ]); // Wait before retrying (convert ms to microseconds) usleep($delay * 1000); } } throw new Exception("Transaction failed after {$maxAttempts} attempts"); } /** * Check if the exception is related to lock/deadlock errors * * @param \Illuminate\Database\QueryException $e * @return bool */ private static function isLockRelatedError(\Illuminate\Database\QueryException $e): bool { $lockErrors = [ '40001', // Serialization failure / Deadlock 'HY000', // General error (includes lock wait timeout) '1205', // Lock wait timeout exceeded '1213', // Deadlock found when trying to get lock ]; $errorCode = $e->errorInfo[1] ?? null; $sqlState = $e->errorInfo[0] ?? null; $message = strtolower($e->getMessage()); // Check error codes if (in_array($sqlState, $lockErrors) || in_array($errorCode, $lockErrors)) { return true; } // Check error message for lock-related keywords $lockKeywords = ['lock wait timeout', 'deadlock', 'lock timeout']; foreach ($lockKeywords as $keyword) { if (strpos($message, $keyword) !== false) { return true; } } return false; } /** * Execute a batch of operations in smaller chunks with transactions * * @param \Illuminate\Support\Collection|array $items Items to process * @param callable $callback Callback to execute for each chunk * @param int $chunkSize Size of each chunk * @param int $delayBetweenChunks Delay in microseconds between chunks * @return array Results from each chunk */ public static function chunkTransaction($items, callable $callback, int $chunkSize = 5, int $delayBetweenChunks = 100000) { $collection = collect($items); $chunks = $collection->chunk($chunkSize); $results = []; foreach ($chunks as $index => $chunk) { try { $result = self::retryTransaction(function () use ($callback, $chunk) { return $callback($chunk); }); $results[] = $result; // Add delay between chunks to prevent overwhelming the database if ($index < $chunks->count() - 1) { usleep($delayBetweenChunks); } } catch (Exception $e) { Log::error("Chunk transaction failed", [ 'chunk_index' => $index, 'chunk_size' => $chunk->count(), 'error' => $e->getMessage() ]); // Decide whether to continue or stop // For now, we'll rethrow the exception throw $e; } } return $results; } /** * Set database session timeout values * * @param int $lockWaitTimeout Lock wait timeout in seconds * @param int $waitTimeout General wait timeout in seconds * @param int $interactiveTimeout Interactive timeout in seconds */ public static function setDatabaseTimeouts(int $lockWaitTimeout = 300, int $waitTimeout = 600, int $interactiveTimeout = 600) { try { DB::statement("SET SESSION innodb_lock_wait_timeout = {$lockWaitTimeout}"); DB::statement("SET SESSION lock_wait_timeout = {$lockWaitTimeout}"); DB::statement("SET SESSION wait_timeout = {$waitTimeout}"); DB::statement("SET SESSION interactive_timeout = {$interactiveTimeout}"); Log::info("Database timeouts set successfully", [ 'lock_wait_timeout' => $lockWaitTimeout, 'wait_timeout' => $waitTimeout, 'interactive_timeout' => $interactiveTimeout ]); } catch (Exception $e) { Log::warning("Failed to set database timeouts", [ 'error' => $e->getMessage() ]); } } }