- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
Retry Framework
Unified retry system for the PHP framework providing consistent retry logic across all components.
Overview
The Retry Framework consolidates retry logic from various parts of the system (Database, HttpClient, Cache, etc.) into a single, configurable, and observable system.
Quick Start
Basic Usage
use App\Framework\Retry\RetryManager;
// Simple retry with exponential backoff
$retryManager = RetryManager::create($clock)
->exponentialBackoff(maxAttempts: 3, initialDelayMs: 100);
$result = $retryManager->execute(function() {
// Your operation that might fail
return $this->unreliableApiCall();
});
// Get the result (throws exception if all retries failed)
$data = $result->getResult();
Fluent API Examples
// Linear delay strategy
$result = RetryManager::create($clock)
->linearDelay(maxAttempts: 5, delayMs: 500)
->execute($operation);
// Fixed retry (no delay)
$result = RetryManager::create($clock)
->fixedRetry(maxAttempts: 2)
->execute($operation);
// Custom exponential backoff
$result = RetryManager::create($clock)
->exponentialBackoff(
maxAttempts: 4,
initialDelayMs: 50,
multiplier: 3.0
)
->execute($operation);
Pre-configured Scenarios
// Database operations
$result = $retryManager->executeDatabaseOperation(function() {
return $this->database->query('SELECT * FROM users');
});
// HTTP requests
$result = $retryManager->executeHttpRequest(function() {
return $this->httpClient->get('https://api.example.com/data');
});
// Cache operations
$result = $retryManager->executeCacheOperation(function() {
return $this->cache->get('expensive-computation');
});
Strategies
ExponentialBackoffStrategy
Doubles the delay between retries: 100ms → 200ms → 400ms → 800ms
use App\Framework\Retry\Strategies\ExponentialBackoffStrategy;
// Custom strategy
$strategy = new ExponentialBackoffStrategy(
maxAttempts: 3,
initialDelay: Duration::fromMilliseconds(100),
multiplier: 2.0,
maxDelay: Duration::fromSeconds(10),
useJitter: true
);
$retryManager = RetryManager::create($clock)->withStrategy($strategy);
Pre-configured factories:
ExponentialBackoffStrategy::forDatabase()- Optimized for database operationsExponentialBackoffStrategy::forHttpClient()- Optimized for HTTP requestsExponentialBackoffStrategy::forCache()- Optimized for cache operations
LinearDelayStrategy
Constant delay between retries: 500ms → 500ms → 500ms
use App\Framework\Retry\Strategies\LinearDelayStrategy;
$strategy = LinearDelayStrategy::medium(3); // 500ms delay, 3 attempts
$retryManager = RetryManager::create($clock)->withStrategy($strategy);
Pre-configured factories:
LinearDelayStrategy::fast()- 100ms delayLinearDelayStrategy::medium()- 500ms delayLinearDelayStrategy::slow()- 2s delay
FixedRetryStrategy
No delay between retries (immediate retry).
use App\Framework\Retry\Strategies\FixedRetryStrategy;
$strategy = FixedRetryStrategy::quick(2); // 2 attempts, no delay
$retryManager = RetryManager::create($clock)->withStrategy($strategy);
Event System Integration
The retry system emits events for monitoring and observability:
// Enable events
$retryManager = RetryManager::create($clock)
->withEventDispatcher($eventDispatcher)
->withContext(['service' => 'user-api']);
// Events are automatically dispatched:
// - RetryAttemptEvent: For each attempt
// - RetrySucceededEvent: When operation succeeds
// - RetryFailedEvent: When all retries are exhausted
Event Handlers
use App\Framework\Core\Events\OnEvent;
use App\Framework\Retry\Events\RetryFailedEvent;
class RetryMonitoring
{
#[OnEvent]
public function onRetryFailed(RetryFailedEvent $event): void
{
$this->logger->error('Retry operation failed', [
'attempts' => $event->getAttemptCount(),
'duration_ms' => $event->getDurationMs(),
'operation_type' => $event->getOperationType(),
'exception' => $event->getLastException()?->getMessage()
]);
}
}
Metrics and Monitoring
The system includes built-in metrics collection:
use App\Framework\Retry\Metrics\RetryMetrics;
// RetryMetrics automatically collects data via events
$metrics = $container->get(RetryMetrics::class);
// Get statistics
$stats = $metrics->getStats();
echo "Success rate: " . $metrics->getSuccessRate() * 100 . "%\n";
echo "Retry rate: " . $metrics->getRetryRate() * 100 . "%\n";
echo "Avg attempts: " . $metrics->getAverageAttemptsPerOperation() . "\n";
// Most problematic operations
$mostRetried = $metrics->getMostRetriedOperations();
$commonExceptions = $metrics->getMostCommonExceptions();
Migration from Legacy Middleware
Database RetryMiddleware
Before:
use App\Framework\Database\Middleware\RetryMiddleware;
$middleware = new RetryMiddleware($timer, maxRetries: 3, retryDelayMs: 100);
After:
use App\Framework\Database\Middleware\UnifiedRetryMiddleware;
$middleware = new UnifiedRetryMiddleware($clock, $eventDispatcher);
HttpClient RetryMiddleware
Before:
use App\Framework\HttpClient\Middleware\RetryMiddleware;
$middleware = new RetryMiddleware($timer, maxRetries: 3, baseDelay: 1.0);
After:
use App\Framework\HttpClient\Middleware\UnifiedRetryMiddleware;
$middleware = UnifiedRetryMiddleware::forApi($clock, $eventDispatcher);
RetryableOperation Interface
For more complex scenarios, implement the RetryableOperation interface:
use App\Framework\Retry\RetryableOperation;
class DatabaseBackupOperation implements RetryableOperation
{
public function execute(): mixed
{
return $this->performBackup();
}
public function canRetry(Throwable $exception): bool
{
// Don't retry on authentication errors
return !($exception instanceof AuthenticationException);
}
public function prepareRetry(int $attempt, Throwable $lastException): void
{
// Clean up before retry
$this->cleanup();
}
}
// Usage
$operation = new DatabaseBackupOperation();
$result = $retryManager->executeOperation($operation);
Configuration
The retry system integrates with the framework's dependency injection:
// In your service provider or initializer
$container->bind(RetryManager::class, function($container) {
return RetryManager::create($container->get(Clock::class))
->withEventDispatcher($container->get(EventDispatcherInterface::class));
});
Best Practices
- Choose appropriate strategies: Use exponential backoff for external services, linear delay for predictable services
- Set reasonable limits: Don't retry indefinitely, set max attempts and timeouts
- Monitor and alert: Use the event system to monitor retry patterns and failures
- Consider circuit breakers: For external services, combine with circuit breaker pattern
- Add context: Use
withContext()to add meaningful metadata for debugging
Error Handling
$result = $retryManager->execute($operation);
if ($result->wasSuccessful()) {
$data = $result->getResult();
echo "Success after {$result->getAttemptCount()} attempts\n";
} else {
echo "Failed after {$result->getAttemptCount()} attempts\n";
echo "Total duration: {$result->getTotalDuration()->toSeconds()}s\n";
throw $result->lastException;
}
Performance Considerations
- Jitter: Exponential backoff includes jitter by default to prevent thundering herd
- Memory: Retry history is kept in memory for the duration of the operation
- Events: Event dispatching adds minimal overhead; disable if not needed
- Strategies: Fixed retry has the lowest overhead, exponential backoff the highest