From 28c9de26ced1aa85dca7143481ebe6dae3d6bfe6 Mon Sep 17 00:00:00 2001 From: Ahed Wakim Date: Mon, 13 Oct 2025 21:40:56 +0300 Subject: [PATCH 1/2] gitflow-feature-stash: update-readme.md --- .php-cs-fixer.cache | 2 +- README.md | 124 ++++++++++++++++++++++++++++++++------------ 2 files changed, 93 insertions(+), 33 deletions(-) diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache index a9ce7a6..12199f3 100644 --- a/.php-cs-fixer.cache +++ b/.php-cs-fixer.cache @@ -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"}} \ No newline at end of file +{"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"}} \ No newline at end of file diff --git a/README.md b/README.md index 77fa78b..0aee840 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,102 @@ -# laravel-mysql-deadlock-retry +# Laravel MySQL Deadlock Retry -A lightweight helper to run Laravel database transactions with automatic retries on MySQL deadlocks and serialization failures. +![CI](https://github.com/Ahed92Wakim/laravel-mysql-deadlock-retry/actions/workflows/ci.yml/badge.svg?event=pull_request) +![PHP](https://img.shields.io/badge/php-8.2-blue) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -Features: -- Retries DB::transaction on MySQL deadlocks (error 1213) and SQLSTATE 40001 -- Exponential backoff with jitter between attempts -- Structured logging per attempt to storage/logs -- Safe in HTTP, CLI and queue contexts (request info captured when available) -- Transaction labeling for easier debugging -- Enhanced logging with SQL query information +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. -Installation: -- Require the package via Composer: `composer require ahed92wakim/laravel-mysql-deadlock-retry` +## Highlights +- Retries only known transient failure scenarios (MySQL driver error `1213` and SQLSTATE `40001`), leaving all other exceptions untouched. +- Exponential backoff with jitter between attempts to reduce stampedes under load. +- Structured logs with request metadata, SQL, bindings, connection information, and stack traces written to dated files under `storage/logs/{Y-m-d}`. +- Safe in HTTP, CLI, and queue contexts: request data is collected when available and ignored when not. +- Optional transaction labels and custom log file names for easier traceability across microservices and jobs. +- Laravel package auto-discovery; no manual service provider registration required. -Usage: +## Installation + +```bash +composer require ahed92wakim/laravel-mysql-deadlock-retry +``` + +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. + +## Usage ```php use MysqlDeadlocks\RetryHelper\DBTransactionRetryHelper as Retry; -$result = Retry::transactionWithRetry(function () { - // Your DB logic here (queries, models, etc.) - // Return any value and it will be returned from transactionWithRetry -}, maxRetries: 3, retryDelay: 2, logFileName: 'mysql-deadlocks', trxLabel: 'user-update'); +$order = Retry::transactionWithRetry( + function () use ($payload) { + $order = Order::create($payload); + $order->logAuditTrail(); + + return $order; + }, + maxRetries: 4, + retryDelay: 1, + logFileName: 'mysql-deadlocks/orders', + trxLabel: 'order-create' +); ``` -Parameters: -- maxRetries: number of attempts (default 3) -- retryDelay: base delay in seconds; actual wait uses exponential backoff with jitter (default 2) -- logFileName: file prefix under storage/logs/{today date} (default 'database/mysql-deadlocks') -- trxLabel: transaction label for easier identification in logs (default '') - -Logging: -- Logs are stored in storage/logs/{date}/ directory -- Successful transactions after retries are logged as warnings -- Failed transactions after all retries are logged as errors -- Logs include SQL queries, stack traces, and request information when available - -Notes: -- Non-deadlock QueryException is thrown immediately. -- When attempts are exhausted, the last QueryException is thrown; if somehow no exception was thrown, a RuntimeException is raised. -- Requires PHP 8.2+ and Laravel 11.0+ \ No newline at end of file +`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. + +### Parameters + +| Parameter | Default | Description | +| --- | --- | --- | +| `maxRetries` | `3` | Total number of attempts (initial try + retries). | +| `retryDelay` | `2` | Base delay (seconds). Actual wait uses exponential backoff with ±25% jitter. | +| `logFileName` | `database/mysql-deadlocks` | Written to `storage/logs/{Y-m-d}/{logFileName}.log`. Can point to subdirectories. | +| `trxLabel` | `''` | Optional label injected into log titles and stored in the service container as `tx.label` for downstream consumers. | + +Call the helper anywhere you would normally open a transaction—controllers, jobs, console commands, or domain services. + +## Retry Conditions + +Retries are attempted only when the caught exception is an `Illuminate\Database\QueryException` that matches one of: +- SQLSTATE `40001` (serialization failure). +- MySQL driver error `1213` (deadlock), whether reported via SQLSTATE or the driver error code. + +Everything else (e.g., constraint violations, syntax errors, driver error `1205`, application exceptions) is surfaced immediately without logging or sleeping. + +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. + +## Logging Behaviour + +Logs are written using a dedicated single-file channel per day: +- Success after retries → a warning entry titled `"[trxLabel] [MYSQL DEADLOCK RETRY - SUCCESS] After (Attempts: x/y) - Warning"`. +- Failure after exhausting retries → an error entry titled `"[trxLabel] [MYSQL DEADLOCK RETRY - FAILED] After (Attempts: x/y) - Error"`. + +Each log entry includes: +- Attempt count, maximum retries, and transaction label. +- Connection name, SQL, resolved raw SQL (when bindings are available), and PDO error info. +- A compacted stack trace and sanitized bindings. +- Request URL, method, authorization header length, and authenticated user ID when the request helper is bound. + +Set `logFileName` to segment logs by feature or workload (e.g., `logFileName: 'database/queues/payments'`). + +## Testing the Package + +Run the test suite with: + +```bash +composer test +``` + +Tests cover the retry flow, logging behaviour, exponential backoff jitter, and non-deadlock scenarios using fakes for the database and logger managers. + +## Requirements + +- PHP `>= 8.2` +- Laravel `>= 11.0` + +## Contributing + +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. + +## License + +This package is open-sourced software released under the MIT License. From 5b068622d19998d14f59056f2ec008cc1611e349 Mon Sep 17 00:00:00 2001 From: Ahed Wakim Date: Mon, 13 Oct 2025 21:44:58 +0300 Subject: [PATCH 2/2] Extends CI to develop and main branches Updates the CI workflow to trigger on push and pull request events for both the `develop` and `main` branches, ensuring comprehensive testing and validation for all relevant code changes. --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14bb2b3..9f3340c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,10 @@ name: CI on: + push: + branches: [ develop, main ] pull_request: - branches: [ master ] + branches: [ develop, main ] jobs: phpunit: