Skip to content

Commit 96ee0ca

Browse files
authored
Feature/file structure (#10)
* Refactors transaction retry logic into services Moves retry logic into dedicated service classes for better organization and testability. Removes the global helper functions and related service provider in favor of a dedicated service. Exposes support classes for logging, tracing, and binding stringification to facilitate reuse and testing. * Configures retry logic via config file Adds a configuration file to manage default values for retry attempts, delay, and logging. This change allows users to customize the retry behavior via a published configuration file, offering flexibility and avoiding hardcoded values. It also introduces configurable log levels for success and failure cases.
1 parent 5e024c6 commit 96ee0ca

15 files changed

+564
-562
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.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":"78359ccc3cc8934fc6b788bb9f8455df","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":"0a3d3a6bed6c3bd5af47a71e29a5be92","tests\/Feature\/ExampleTest.php":"a1e5352ea369ad36f88f4f566c340371","tests\/Pest.php":"44a41307b2bca2c9b747aa2f40c5262b","src\/Providers\/DatabaseRetryServiceProvider.php":"c4b1b48a744c843ed40bb818370ab922","src\/Services\/DeadlockTransactionRetrier.php":"62fc72973cf461c9029fdddaa4d721ce","src\/Support\/DeadlockLogWriter.php":"b298e47ae03b1255eb9d09d8c3758ef4","src\/Support\/BindingStringifier.php":"3aa21139dad20340d9518fa57e0845ca","src\/Support\/TraceFormatter.php":"13f19f8c9de611faa05847ae3890b73d"}}

README.md

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ Resilient database transactions for Laravel applications that need to gracefully
3535
composer require ahed92wakim/laravel-mysql-deadlock-retry
3636
```
3737

38-
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.
38+
The package ships with the `DatabaseRetryServiceProvider`, which Laravel auto-discovers. No additional setup is needed.
3939

4040
## Usage
4141

4242
```php
43-
use MysqlDeadlocks\RetryHelper\DBTransactionRetryHelper as Retry;
43+
use MysqlDeadlocks\RetryHelper\Services\DeadlockTransactionRetrier as Retry;
4444

45-
$order = Retry::transactionWithRetry(
45+
$order = Retry::runWithRetry(
4646
function () use ($payload) {
4747
$order = Order::create($payload);
4848
$order->logAuditTrail();
@@ -56,19 +56,35 @@ $order = Retry::transactionWithRetry(
5656
);
5757
```
5858

59-
`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.
59+
`runWithRetry()` 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.
6060

6161
### Parameters
6262

6363
| Parameter | Default | Description |
6464
| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
65-
| `maxRetries` | `3` | Total number of attempts (initial try + retries). |
66-
| `retryDelay` | `2` | Base delay (seconds). Actual wait uses exponential backoff with ±25% jitter. |
67-
| `logFileName` | `database/mysql-deadlocks` | Written to `storage/logs/{Y-m-d}/{logFileName}.log`. Can point to subdirectories. |
65+
| `maxRetries` | Config (`default: 3`) | Total number of attempts (initial try + retries). |
66+
| `retryDelay` | Config (`default: 2s`) | Base delay (seconds). Actual wait uses exponential backoff with ±25% jitter. |
67+
| `logFileName` | Config (`default: database/mysql-deadlocks`) | Written to `storage/logs/{Y-m-d}/{logFileName}.log`. Can point to subdirectories. |
6868
| `trxLabel` | `''` | Optional label injected into log titles and stored in the service container as `tx.label` for downstream consumers. |
6969

7070
Call the helper anywhere you would normally open a transaction—controllers, jobs, console commands, or domain services.
7171

72+
## Configuration
73+
74+
Publish the configuration file to tweak defaults globally:
75+
76+
```bash
77+
php artisan vendor:publish --tag=mysql-deadlock-retry-config
78+
```
79+
80+
Key options (`config/mysql-deadlock-retry.php`):
81+
82+
- `max_retries`, `retry_delay`, and `log_file_name` set the package-wide defaults when you omit parameters. Each respects the matching environment variable (`MYSQL_DEADLOCK_MAX_RETRIES`, `MYSQL_DEADLOCK_RETRY_DELAY`, `MYSQL_DEADLOCK_LOG_FILE`).
83+
- `logging.channel` points at any existing Laravel log channel so you can reuse stacks or third-party drivers.
84+
- `logging.config` provides a full configuration array for `Log::build()` when you want a dedicated writer.
85+
- `logging.via` accepts a container binding, class name, or callable that resolves a PSR-3 logger—ideal when you need to hand logs off to a completely custom pipeline.
86+
- `logging.levels.success` / `logging.levels.failure` let you tune the severity emitted for successful retries and exhausted attempts (defaults: `warning` and `error`).
87+
7288
## Retry Conditions
7389

7490
Retries are attempted only when the caught exception is an `Illuminate\Database\QueryException` that matches one of:
@@ -82,7 +98,7 @@ If no attempt succeeds and all retries are exhausted, the last `QueryException`
8298

8399
## Logging Behaviour
84100

85-
Logs are written using a dedicated single-file channel per day:
101+
By default, logs are written using a dedicated single-file channel per day. Override `logging.channel`, `logging.config`, or `logging.via` to integrate with your own logging stack:
86102

87103
- Success after retries → a warning entry titled `"[trxLabel] [MYSQL DEADLOCK RETRY - SUCCESS] After (Attempts: x/y) - Warning"`.
88104
- Failure after exhausting retries → an error entry titled `"[trxLabel] [MYSQL DEADLOCK RETRY - FAILED] After (Attempts: x/y) - Error"`.
@@ -96,6 +112,16 @@ Each log entry includes:
96112

97113
Set `logFileName` to segment logs by feature or workload (e.g., `logFileName: 'database/queues/payments'`).
98114

115+
## Helper Utilities
116+
117+
The package exposes dedicated support classes you can reuse in your own instrumentation:
118+
119+
- `MysqlDeadlocks\RetryHelper\Support\DeadlockLogWriter` writes structured entries using the same format as the retrier.
120+
- `MysqlDeadlocks\RetryHelper\Support\TraceFormatter` converts debug backtraces into log-friendly arrays.
121+
- `MysqlDeadlocks\RetryHelper\Support\BindingStringifier` sanitises query bindings before logging.
122+
123+
For testing scenarios, the retrier looks for a namespaced `MysqlDeadlocks\RetryHelper\sleep()` function before falling back to PHP's global `sleep()`, making it easy to assert backoff intervals without introducing delays.
124+
99125
## Testing the Package
100126

101127
Run the test suite with:

composer.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111
"autoload": {
1212
"psr-4": {
1313
"MysqlDeadlocks\\RetryHelper\\": "src/"
14-
},
15-
"files": [
16-
"src/Helper.php"
17-
]
14+
}
1815
},
1916
"autoload-dev": {
2017
"psr-4": {
@@ -24,7 +21,7 @@
2421
"extra": {
2522
"laravel": {
2623
"providers": [
27-
"MysqlDeadlocks\\RetryHelper\\RetryServiceProvider"
24+
"MysqlDeadlocks\\RetryHelper\\Providers\\DatabaseRetryServiceProvider"
2825
]
2926
}
3027
},

config/mysql-deadlock-retry.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
return [
4+
/*
5+
|--------------------------------------------------------------------------
6+
| Retry Defaults
7+
|--------------------------------------------------------------------------
8+
|
9+
| Configure the retry strategy that will be used when no explicit overrides
10+
| are provided to the retrier helper. These values can be fine-tuned per
11+
| environment through the accompanying environment variables.
12+
|
13+
*/
14+
15+
'max_retries' => (int) env('MYSQL_DEADLOCK_MAX_RETRIES', 3),
16+
17+
'retry_delay' => (int) env('MYSQL_DEADLOCK_RETRY_DELAY', 2),
18+
19+
'log_file_name' => env('MYSQL_DEADLOCK_LOG_FILE', 'database/mysql-deadlocks'),
20+
21+
/*
22+
|--------------------------------------------------------------------------
23+
| Logging
24+
|--------------------------------------------------------------------------
25+
|
26+
| Control how retry attempts are logged. Provide a `channel` to reuse any
27+
| logging channel defined in your application, supply a `config` array to
28+
| build a dedicated logger on the fly. When none are defined,
29+
| the package will continue to emit dated single-file logs per prior
30+
| behaviour.
31+
|
32+
*/
33+
34+
'logging' => [
35+
'channel' => env('MYSQL_DEADLOCK_LOG_CHANNEL'),
36+
37+
'config' => null,
38+
39+
'levels' => [
40+
'success' => env('MYSQL_DEADLOCK_LOG_SUCCESS_LEVEL', 'warning'),
41+
'failure' => env('MYSQL_DEADLOCK_LOG_FAILURE_LEVEL', 'error'),
42+
],
43+
],
44+
];
45+

0 commit comments

Comments
 (0)