ahed92wakim / laravel-mysql-deadlock-retry
Gracefully retry Laravel transactions on database deadlocks and other retryable errors with configurable backoff, structured logging, and reusable helpers.
Installs: 45
Dependents: 0
Suggesters: 0
Security: 0
Stars: 4
Watchers: 1
Forks: 1
Open Issues: 1
pkg:composer/ahed92wakim/laravel-mysql-deadlock-retry
Requires
- php: >=8.2
- laravel/framework: >=11.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.88
- pestphp/pest: ^3.8
- phpunit/phpunit: ^11.5
- dev-main
- 2.0.0
- 1.3.1
- 1.3
- 1.2.5
- 1.2.4
- 1.2.3
- 1.2.1
- 1.2
- 1.1
- dev-feature/transactions
- dev-feature/file-structure
- dev-develop
- dev-feature/composer-file
- dev-feature/logo
- dev-feature/log-all-db-transactions
- dev-feature/update-readme.md
- dev-feature/pest-test
- dev-feature/modification
- dev-hotfix/fix-log
- dev-feature/improvement
This package is auto-updated.
Last update: 2025-10-22 16:54:22 UTC
README
Resilient database transactions for Laravel applications that need to gracefully handle deadlocks, serialization failures, and any other transient database errors you configure. This helper wraps DB::transaction() with targeted retries, structured logging, and exponential backoff so you can keep your business logic simple while surviving temporary contention.
Highlights
- Retries known transient failures out of the box (SQLSTATE
40001, MySQL driver errors1213and1205), and lets you add extra SQLSTATE codes, driver error codes, or exception classes through configuration. - 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}. - Log titles include the exception class and codes, making it easy to see exactly what triggered the retry.
- 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.
Installation
composer require ahed92wakim/laravel-db-transaction-retry
The package ships with the DatabaseTransactionRetryServiceProvider, which Laravel auto-discovers. No additional setup is needed.
Usage
use DatabaseTransactions\RetryHelper\Services\TransactionRetrier as Retry; $order = Retry::runWithRetry( function () use ($payload) { $order = Order::create($payload); $order->logAuditTrail(); return $order; }, maxRetries: 4, retryDelay: 1, logFileName: 'database/transaction-retries/orders', trxLabel: 'order-create' );
runWithRetry() returns the value produced by your callback, just like DB::transaction(). If every attempt fails, the last exception is re-thrown so your calling code can continue its normal error handling.
Parameters
| Parameter | Default | Description |
|---|---|---|
maxRetries |
Config (default: 3) |
Total number of attempts (initial try + retries). |
retryDelay |
Config (default: 2s) |
Base delay (seconds). Actual wait uses exponential backoff with ±25% jitter. |
logFileName |
Config (default: database/transaction-retries) |
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.
Configuration
Publish the configuration file to tweak defaults globally:
php artisan vendor:publish --tag=database-transaction-retry-config
-
Key options (
config/database-transaction-retry.php): -
max_retries,retry_delay, andlog_file_nameset the package-wide defaults when you omit parameters. Each respects environment variables (DB_TRANSACTION_RETRY_MAX_RETRIES,DB_TRANSACTION_RETRY_DELAY,DB_TRANSACTION_RETRY_LOG_FILE). -
lock_wait_timeout_secondslets you overrideinnodb_lock_wait_timeoutper attempt; set the matching environment variable (DB_TRANSACTION_RETRY_LOCK_WAIT_TIMEOUT) to control the session value or leave null to use the database default. -
logging.channelpoints at any existing Laravel log channel so you can reuse stacks or third-party drivers. -
logging.levels.success/logging.levels.failurelet you tune the severity emitted for successful retries and exhausted attempts (defaults:warninganderror). -
retryable_exceptions.sql_stateslists SQLSTATE codes that should trigger a retry (defaults to40001). -
retryable_exceptions.driver_error_codeslists driver-specific error codes (defaults to1213deadlocks and1205lock wait timeouts). Including1205not only enables retries but also activates the optional session lock wait timeout override when configured. -
retryable_exceptions.classeslets you specify fully-qualified exception class names that should always be retried.
Retry Conditions
Retries are attempted when the caught exception matches one of the configured conditions:
Illuminate\Database\QueryExceptionwith a SQLSTATE listed inretryable_exceptions.sql_states.Illuminate\Database\QueryExceptionwith a driver error code listed inretryable_exceptions.driver_error_codes(defaults include1213deadlocks and1205lock wait timeouts).- Any exception instance whose class appears in
retryable_exceptions.classes.
Everything else (e.g., constraint violations, syntax errors, application exceptions) is surfaced immediately without logging or sleeping. If no attempt succeeds and all retries are exhausted, the last exception is re-thrown. In the rare case nothing is thrown but the loop exits, a RuntimeException is raised to signal exhaustion.
Lock Wait Timeout
When lock_wait_timeout_seconds is configured, the retrier issues SET SESSION innodb_lock_wait_timeout = {seconds} on the active connection before each attempt, but only when the retry rules include the lock-wait timeout driver code (1205). This keeps the timeout predictable even after reconnects or pool reuse, and on drivers that do not support the statement the helper safely ignores the failure.
Logging Behaviour
By default, logs are written using a dedicated single-file channel per day. Override logging.channel to integrate with your own logging stack:
- Success after retries → a warning entry titled
"[trxLabel] [DATABASE TRANSACTION RETRY - SUCCESS] ExceptionClass (Codes) After (Attempts: x/y) - Warning". - Failure after exhausting retries → an error entry titled
"[trxLabel] [DATABASE TRANSACTION RETRY - FAILED] ExceptionClass (Codes) After (Attempts: x/y) - Error".
Each log entry includes:
- Attempt count, maximum retries, and transaction label.
- Exception class, SQLSTATE, driver error code, connection name, SQL, resolved raw SQL, and PDO error info when available.
- 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').
Helper Utilities
The package exposes dedicated support classes you can reuse in your own instrumentation:
DatabaseTransactions\RetryHelper\Support\TransactionRetryLogWriterwrites structured entries using the same format as the retrier.DatabaseTransactions\RetryHelper\Support\TraceFormatterconverts debug backtraces into log-friendly arrays.DatabaseTransactions\RetryHelper\Support\BindingStringifiersanitises query bindings before logging.
For testing scenarios, the retrier looks for a namespaced DatabaseTransactions\RetryHelper\sleep() function before falling back to PHP's global sleep(), making it easy to assert backoff intervals without introducing delays.
Testing the Package
Run the test suite with:
composer test
Tests cover the retry flow, logging behaviour, exponential backoff jitter, and non-retryable 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.