Skip to content

Commit 28c9de2

Browse files
committed
gitflow-feature-stash: update-readme.md
1 parent 81e71e8 commit 28c9de2

File tree

2 files changed

+93
-33
lines changed

2 files changed

+93
-33
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.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":"3dfeb60b0234603d2046f39592b6a547","src\/Helper.php":"8ef8db53eed02278815b175b445a2ee9"}}
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"}}

README.md

Lines changed: 92 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,102 @@
1-
# laravel-mysql-deadlock-retry
1+
# Laravel MySQL Deadlock Retry
22

3-
A lightweight helper to run Laravel database transactions with automatic retries on MySQL deadlocks and serialization failures.
3+
![CI](https://github.com/Ahed92Wakim/laravel-mysql-deadlock-retry/actions/workflows/ci.yml/badge.svg?event=pull_request)
4+
![PHP](https://img.shields.io/badge/php-8.2-blue)
5+
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
46

5-
Features:
6-
- Retries DB::transaction on MySQL deadlocks (error 1213) and SQLSTATE 40001
7-
- Exponential backoff with jitter between attempts
8-
- Structured logging per attempt to storage/logs
9-
- Safe in HTTP, CLI and queue contexts (request info captured when available)
10-
- Transaction labeling for easier debugging
11-
- Enhanced logging with SQL query information
7+
Resilient database transactions for Laravel applications that need to gracefully handle MySQL deadlocks and serialization failures. This helper wraps `DB::transaction()` with targeted retries, structured logging, and exponential backoff so you can keep your business logic simple while surviving transient contention.
128

13-
Installation:
14-
- Require the package via Composer: `composer require ahed92wakim/laravel-mysql-deadlock-retry`
9+
## Highlights
10+
- Retries only known transient failure scenarios (MySQL driver error `1213` and SQLSTATE `40001`), leaving all other exceptions untouched.
11+
- Exponential backoff with jitter between attempts to reduce stampedes under load.
12+
- Structured logs with request metadata, SQL, bindings, connection information, and stack traces written to dated files under `storage/logs/{Y-m-d}`.
13+
- Safe in HTTP, CLI, and queue contexts: request data is collected when available and ignored when not.
14+
- Optional transaction labels and custom log file names for easier traceability across microservices and jobs.
15+
- Laravel package auto-discovery; no manual service provider registration required.
1516

16-
Usage:
17+
## Installation
18+
19+
```bash
20+
composer require ahed92wakim/laravel-mysql-deadlock-retry
21+
```
22+
23+
The package ships with a service provider that is auto-discovered. No additional setup is needed, and the helper functions in `src/Helper.php` are automatically loaded.
24+
25+
## Usage
1726

1827
```php
1928
use MysqlDeadlocks\RetryHelper\DBTransactionRetryHelper as Retry;
2029

21-
$result = Retry::transactionWithRetry(function () {
22-
// Your DB logic here (queries, models, etc.)
23-
// Return any value and it will be returned from transactionWithRetry
24-
}, maxRetries: 3, retryDelay: 2, logFileName: 'mysql-deadlocks', trxLabel: 'user-update');
30+
$order = Retry::transactionWithRetry(
31+
function () use ($payload) {
32+
$order = Order::create($payload);
33+
$order->logAuditTrail();
34+
35+
return $order;
36+
},
37+
maxRetries: 4,
38+
retryDelay: 1,
39+
logFileName: 'mysql-deadlocks/orders',
40+
trxLabel: 'order-create'
41+
);
2542
```
2643

27-
Parameters:
28-
- maxRetries: number of attempts (default 3)
29-
- retryDelay: base delay in seconds; actual wait uses exponential backoff with jitter (default 2)
30-
- logFileName: file prefix under storage/logs/{today date} (default 'database/mysql-deadlocks')
31-
- trxLabel: transaction label for easier identification in logs (default '')
32-
33-
Logging:
34-
- Logs are stored in storage/logs/{date}/ directory
35-
- Successful transactions after retries are logged as warnings
36-
- Failed transactions after all retries are logged as errors
37-
- Logs include SQL queries, stack traces, and request information when available
38-
39-
Notes:
40-
- Non-deadlock QueryException is thrown immediately.
41-
- When attempts are exhausted, the last QueryException is thrown; if somehow no exception was thrown, a RuntimeException is raised.
42-
- Requires PHP 8.2+ and Laravel 11.0+
44+
`transactionWithRetry()` returns the value produced by your callback, just like `DB::transaction()`. If every attempt fails, the last `QueryException` is re-thrown so your calling code can continue its normal error handling.
45+
46+
### Parameters
47+
48+
| Parameter | Default | Description |
49+
| --- | --- | --- |
50+
| `maxRetries` | `3` | Total number of attempts (initial try + retries). |
51+
| `retryDelay` | `2` | Base delay (seconds). Actual wait uses exponential backoff with ±25% jitter. |
52+
| `logFileName` | `database/mysql-deadlocks` | Written to `storage/logs/{Y-m-d}/{logFileName}.log`. Can point to subdirectories. |
53+
| `trxLabel` | `''` | Optional label injected into log titles and stored in the service container as `tx.label` for downstream consumers. |
54+
55+
Call the helper anywhere you would normally open a transaction—controllers, jobs, console commands, or domain services.
56+
57+
## Retry Conditions
58+
59+
Retries are attempted only when the caught exception is an `Illuminate\Database\QueryException` that matches one of:
60+
- SQLSTATE `40001` (serialization failure).
61+
- MySQL driver error `1213` (deadlock), whether reported via SQLSTATE or the driver error code.
62+
63+
Everything else (e.g., constraint violations, syntax errors, driver error `1205`, application exceptions) is surfaced immediately without logging or sleeping.
64+
65+
If no attempt succeeds and all retries are exhausted, the last `QueryException` is re-thrown. In the rare case nothing is thrown but the loop exits, a `RuntimeException` is raised to signal exhaustion.
66+
67+
## Logging Behaviour
68+
69+
Logs are written using a dedicated single-file channel per day:
70+
- Success after retries → a warning entry titled `"[trxLabel] [MYSQL DEADLOCK RETRY - SUCCESS] After (Attempts: x/y) - Warning"`.
71+
- Failure after exhausting retries → an error entry titled `"[trxLabel] [MYSQL DEADLOCK RETRY - FAILED] After (Attempts: x/y) - Error"`.
72+
73+
Each log entry includes:
74+
- Attempt count, maximum retries, and transaction label.
75+
- Connection name, SQL, resolved raw SQL (when bindings are available), and PDO error info.
76+
- A compacted stack trace and sanitized bindings.
77+
- Request URL, method, authorization header length, and authenticated user ID when the request helper is bound.
78+
79+
Set `logFileName` to segment logs by feature or workload (e.g., `logFileName: 'database/queues/payments'`).
80+
81+
## Testing the Package
82+
83+
Run the test suite with:
84+
85+
```bash
86+
composer test
87+
```
88+
89+
Tests cover the retry flow, logging behaviour, exponential backoff jitter, and non-deadlock scenarios using fakes for the database and logger managers.
90+
91+
## Requirements
92+
93+
- PHP `>= 8.2`
94+
- Laravel `>= 11.0`
95+
96+
## Contributing
97+
98+
Bugs, ideas, and pull requests are welcome. Feel free to open an issue describing the problem or improvement before submitting a PR so we can collaborate on scope.
99+
100+
## License
101+
102+
This package is open-sourced software released under the MIT License.

0 commit comments

Comments
 (0)