Skip to content

Commit 4e1ee72

Browse files
committed
Enhance transaction retry logic with improved deadlock handling and logging context
1 parent e0536c6 commit 4e1ee72

File tree

2 files changed

+50
-30
lines changed

2 files changed

+50
-30
lines changed

.php-cs-fixer.cache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +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"}}
1+
{"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"}}

src/DBTransactionRetryHelper.php

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,14 @@ public static function transactionWithRetry(Closure $callback, int $maxRetries =
2626
if (is_null($trxLabel)) {
2727
$trxLabel = '';
2828
}
29-
$attempt = 0;
30-
$log = [];
29+
30+
if ($trxLabel !== '') {
31+
app()->instance('tx.label', $trxLabel);
32+
}
33+
34+
$attempt = 0;
35+
$lastDeadlockException = null;
36+
$lastDeadlockAttempt = 0;
3137

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

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

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

5156
if ($isDeadlock) {
5257
$attempt++;
53-
$log[] = static::buildLogContext($e, $attempt, $maxRetries, $trxLabel);
58+
$lastDeadlockException = $e;
59+
$lastDeadlockAttempt = $attempt;
5460

5561
if ($attempt >= $maxRetries) {
5662
// exhausted retries — throw after logging below in finally
@@ -63,20 +69,31 @@ public static function transactionWithRetry(Closure $callback, int $maxRetries =
6369
}
6470
} else {
6571
// Non-deadlock: DO NOT log, just rethrow
66-
$throwable = $e;
72+
$lastDeadlockException = null;
73+
$lastDeadlockAttempt = 0;
74+
$throwable = $e;
6775
}
6876
} finally {
6977
if (is_null($throwable) && !$exceptionCatched) {
7078
// Success on first try, nothing to do.
7179
// If you want to warn when there WERE previous retries that succeeded, keep this block:
72-
if (count($log) > 0) {
80+
if ($lastDeadlockException !== null) {
7381
// optional: downgrade to warning for eventual success after retries
74-
generateLog($log[count($log) - 1], $logFileName, 'warning');
82+
generateLog(
83+
static::buildLogContext($lastDeadlockException, $lastDeadlockAttempt, $maxRetries, $trxLabel),
84+
$logFileName,
85+
'warning'
86+
);
87+
$lastDeadlockException = null;
88+
$lastDeadlockAttempt = 0;
7589
}
7690
} elseif (!is_null($throwable)) {
7791
// We only log when it is a DEADLOCK and retries are exhausted.
78-
if ($isDeadlock && count($log) > 0) {
79-
generateLog($log[count($log) - 1], $logFileName);
92+
if ($isDeadlock && $lastDeadlockException !== null) {
93+
generateLog(
94+
static::buildLogContext($lastDeadlockException, $lastDeadlockAttempt, $maxRetries, $trxLabel),
95+
$logFileName
96+
);
8097
}
8198

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

109126
$connectionName = $e->getConnectionName();
110-
$conn = DB::connection($connectionName);
111-
112127
// if laravel version <= 11.x then getRawSql() is not available and we will do it manually
113128
$rawSql = method_exists($e, 'getRawSql') ? $e->getRawSql() : null;
114-
if (is_null($rawSql) && !is_null($sql) && !empty($bindings)) {
115-
$rawSql = $conn->getQueryGrammar()->substituteBindingsIntoRawSql($sql, $bindings);
129+
if ($rawSql === null && $sql !== null && $bindings !== []) {
130+
static $grammarCache = [];
131+
132+
$key = $connectionName ?? '__default__';
133+
$grammar = $grammarCache[$key] ??= DB::connection($connectionName)->getQueryGrammar();
134+
135+
$rawSql = $grammar->substituteBindingsIntoRawSql($sql, $bindings);
116136
}
117137

118-
$requestData = [
138+
$context = [
119139
'url' => null,
120140
'method' => null,
121141
'token' => null,
@@ -124,28 +144,28 @@ protected static function buildLogContext(QueryException $e, int $attempt, int $
124144

125145
try {
126146
if (function_exists('request') && app()->bound('request')) {
127-
$req = request();
128-
$requestData['url'] = method_exists($req, 'getUri') ? $req->getUri() : null;
129-
$requestData['method'] = method_exists($req, 'getMethod') ? $req->getMethod() : null;
147+
$req = request();
148+
$context['url'] = method_exists($req, 'getUri') ? $req->getUri() : null;
149+
$context['method'] = method_exists($req, 'getMethod') ? $req->getMethod() : null;
130150
if (method_exists($req, 'header')) {
131-
$auth = $req->header('authorization');
132-
$requestData['authHeaderLen'] = $auth ? strlen($auth) : null;
151+
$auth = $req->header('authorization');
152+
$context['authHeaderLen'] = $auth ? strlen($auth) : null;
133153
}
134-
$requestData['userId'] = method_exists($req, 'user') && $req->user() ? ($req->user()->id ?? null) : null;
154+
$context['userId'] = method_exists($req, 'user') && $req->user() ? ($req->user()->id ?? null) : null;
135155
}
136156
} catch (Throwable) {
137157
// ignore
138158
}
139159

140-
return array_merge($requestData, [
141-
'attempt' => $attempt,
142-
'maxRetries' => $maxRetries,
143-
'trxLabel' => $trxLabel,
144-
'errorInfo' => $e->errorInfo,
145-
'rawSql' => $rawSql,
146-
'connection' => $connectionName,
147-
'trace' => getDebugBacktraceArray(),
148-
]);
160+
$context['attempt'] = $attempt;
161+
$context['maxRetries'] = $maxRetries;
162+
$context['trxLabel'] = $trxLabel;
163+
$context['errorInfo'] = $e->errorInfo;
164+
$context['rawSql'] = $rawSql;
165+
$context['connection'] = $connectionName;
166+
$context['trace'] = getDebugBacktraceArray();
167+
168+
return $context;
149169
}
150170

151171
/**

0 commit comments

Comments
 (0)