Files
citrus-cms/app/Helpers/TransactionHelper.php
T
2026-04-28 21:14:25 +03:00

169 lines
6.1 KiB
PHP

<?php
namespace App\Helpers;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Exception;
class TransactionHelper
{
/**
* Execute a database transaction with exponential backoff retry mechanism
*
* @param callable $callback The callback function to execute
* @param int $maxAttempts Maximum number of retry attempts
* @param int $baseDelay Base delay in milliseconds for exponential backoff
* @param int $maxDelay Maximum delay in milliseconds
* @return mixed Result from the callback
* @throws Exception
*/
public static function retryTransaction(callable $callback, int $maxAttempts = 5, int $baseDelay = 100, int $maxDelay = 5000)
{
$attempt = 0;
while ($attempt < $maxAttempts) {
try {
return DB::transaction($callback);
} catch (\Illuminate\Database\QueryException $e) {
$attempt++;
// Check if it's a lock wait timeout or deadlock error
$isLockError = self::isLockRelatedError($e);
if (!$isLockError || $attempt >= $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()
]);
}
}
}