Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .php-cs-fixer.cache
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"php":"8.2.29","version":"3.88.2:v3.88.2#a8d15584bafb0f0d9d938827840060fd4a3ebc99","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"align_single_space_minimal"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":true,"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"sort_algorithm":"alpha"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"single_quote":true,"no_unused_imports":true,"no_superfluous_phpdoc_tags":true,"phpdoc_trim":true,"phpdoc_align":{"align":"left"},"blank_line_before_statement":{"statements":["return"]},"simplified_null_return":true,"void_return":true},"hashes":{"src\/Helper.php":"8ef8db53eed02278815b175b445a2ee9","src\/DBTransactionRetryHelperOld.php":"358e3a95a390c013376e05cb0911b599","src\/DBTransactionRetryHelper.php":"3dfeb60b0234603d2046f39592b6a547","src\/RetryServiceProvider.php":"b6642465f4ed70477d21c0460e3677df","tests\/TestCase.php":"6df2b13208f4952f10b306fad99e1c51","tests\/bootstrap.php":"8af7490a2832c4cce20f0980636bad41","tests\/DBTransactionRetryHelperTest.php":"5e9993c586d9318449b2181ece54bc73","\/tmp\/PHP CS Fixertemp_folder\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder1\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20",".php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder2\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder10\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder4\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder5\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder11\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder9\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder815\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder8\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder3\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder7\/.php-cs-fixer.php":"f08f20b53da80da9c95f886b732fca20","\/tmp\/PHP CS Fixertemp_folder6\/src\/DBTransactionRetryHelper.php":"3dfeb60b0234603d2046f39592b6a547","\/tmp\/PHP CS Fixertemp_folder\/src\/DBTransactionRetryHelper.php":"3dfeb60b0234603d2046f39592b6a547","\/tmp\/PHP CS Fixertemp_folder1\/src\/DBTransactionRetryHelper.php":"3dfeb60b0234603d2046f39592b6a547","tests\/Unit\/ExampleTest.php":"3bbd4ea8029698f723c35a66d8592087","tests\/Unit\/DBTransactionRetryHelperTest.php":"38a42cae2dcaf6fa55519bec4b64e252","tests\/Feature\/ExampleTest.php":"a1e5352ea369ad36f88f4f566c340371","tests\/Pest.php":"44a41307b2bca2c9b747aa2f40c5262b"}}
{"php":"8.4.13","version":"3.88.2:v3.88.2#a8d15584bafb0f0d9d938827840060fd4a3ebc99","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"align_single_space_minimal"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":true,"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"sort_algorithm":"alpha"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"single_quote":true,"no_unused_imports":true,"no_superfluous_phpdoc_tags":true,"phpdoc_trim":true,"phpdoc_align":{"align":"left"},"blank_line_before_statement":{"statements":["return"]},"simplified_null_return":true,"void_return":true},"hashes":{"src\/RetryServiceProvider.php":"b6642465f4ed70477d21c0460e3677df","src\/DBTransactionRetryHelperOld.php":"358e3a95a390c013376e05cb0911b599","src\/DBTransactionRetryHelper.php":"3619e5ff7703069c1f184c1fd247bbce","src\/Helper.php":"8ef8db53eed02278815b175b445a2ee9","tests\/Unit\/DBTransactionRetryHelperTest.php":"38a42cae2dcaf6fa55519bec4b64e252","tests\/Unit\/ExampleTest.php":"3bbd4ea8029698f723c35a66d8592087","tests\/bootstrap.php":"8af7490a2832c4cce20f0980636bad41","tests\/Feature\/ExampleTest.php":"a1e5352ea369ad36f88f4f566c340371","tests\/Pest.php":"44a41307b2bca2c9b747aa2f40c5262b","tests\/TestCase.php":"6df2b13208f4952f10b306fad99e1c51"}}
78 changes: 49 additions & 29 deletions src/DBTransactionRetryHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ public static function transactionWithRetry(Closure $callback, int $maxRetries =
if (is_null($trxLabel)) {
$trxLabel = '';
}
$attempt = 0;
$log = [];

if ($trxLabel !== '') {
app()->instance('tx.label', $trxLabel);
}

$attempt = 0;
$lastDeadlockException = null;
$lastDeadlockAttempt = 0;

while ($attempt < $maxRetries) {
// reset per-attempt flags to avoid stale values
Expand All @@ -37,7 +43,6 @@ public static function transactionWithRetry(Closure $callback, int $maxRetries =

try {
// Execute the transaction
$trxLabel === '' || app()->instance('tx.label', $trxLabel);
$result = DB::transaction($callback);

return $result;
Expand All @@ -50,7 +55,8 @@ public static function transactionWithRetry(Closure $callback, int $maxRetries =

if ($isDeadlock) {
$attempt++;
$log[] = static::buildLogContext($e, $attempt, $maxRetries, $trxLabel);
$lastDeadlockException = $e;
$lastDeadlockAttempt = $attempt;

if ($attempt >= $maxRetries) {
// exhausted retries — throw after logging below in finally
Expand All @@ -63,20 +69,31 @@ public static function transactionWithRetry(Closure $callback, int $maxRetries =
}
} else {
// Non-deadlock: DO NOT log, just rethrow
$throwable = $e;
$lastDeadlockException = null;
$lastDeadlockAttempt = 0;
$throwable = $e;
}
} finally {
if (is_null($throwable) && !$exceptionCatched) {
// Success on first try, nothing to do.
// If you want to warn when there WERE previous retries that succeeded, keep this block:
if (count($log) > 0) {
if ($lastDeadlockException !== null) {
// optional: downgrade to warning for eventual success after retries
generateLog($log[count($log) - 1], $logFileName, 'warning');
generateLog(
static::buildLogContext($lastDeadlockException, $lastDeadlockAttempt, $maxRetries, $trxLabel),
$logFileName,
'warning'
);
$lastDeadlockException = null;
$lastDeadlockAttempt = 0;
}
} elseif (!is_null($throwable)) {
// We only log when it is a DEADLOCK and retries are exhausted.
if ($isDeadlock && count($log) > 0) {
generateLog($log[count($log) - 1], $logFileName);
if ($isDeadlock && $lastDeadlockException !== null) {
generateLog(
static::buildLogContext($lastDeadlockException, $lastDeadlockAttempt, $maxRetries, $trxLabel),
$logFileName
);
}

// For NON-deadlock, nothing is logged — just throw.
Expand Down Expand Up @@ -107,15 +124,18 @@ protected static function buildLogContext(QueryException $e, int $attempt, int $
$bindings = method_exists($e, 'getBindings') ? $e->getBindings() : [];

$connectionName = $e->getConnectionName();
$conn = DB::connection($connectionName);

// if laravel version <= 11.x then getRawSql() is not available and we will do it manually
$rawSql = method_exists($e, 'getRawSql') ? $e->getRawSql() : null;
if (is_null($rawSql) && !is_null($sql) && !empty($bindings)) {
$rawSql = $conn->getQueryGrammar()->substituteBindingsIntoRawSql($sql, $bindings);
if ($rawSql === null && $sql !== null && $bindings !== []) {
static $grammarCache = [];

$key = $connectionName ?? '__default__';
$grammar = $grammarCache[$key] ??= DB::connection($connectionName)->getQueryGrammar();

$rawSql = $grammar->substituteBindingsIntoRawSql($sql, $bindings);
}

$requestData = [
$context = [
'url' => null,
'method' => null,
'token' => null,
Expand All @@ -124,28 +144,28 @@ protected static function buildLogContext(QueryException $e, int $attempt, int $

try {
if (function_exists('request') && app()->bound('request')) {
$req = request();
$requestData['url'] = method_exists($req, 'getUri') ? $req->getUri() : null;
$requestData['method'] = method_exists($req, 'getMethod') ? $req->getMethod() : null;
$req = request();
$context['url'] = method_exists($req, 'getUri') ? $req->getUri() : null;
$context['method'] = method_exists($req, 'getMethod') ? $req->getMethod() : null;
if (method_exists($req, 'header')) {
$auth = $req->header('authorization');
$requestData['authHeaderLen'] = $auth ? strlen($auth) : null;
$auth = $req->header('authorization');
$context['authHeaderLen'] = $auth ? strlen($auth) : null;
}
$requestData['userId'] = method_exists($req, 'user') && $req->user() ? ($req->user()->id ?? null) : null;
$context['userId'] = method_exists($req, 'user') && $req->user() ? ($req->user()->id ?? null) : null;
}
} catch (Throwable) {
// ignore
}

return array_merge($requestData, [
'attempt' => $attempt,
'maxRetries' => $maxRetries,
'trxLabel' => $trxLabel,
'errorInfo' => $e->errorInfo,
'rawSql' => $rawSql,
'connection' => $connectionName,
'trace' => getDebugBacktraceArray(),
]);
$context['attempt'] = $attempt;
$context['maxRetries'] = $maxRetries;
$context['trxLabel'] = $trxLabel;
$context['errorInfo'] = $e->errorInfo;
$context['rawSql'] = $rawSql;
$context['connection'] = $connectionName;
$context['trace'] = getDebugBacktraceArray();

return $context;
}

/**
Expand Down