169 lines
6.1 KiB
PHP
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()
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|