gregpriday / laravel-retry
A flexible retry mechanism for Laravel applications
Requires
- php: ^8.1
- illuminate/support: ^10.0|^11.0
Requires (Dev)
- captainhook/captainhook: *
- captainhook/hook-installer: ^1.0
- laravel/pint: ^1.0
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2025-03-07 03:53:05 UTC
README
A robust and flexible retry mechanism for Laravel applications that provides a powerful way to handle transient failures. This package helps you handle temporary failures in HTTP requests, database queries, API calls, and any other potentially unstable operations.
Features
-
Multiple Retry Strategies
- Exponential Backoff (with optional jitter)
- Linear Backoff
- Fixed Delay
- Decorrelated Jitter (AWS-style)
- Rate Limiting
- Circuit Breaker pattern
- Composable strategies for complex scenarios
-
Promise-like Result Handling
- Chain success/failure handlers
- Handle errors gracefully
- Access attempt history
- Fluent interface
-
Built-in Exception Handling
- Automatic detection of retryable errors
- Extensible exception handler system
- Built-in support for Guzzle exceptions
- Custom error pattern matching
-
Comprehensive Configuration
- Configurable retry attempts
- Adjustable delays and timeouts
- Progress tracking and logging
- Custom retry conditions
Installation
You can install the package via Composer:
composer require gregpriday/laravel-retry
After installation, you can publish the configuration file:
php artisan vendor:publish --tag="retry-config"
Basic Usage
Simple Retry Operation
use GregPriday\LaravelRetry\Facades\Retry; $result = Retry::run(function () { return Http::get('https://api.example.com/data'); })->then(function ($response) { // Handle successful response return $response->json(); })->catch(function (Throwable $e) { // Handle complete failure after all retries Log::error('API call failed', ['error' => $e->getMessage()]); return null; });
Using Dependency Injection
use GregPriday\LaravelRetry\Retry; class ApiService { public function __construct( private Retry $retry ) {} public function fetchData() { return $this->retry ->maxRetries(5) ->retryDelay(1) ->run(fn() => Http::get('https://api.example.com/data')) ->value(); } }
Configuring Retry Behavior
Retry::make() ->maxRetries(5) // Maximum retry attempts ->retryDelay(2) // Base delay in seconds ->timeout(30) // Timeout per attempt ->withProgress(function ($message) { Log::info($message); // Track progress }) ->run(function () { return DB::table('users')->get(); });
Custom Retry Conditions
Retry::make() ->retryIf(function (Throwable $e, array $context) { // Retry on rate limit errors if we have attempts remaining return $e instanceof RateLimitException && $context['remaining_attempts'] > 0; }) ->run(function () { return $api->fetch(); }); // Or use retryUnless for inverse conditions Retry::make() ->retryUnless(function (Throwable $e, array $context) { // Don't retry if we've seen too many of the same error return $context['attempt'] >= 2; }) ->run(function () { return $api->fetch(); });
Retry Strategies
The package includes several retry strategies to handle different scenarios:
Exponential Backoff (Default)
Increases delay exponentially between retries:
use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy; $strategy = new ExponentialBackoffStrategy( multiplier: 2.0, // Each delay will be 2x longer maxDelay: 30, // Maximum delay in seconds withJitter: true // Add randomness to prevent thundering herd ); Retry::withStrategy($strategy)->run(fn() => doSomething());
Linear Backoff
Increases delay linearly between retries:
use GregPriday\LaravelRetry\Strategies\LinearBackoffStrategy; $strategy = new LinearBackoffStrategy( increment: 5, // Add 5 seconds each retry maxDelay: 30 // Maximum delay in seconds );
Fixed Delay
Uses the same delay between all retries:
use GregPriday\LaravelRetry\Strategies\FixedDelayStrategy; $strategy = new FixedDelayStrategy( withJitter: true, // Add randomness jitterPercent: 0.2 // ±20% jitter );
Decorrelated Jitter
Implements AWS's "Exponential Backoff and Jitter" algorithm:
use GregPriday\LaravelRetry\Strategies\DecorrelatedJitterStrategy; $strategy = new DecorrelatedJitterStrategy( maxDelay: 30, // Maximum delay in seconds minFactor: 1.0, // Minimum multiplier for base delay maxFactor: 3.0 // Maximum multiplier for base delay );
Rate Limit Strategy
Implements rate limiting across multiple retry attempts:
use GregPriday\LaravelRetry\Strategies\RateLimitStrategy; $strategy = new RateLimitStrategy( innerStrategy: new ExponentialBackoffStrategy(), maxAttempts: 100, // Maximum attempts per time window timeWindow: 60 // Time window in seconds );
Circuit Breaker Strategy
Implements the Circuit Breaker pattern:
use GregPriday\LaravelRetry\Strategies\CircuitBreakerStrategy; $strategy = new CircuitBreakerStrategy( innerStrategy: new ExponentialBackoffStrategy(), failureThreshold: 5, // Failures before opening circuit resetTimeout: 60 // Seconds before attempting reset );
Combining Strategies
Strategies can be combined for complex scenarios:
$rateLimit = new RateLimitStrategy( new ExponentialBackoffStrategy(), maxAttempts: 100, timeWindow: 60 ); $circuitBreaker = new CircuitBreakerStrategy( $rateLimit, failureThreshold: 5, resetTimeout: 60 ); Retry::withStrategy($circuitBreaker)->run(fn() => doSomething());
Result Handling
The package provides a promise-like interface for handling results:
$result = Retry::run(fn() => riskyOperation()) ->then(function($value) { // Handle success return processValue($value); }) ->catch(function(Throwable $e) { // Handle failure Log::error('Operation failed', ['error' => $e]); return fallbackValue(); }) ->finally(function() { // Always runs cleanup(); }); // Access the final value (throws on error) $value = $result->value(); // Or handle success/failure states explicitly if ($result->succeeded()) { $value = $result->getResult(); } else { $error = $result->getError(); } // Access retry history $history = $result->getExceptionHistory();
Laravel Retry - Advanced Usage
Exception Handling System
Built-in Exception Handlers
The package includes built-in handlers for common exceptions:
- Network timeouts
- Connection errors
- Rate limiting
- Server errors
- SSL/TLS issues
- Temporary unavailability
Custom Exception Handlers
Create custom handlers by extending the BaseHandler
class:
use GregPriday\LaravelRetry\Exceptions\Handlers\BaseHandler; class CustomDatabaseHandler extends BaseHandler { protected function getHandlerPatterns(): array { return [ '/deadlock found/i', '/lock wait timeout/i', '/connection lost/i' ]; } protected function getHandlerExceptions(): array { return [ \PDOException::class, \Doctrine\DBAL\Exception\DeadlockException::class, \Illuminate\Database\QueryException::class ]; } public function isApplicable(): bool { return true; } }
Register custom handlers:
// In a service provider use GregPriday\LaravelRetry\Exceptions\ExceptionHandlerManager; public function boot(ExceptionHandlerManager $manager) { $manager->registerHandler(new CustomDatabaseHandler()); }
Exception History
Track and analyze retry attempts:
$result = Retry::run(fn() => riskyOperation()); foreach ($result->getExceptionHistory() as $entry) { Log::info('Retry attempt details', [ 'attempt' => $entry['attempt'], 'exception' => $entry['exception']->getMessage(), 'timestamp' => $entry['timestamp'], 'was_retryable' => $entry['was_retryable'] ]); }
Testing & Debugging
Testing Retry Logic
The package is designed for easy testing:
use GregPriday\LaravelRetry\Tests\TestCase; class MyServiceTest extends TestCase { public function test_it_retries_failed_operations() { $counter = 0; $result = $this->retry ->maxRetries(3) ->run(function() use (&$counter) { $counter++; if ($counter < 3) { throw new Exception('Temporary failure'); } return 'success'; }); $this->assertTrue($result->succeeded()); $this->assertEquals(3, $counter); $this->assertCount(2, $result->getExceptionHistory()); } }
Debugging Tools
Monitor retry operations:
Retry::make() ->withProgress(function ($message) { Log::channel('retry')->info($message); // Or send to monitoring service Monitoring::recordRetryAttempt([ 'message' => $message, 'timestamp' => now() ]); }) ->run(fn() => operation());
Advanced Configuration
Global Configuration
Customize default behavior in config/retry.php
:
return [ 'max_retries' => env('RETRY_MAX_ATTEMPTS', 3), 'delay' => env('RETRY_DELAY', 5), 'timeout' => env('RETRY_TIMEOUT', 120), 'handler_paths' => [ app_path('Exceptions/Retry/Handlers') ], ];
Runtime Configuration
Apply configuration per operation:
Retry::make() ->maxRetries(5) ->retryDelay(2) ->timeout(30) ->withStrategy(new CustomStrategy()) ->withProgress($callback) ->retryIf($condition) ->run(fn() => operation());
Service Container Bindings
Customize the service registration:
use GregPriday\LaravelRetry\Retry; use GregPriday\LaravelRetry\Strategies\ExponentialBackoffStrategy; $this->app->bind(Retry::class, function ($app) { return new Retry( maxRetries: 5, retryDelay: 1, timeout: 30, strategy: new ExponentialBackoffStrategy( multiplier: 2.0, maxDelay: 30, withJitter: true ) ); });
Performance Considerations
Memory Usage
The package maintains an exception history for each retry operation. For long-running processes or high-frequency retries, consider:
// Clear exception history between runs if needed $retry = Retry::make(); foreach ($items as $item) { $result = $retry->run(fn() => processItem($item)); // History is automatically reset on next run }
Timeout Handling
Set appropriate timeouts to prevent long-running operations:
Retry::make() ->timeout(5) // Maximum time per attempt ->run(function() { return Http::timeout(3)->get('https://api.example.com'); });
Common Patterns & Best Practices
Idempotency
Ensure operations are safe to retry:
Retry::run(function() { return DB::transaction(function() { // Use idempotency keys or check existence if (!Payment::whereReference($ref)->exists()) { return Payment::create([...]); } }); });
Rate Limiting
Handle API rate limits:
$strategy = new RateLimitStrategy( new ExponentialBackoffStrategy(), maxAttempts: 100, timeWindow: 60, storageKey: 'api-client' // Separate limits per client ); Retry::withStrategy($strategy) ->retryIf(function($e) { return $e instanceof RateLimitException; }) ->run(fn() => apiCall());
Circuit Breaking
Protect downstream services:
$circuitBreaker = new CircuitBreakerStrategy( new ExponentialBackoffStrategy(), failureThreshold: 5, resetTimeout: 60 ); $retry = Retry::make()->withStrategy($circuitBreaker); while (true) { try { $result = $retry->run(fn() => serviceCall()); // Process result... } catch (Exception $e) { // Circuit is open, wait before retrying sleep($circuitBreaker->getResetTimeout()); } }
Contributing
We welcome contributions! Please see CONTRIBUTING.md for details.
Development Setup
- Clone the repository
- Install dependencies:
composer install
Running Tests
composer test
Style fixing:
composer format
Submitting Changes
- Fork the repository
- Create a feature branch
- Commit your changes
- Push to the branch
- Create a Pull Request
License
The MIT License (MIT). Please see License File for more information.
Support
- For bugs and features, use the GitHub issue tracker
- Feel free to email me.