Files
michaelschiemer/src/Framework/Retry
Michael Schiemer c8b47e647d feat(Docker): Upgrade to PHP 8.5.0RC3 with native ext-uri support
BREAKING CHANGE: Requires PHP 8.5.0RC3

Changes:
- Update Docker base image from php:8.4-fpm to php:8.5.0RC3-fpm
- Enable ext-uri for native WHATWG URL parsing support
- Update composer.json PHP requirement from ^8.4 to ^8.5
- Add ext-uri as required extension in composer.json
- Move URL classes from Url.php85/ to Url/ directory (now compatible)
- Remove temporary PHP 8.4 compatibility workarounds

Benefits:
- Native URL parsing with Uri\WhatWg\Url class
- Better performance for URL operations
- Future-proof with latest PHP features
- Eliminates PHP version compatibility issues
2025-10-27 09:31:28 +01:00
..

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 operations
  • ExponentialBackoffStrategy::forHttpClient() - Optimized for HTTP requests
  • ExponentialBackoffStrategy::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 delay
  • LinearDelayStrategy::medium() - 500ms delay
  • LinearDelayStrategy::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

  1. Choose appropriate strategies: Use exponential backoff for external services, linear delay for predictable services
  2. Set reasonable limits: Don't retry indefinitely, set max attempts and timeouts
  3. Monitor and alert: Use the event system to monitor retry patterns and failures
  4. Consider circuit breakers: For external services, combine with circuit breaker pattern
  5. 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