Skip to content
Merged
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ vendor/
.vscode/
.DS_Store
resources/

storage/runtime/retry-disabled.marker
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.89.0:v3.89.0#4dd6768cb7558440d27d18f54909eee417317ce9","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":{"tests\/bootstrap.php":"4ae74313e457f6662f4831ee2140eb34","src\/Providers\/DatabaseTransactionRetryServiceProvider.php":"1071a19b80835472e035374cc17b7056","src\/Services\/TransactionRetrier.php":"7d99d773c44861e3f12524a5119f5240","src\/Support\/BindingStringifier.php":"bbead2bae37761124652320cd28db412","src\/Support\/TransactionRetryLogWriter.php":"2bc60103437973d7b471ad6bc91c43cf","src\/Support\/TraceFormatter.php":"e640e32b17149f1cfec9a1d990f04c84","tests\/TestCase.php":"a45ed4c82e3b3f4ad47544b81fda41f5","tests\/Unit\/ExampleTest.php":"3bbd4ea8029698f723c35a66d8592087","tests\/Unit\/DBTransactionRetryHelperTest.php":"07bb6b8e6c8b3ce61a7e67f128f12f4a","tests\/Feature\/ExampleTest.php":"a1e5352ea369ad36f88f4f566c340371","tests\/Pest.php":"44a41307b2bca2c9b747aa2f40c5262b"}}
{"php":"8.2.29","version":"3.89.0:v3.89.0#4dd6768cb7558440d27d18f54909eee417317ce9","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":{"tests\/Unit\/DBTransactionRetryHelperTest.php":"27f37268e9ae100d356ca1c06c756616","tests\/Feature\/ExampleTest.php":"a1e5352ea369ad36f88f4f566c340371","tests\/Pest.php":"44a41307b2bca2c9b747aa2f40c5262b","tests\/bootstrap.php":"4ae74313e457f6662f4831ee2140eb34","src\/Console\/StartRetryCommand.php":"b0e6c76186a59d1341fa6600e096f9a3","src\/Console\/StopRetryCommand.php":"ad3d7cbb9841006db54f12168c58d369","src\/Providers\/DatabaseTransactionRetryServiceProvider.php":"638c2f84c78a86c8e8c5e4daef07c46e","src\/Services\/TransactionRetrier.php":"504c45ab8315c7b3d60a64e0a52759af","src\/Support\/RetryToggle.php":"2516f4c290940019b1f1c069fec64be8","src\/Support\/BindingStringifier.php":"bbead2bae37761124652320cd28db412","src\/Support\/TransactionRetryLogWriter.php":"2bc60103437973d7b471ad6bc91c43cf","src\/Support\/TraceFormatter.php":"e640e32b17149f1cfec9a1d990f04c84","tests\/TestCase.php":"897cfbd81822f4b71075ccb1739df70d","tests\/Unit\/ExampleTest.php":"3bbd4ea8029698f723c35a66d8592087"}}
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License">
</a>
<img src="https://img.shields.io/badge/Laravel-%5E11-red.svg" alt="Laravel ^11">
<img src="https://img.shields.io/badge/Laravel-11%20%7C%7C%2012-red.svg" alt="Laravel 11 or 12">
<img src="https://img.shields.io/badge/PHP-%5E8.2-blue.svg" alt="PHP ^8.2">
<img src="https://img.shields.io/badge/style-PHP%20CS%20Fixer-informational.svg" alt="PHP CS Fixer">
</p>
Expand Down Expand Up @@ -117,6 +117,19 @@ Each log entry includes:

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

## Runtime Toggle

Use the built-in Artisan commands to temporarily disable or re-enable retries without touching configuration files:

```bash
php artisan db-transaction-retry:stop # disable retries
php artisan db-transaction-retry:start # enable retries
```

The commands write a small marker file inside the package (`storage/runtime/retry-disabled.marker`). As long as that file exists retries stay off; removing it or running `db-transaction-retry:start` brings them back. You can still set the `DB_TRANSACTION_RETRY_ENABLED` environment variable for a permanent default.

> **Heads up:** The `db-transaction-retry:start` command only removes the disable marker—it does not override an explicit `database-transaction-retry.enabled=false` configuration (including the `DB_TRANSACTION_RETRY_ENABLED=false` environment variable). Update that setting to `true` if you want retries to remain enabled after the current process.

## Helper Utilities

The package exposes dedicated support classes you can reuse in your own instrumentation:
Expand Down
12 changes: 12 additions & 0 deletions config/database-transaction-retry.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
<?php

return [
/*
|--------------------------------------------------------------------------
| Toggle
|--------------------------------------------------------------------------
|
| Enable or disable the retry helper at runtime. This can be overridden
| through the provided Artisan commands when a cache store is available.
|
*/

'enabled' => env('DB_TRANSACTION_RETRY_ENABLED', true),

/*
|--------------------------------------------------------------------------
| Retry Defaults
Expand Down
48 changes: 48 additions & 0 deletions src/Console/StartRetryCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace DatabaseTransactions\RetryHelper\Console;

use DatabaseTransactions\RetryHelper\Support\RetryToggle;
use Illuminate\Console\Command;

class StartRetryCommand extends Command
{
protected $signature = 'db-transaction-retry:start';

protected $description = 'Enable database transaction retry handling.';

public function handle(): int
{
$configEnabled = config('database-transaction-retry.enabled');
$explicitlyDisabled = RetryToggle::isExplicitlyDisabledValue($configEnabled);
if ($explicitlyDisabled) {
$this->warn('Base configuration keeps retries disabled. Update `database-transaction-retry.enabled` (or `DB_TRANSACTION_RETRY_ENABLED`) if you want retries to stay on after this run.');

return self::SUCCESS;
}
config(['database-transaction-retry.enabled' => true]);

$persisted = RetryToggle::enable();
$marker = RetryToggle::markerPath();

if ($persisted) {
$this->info('Database transaction retries have been enabled.');
$this->line("Cleared toggle marker: {$marker}");
} else {
$this->warn('Database transaction retries could not be fully enabled because the toggle marker could not be removed.');
$this->line("Please delete {$marker} manually or adjust permissions.");
}

$state = RetryToggle::isEnabled(config('database-transaction-retry', []))
? 'ENABLED'
: 'DISABLED';

if ($persisted) {
$this->line("Current status: {$state}");
} else {
$this->line("Current status: {$state} (marker still present; retries remain disabled)");
}

return self::SUCCESS;
}
}
49 changes: 49 additions & 0 deletions src/Console/StopRetryCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace DatabaseTransactions\RetryHelper\Console;

use DatabaseTransactions\RetryHelper\Support\RetryToggle;
use Illuminate\Console\Command;

class StopRetryCommand extends Command
{
protected $signature = 'db-transaction-retry:stop';

protected $description = 'Disable database transaction retry handling.';

public function handle(): int
{
$configEnabled = config('database-transaction-retry.enabled');
$explicitlyDisabled = RetryToggle::isExplicitlyDisabledValue($configEnabled);
if ($explicitlyDisabled) {
$this->warn('Base configuration already disables retries via `database-transaction-retry.enabled` or `DB_TRANSACTION_RETRY_ENABLED`.');

return self::SUCCESS;
}
config(['database-transaction-retry.enabled' => false]);

$persisted = RetryToggle::disable();
$marker = RetryToggle::markerPath();

if ($persisted) {
$this->info('Database transaction retries have been disabled.');
$this->line("Created toggle marker: {$marker}");
} else {
$this->warn('Database transaction retries disabled for this process, but the toggle marker could not be written.');
$this->line("Please create {$marker} manually or adjust permissions.");
}

$state = RetryToggle::isEnabled(config('database-transaction-retry', []))
? 'ENABLED'
: 'DISABLED';

if ($persisted) {
$this->line("Current status: {$state}");
} else {
$this->line("Current status: {$state} (runtime only; persistence failed)");
$this->warn('Retries will be re-enabled on the next run unless the marker is created manually.');
}

return self::SUCCESS;
}
}
7 changes: 7 additions & 0 deletions src/Providers/DatabaseTransactionRetryServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace DatabaseTransactions\RetryHelper\Providers;

use DatabaseTransactions\RetryHelper\Console\StartRetryCommand;
use DatabaseTransactions\RetryHelper\Console\StopRetryCommand;
use Illuminate\Support\ServiceProvider;

class DatabaseTransactionRetryServiceProvider extends ServiceProvider
Expand All @@ -27,6 +29,11 @@ public function boot(): void
$this->publishes([
__DIR__ . '/../../config/database-transaction-retry.php' => $configPath,
], 'database-transaction-retry-config');

$this->commands([
StartRetryCommand::class,
StopRetryCommand::class,
]);
}
}
}
26 changes: 24 additions & 2 deletions src/Services/TransactionRetrier.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace DatabaseTransactions\RetryHelper\Services;

use Closure;
use DatabaseTransactions\RetryHelper\Support\RetryToggle;
use DatabaseTransactions\RetryHelper\Support\TraceFormatter;
use DatabaseTransactions\RetryHelper\Support\TransactionRetryLogWriter;
use Illuminate\Database\QueryException;
Expand Down Expand Up @@ -31,7 +32,15 @@ public static function runWithRetry(
?string $logFileName = null,
string $trxLabel = ''
): mixed {
$config = function_exists('config') ? config('database-transaction-retry', []) : [];
$config = function_exists('config') ? config('database-transaction-retry', []) : [];
$config = is_array($config) ? $config : [];
$trxLabel = $trxLabel ?? '';

if (! RetryToggle::isEnabled($config)) {
static::exposeTransactionLabel($trxLabel);

return DB::transaction($callback);
}

$maxRetries ??= (int) ($config['max_retries'] ?? 3);
$retryDelay ??= (int) ($config['retry_delay'] ?? 2);
Expand All @@ -53,7 +62,7 @@ public static function runWithRetry(
static::applyLockWaitTimeout($config);

// Expose the transaction label if the app wants to read it during the callback.
$trxLabel === '' || app()->instance('tx.label', $trxLabel);
static::exposeTransactionLabel($trxLabel);

return DB::transaction($callback);
} catch (Throwable $exception) {
Expand Down Expand Up @@ -340,4 +349,17 @@ protected static function isLockWaitRetryEnabled(array $config): bool

return in_array(1205, $driverCodes, true);
}

protected static function exposeTransactionLabel(string $trxLabel): void
{
if ($trxLabel === '') {
return;
}

if (! function_exists('app')) {
return;
}

app()->instance('tx.label', $trxLabel);
}
}
140 changes: 140 additions & 0 deletions src/Support/RetryToggle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

namespace DatabaseTransactions\RetryHelper\Support;

class RetryToggle
{
protected const MARKER_FILENAME = 'retry-disabled.marker';

protected static ?bool $override = null;

public static function isEnabled(array $config = []): bool
{
if (file_exists(static::markerPath())) {
return false;
}

if (is_bool(static::$override)) {
return static::$override;
}

$fallback = static::normalizeBoolean($config['enabled'] ?? null, true);

return $fallback;
}

public static function enable(): bool
{
static::$override = null;

return static::removeMarker();
}

public static function disable(): bool
{
static::$override = false;

return static::writeMarker();
}

public static function markerPath(): string
{
return static::stateDirectory() . '/' . static::MARKER_FILENAME;
}

public static function isExplicitlyDisabledValue(mixed $value): bool
{
if ($value === null) {
return false;
}

if (is_string($value) && trim($value) === '') {
return false;
}

return static::normalizeBoolean($value, true) === false;
}

protected static function writeMarker(): bool
{
$directory = static::stateDirectory();

if (! is_dir($directory) && ! @mkdir($directory, 0777, true) && ! is_dir($directory)) {
return false;
}

return file_put_contents(static::markerPath(), (string) time()) !== false;
}

protected static function removeMarker(): bool
{
$path = static::markerPath();

if (file_exists($path) && ! @unlink($path)) {
return false;
}

static::cleanupStateDirectory();

return true;
}

protected static function stateDirectory(): string
{
return dirname(__DIR__, 2) . '/storage/runtime';
}

protected static function normalizeBoolean(mixed $value, bool $fallback): bool
{
if (is_bool($value)) {
return $value;
}

if (is_string($value)) {
$value = strtolower(trim($value));

if ($value === '') {
return $fallback;
}

if (in_array($value, ['false', '0', 'off', 'no'], true)) {
return false;
}

if (in_array($value, ['true', '1', 'on', 'yes'], true)) {
return true;
}
}

if (is_numeric($value)) {
return (int) $value !== 0;
}

return $fallback;
}

protected static function cleanupStateDirectory(): void
{
$directory = static::stateDirectory();

if (is_dir($directory)) {
$entries = @scandir($directory) ?: [];
$entries = array_diff($entries, ['.', '..']);

if (count($entries) === 0) {
@rmdir($directory);
}
}

$root = dirname($directory);

if (is_dir($root)) {
$entries = @scandir($root) ?: [];
$entries = array_diff($entries, ['.', '..']);

if (count($entries) === 0) {
@rmdir($root);
}
}
}
}
5 changes: 5 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ public function storagePath($path = ''): string

return $path === '' ? $base : $base . '/' . ltrim((string) $path, '/');
}

public function runningUnitTests(): bool
{
return true;
}
}
Loading