Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry\Events;
use App\Framework\Core\ValueObjects\Timestamp;
use Throwable;
/**
* Event für jeden Retry-Versuch
*/
final readonly class RetryAttemptEvent
{
public function __construct(
public int $attempt,
public int $maxAttempts,
public ?Throwable $lastException,
public array $context,
public Timestamp $timestamp
) {
}
public function isFirstAttempt(): bool
{
return $this->attempt === 1;
}
public function isRetryAttempt(): bool
{
return $this->attempt > 1;
}
public function isLastAttempt(): bool
{
return $this->attempt >= $this->maxAttempts;
}
public function getOperationType(): ?string
{
return $this->context['operation_type'] ?? null;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry\Events;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Retry\RetryResult;
/**
* Event für fehlgeschlagene Retry-Operationen
*/
final readonly class RetryFailedEvent
{
public function __construct(
public RetryResult $result,
public array $context,
public Timestamp $timestamp
) {
}
public function getAttemptCount(): int
{
return $this->result->totalAttempts;
}
public function getDurationMs(): int
{
return (int)$this->result->totalDuration->toMilliseconds();
}
public function getLastException(): ?\Throwable
{
return $this->result->lastException;
}
public function getOperationType(): ?string
{
return $this->context['operation_type'] ?? null;
}
public function getAttemptHistory(): array
{
return $this->result->attemptHistory;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry\Events;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Retry\RetryResult;
/**
* Event für erfolgreiche Retry-Operationen
*/
final readonly class RetrySucceededEvent
{
public function __construct(
public RetryResult $result,
public array $context,
public Timestamp $timestamp
) {
}
public function wasRetriedOperation(): bool
{
return $this->result->totalAttempts > 1;
}
public function getAttemptCount(): int
{
return $this->result->totalAttempts;
}
public function getDurationMs(): int
{
return (int)$this->result->totalDuration->toMilliseconds();
}
public function getOperationType(): ?string
{
return $this->context['operation_type'] ?? null;
}
}

View File

@@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry\Metrics;
use App\Framework\Core\Events\OnEvent;
use App\Framework\Retry\Events\RetryAttemptEvent;
use App\Framework\Retry\Events\RetryFailedEvent;
use App\Framework\Retry\Events\RetrySucceededEvent;
/**
* Sammelt Metriken für Retry-Operationen
*/
final class RetryMetrics
{
private array $stats = [
'total_operations' => 0,
'succeeded_operations' => 0,
'failed_operations' => 0,
'total_attempts' => 0,
'retry_rate' => 0.0,
'success_after_retry_rate' => 0.0,
'by_operation_type' => [],
'by_exception_type' => [],
'avg_attempts_per_operation' => 0.0,
'avg_duration_ms' => 0.0,
];
#[OnEvent]
public function onRetryAttempt(RetryAttemptEvent $event): void
{
$this->stats['total_attempts']++;
$operationType = $event->getOperationType() ?? 'unknown';
if (! isset($this->stats['by_operation_type'][$operationType])) {
$this->stats['by_operation_type'][$operationType] = [
'attempts' => 0,
'operations' => 0,
'successes' => 0,
'failures' => 0,
];
}
$this->stats['by_operation_type'][$operationType]['attempts']++;
// Exception-Statistiken
if ($event->lastException !== null) {
$exceptionType = $event->lastException::class;
if (! isset($this->stats['by_exception_type'][$exceptionType])) {
$this->stats['by_exception_type'][$exceptionType] = 0;
}
$this->stats['by_exception_type'][$exceptionType]++;
}
}
#[OnEvent]
public function onRetrySucceeded(RetrySucceededEvent $event): void
{
$this->stats['total_operations']++;
$this->stats['succeeded_operations']++;
$operationType = $event->getOperationType() ?? 'unknown';
if (! isset($this->stats['by_operation_type'][$operationType])) {
$this->stats['by_operation_type'][$operationType] = [
'attempts' => 0,
'operations' => 0,
'successes' => 0,
'failures' => 0,
];
}
$this->stats['by_operation_type'][$operationType]['operations']++;
$this->stats['by_operation_type'][$operationType]['successes']++;
$this->updateCalculatedStats();
}
#[OnEvent]
public function onRetryFailed(RetryFailedEvent $event): void
{
$this->stats['total_operations']++;
$this->stats['failed_operations']++;
$operationType = $event->getOperationType() ?? 'unknown';
if (! isset($this->stats['by_operation_type'][$operationType])) {
$this->stats['by_operation_type'][$operationType] = [
'attempts' => 0,
'operations' => 0,
'successes' => 0,
'failures' => 0,
];
}
$this->stats['by_operation_type'][$operationType]['operations']++;
$this->stats['by_operation_type'][$operationType]['failures']++;
$this->updateCalculatedStats();
}
public function getStats(): array
{
return $this->stats;
}
public function getSuccessRate(): float
{
if ($this->stats['total_operations'] === 0) {
return 0.0;
}
return $this->stats['succeeded_operations'] / $this->stats['total_operations'];
}
public function getRetryRate(): float
{
return $this->stats['retry_rate'];
}
public function getAverageAttemptsPerOperation(): float
{
return $this->stats['avg_attempts_per_operation'];
}
public function getMostRetriedOperations(): array
{
$operations = $this->stats['by_operation_type'];
uasort($operations, function ($a, $b) {
return $b['attempts'] <=> $a['attempts'];
});
return array_slice($operations, 0, 5, true);
}
public function getMostCommonExceptions(): array
{
arsort($this->stats['by_exception_type']);
return array_slice($this->stats['by_exception_type'], 0, 5, true);
}
public function reset(): void
{
$this->stats = [
'total_operations' => 0,
'succeeded_operations' => 0,
'failed_operations' => 0,
'total_attempts' => 0,
'retry_rate' => 0.0,
'success_after_retry_rate' => 0.0,
'by_operation_type' => [],
'by_exception_type' => [],
'avg_attempts_per_operation' => 0.0,
'avg_duration_ms' => 0.0,
];
}
private function updateCalculatedStats(): void
{
// Retry-Rate: Wie viele Operationen wurden wiederholt?
$operationsWithRetry = 0;
foreach ($this->stats['by_operation_type'] as $stats) {
if ($stats['attempts'] > $stats['operations']) {
$operationsWithRetry += $stats['operations'];
}
}
if ($this->stats['total_operations'] > 0) {
$this->stats['retry_rate'] = $operationsWithRetry / $this->stats['total_operations'];
$this->stats['avg_attempts_per_operation'] = $this->stats['total_attempts'] / $this->stats['total_operations'];
}
// Success-After-Retry-Rate
if ($operationsWithRetry > 0) {
$successAfterRetry = 0;
foreach ($this->stats['by_operation_type'] as $stats) {
if ($stats['attempts'] > $stats['operations']) {
$successAfterRetry += $stats['successes'];
}
}
$this->stats['success_after_retry_rate'] = $successAfterRetry / $operationsWithRetry;
}
}
}

View File

@@ -0,0 +1,288 @@
# 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
```php
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
```php
// 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
```php
// 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
```php
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
```php
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).
```php
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:
```php
// 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
```php
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:
```php
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:**
```php
use App\Framework\Database\Middleware\RetryMiddleware;
$middleware = new RetryMiddleware($timer, maxRetries: 3, retryDelayMs: 100);
```
**After:**
```php
use App\Framework\Database\Middleware\UnifiedRetryMiddleware;
$middleware = new UnifiedRetryMiddleware($clock, $eventDispatcher);
```
### HttpClient RetryMiddleware
**Before:**
```php
use App\Framework\HttpClient\Middleware\RetryMiddleware;
$middleware = new RetryMiddleware($timer, maxRetries: 3, baseDelay: 1.0);
```
**After:**
```php
use App\Framework\HttpClient\Middleware\UnifiedRetryMiddleware;
$middleware = UnifiedRetryMiddleware::forApi($clock, $eventDispatcher);
```
## RetryableOperation Interface
For more complex scenarios, implement the `RetryableOperation` interface:
```php
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:
```php
// 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
```php
$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

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry;
use App\Framework\Core\Events\EventDispatcherInterface;
use App\Framework\DateTime\Clock;
use App\Framework\DI\Initializer;
/**
* Initializer für das Retry-System
*
* Registriert RetryManager im DI-Container
*/
#[Initializer]
final readonly class RetryInitializer
{
public function __invoke(Clock $clock, ?EventDispatcherInterface $eventDispatcher = null): RetryManager
{
$retryManager = RetryManager::create($clock);
if ($eventDispatcher !== null) {
$retryManager = $retryManager->withEventDispatcher($eventDispatcher);
}
return $retryManager;
}
}

View File

@@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry;
use App\Framework\Core\Events\EventDispatcherInterface;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\Retry\Events\RetryAttemptEvent;
use App\Framework\Retry\Events\RetryFailedEvent;
use App\Framework\Retry\Events\RetrySucceededEvent;
use App\Framework\Retry\Strategies\ExponentialBackoffStrategy;
use App\Framework\Retry\Strategies\FixedRetryStrategy;
use App\Framework\Retry\Strategies\LinearDelayStrategy;
use Throwable;
/**
* Zentraler Retry-Manager mit Fluent API
*
* Bietet eine einheitliche Schnittstelle für alle Retry-Operationen
*/
final class RetryManager
{
private RetryStrategy $strategy;
private ?EventDispatcherInterface $eventDispatcher = null;
private array $context = [];
public function __construct(
private readonly Clock $clock,
?RetryStrategy $strategy = null
) {
$this->strategy = $strategy ?? new FixedRetryStrategy(3);
}
/**
* Factory-Methoden für fluent API
*/
public static function create(Clock $clock): self
{
return new self($clock);
}
/**
* Fluent API für Strategien
*/
public function withStrategy(RetryStrategy $strategy): self
{
$clone = clone $this;
$clone->strategy = $strategy;
return $clone;
}
public function exponentialBackoff(
int $maxAttempts = 3,
int $initialDelayMs = 100,
float $multiplier = 2.0
): self {
return $this->withStrategy(new ExponentialBackoffStrategy(
maxAttempts: $maxAttempts,
initialDelay: Duration::fromMilliseconds($initialDelayMs),
multiplier: $multiplier
));
}
public function linearDelay(int $maxAttempts = 3, int $delayMs = 500): self
{
return $this->withStrategy(new LinearDelayStrategy(
maxAttempts: $maxAttempts,
delay: Duration::fromMilliseconds($delayMs)
));
}
public function fixedRetry(int $maxAttempts = 3): self
{
return $this->withStrategy(new FixedRetryStrategy($maxAttempts));
}
/**
* Event-System Integration
*/
public function withEventDispatcher(EventDispatcherInterface $dispatcher): self
{
$clone = clone $this;
$clone->eventDispatcher = $dispatcher;
return $clone;
}
public function withContext(array $context): self
{
$clone = clone $this;
$clone->context = array_merge($this->context, $context);
return $clone;
}
/**
* Hauptmethode: Führt Operation mit Retry aus
*/
public function execute(callable $operation): RetryResult
{
$startTime = $this->clock->time();
$attemptHistory = [];
$currentAttempt = 0;
$lastException = null;
while (true) {
$currentAttempt++;
$attemptStart = $this->clock->time();
try {
// Event für Versuch
$this->dispatchAttemptEvent($currentAttempt, $lastException);
// Operation ausführen
$result = $operation();
// Erfolg!
$totalDuration = Duration::between($startTime, $this->clock->time());
$retryResult = RetryResult::success(
result: $result,
attempts: $currentAttempt,
duration: $totalDuration,
history: $attemptHistory
);
$this->dispatchSuccessEvent($retryResult);
return $retryResult;
} catch (Throwable $exception) {
$attemptDuration = Duration::between($attemptStart, $this->clock->time());
$attemptHistory[] = [
'attempt' => $currentAttempt,
'exception' => $exception::class,
'message' => $exception->getMessage(),
'duration' => $attemptDuration->toMilliseconds(),
];
$lastException = $exception;
// Prüfen ob Retry möglich
if (! $this->strategy->shouldRetry($currentAttempt, $exception)) {
// Keine weiteren Versuche
$totalDuration = Duration::between($startTime, $this->clock->time());
$retryResult = RetryResult::failure(
exception: $exception,
attempts: $currentAttempt,
duration: $totalDuration,
history: $attemptHistory
);
$this->dispatchFailureEvent($retryResult);
return $retryResult;
}
// Delay vor nächstem Versuch
$delay = $this->strategy->getDelay($currentAttempt, $exception);
if (! $delay->isZero()) {
usleep((int)$delay->toMicroseconds());
}
}
}
}
/**
* RetryableOperation Interface Support
*/
public function executeOperation(RetryableOperation $operation): RetryResult
{
return $this->execute(function () use ($operation) {
return $operation->execute();
});
}
/**
* Convenience-Methoden für häufige Szenarien
*/
public function executeDatabaseOperation(callable $operation): RetryResult
{
return $this
->withStrategy(ExponentialBackoffStrategy::forDatabase())
->withContext(['operation_type' => 'database'])
->execute($operation);
}
public function executeHttpRequest(callable $operation): RetryResult
{
return $this
->withStrategy(ExponentialBackoffStrategy::forHttpClient())
->withContext(['operation_type' => 'http'])
->execute($operation);
}
public function executeCacheOperation(callable $operation): RetryResult
{
return $this
->withStrategy(ExponentialBackoffStrategy::forCache())
->withContext(['operation_type' => 'cache'])
->execute($operation);
}
/**
* Event-Dispatching
*/
private function dispatchAttemptEvent(int $attempt, ?Throwable $lastException): void
{
if ($this->eventDispatcher === null) {
return;
}
$this->eventDispatcher->dispatch(new RetryAttemptEvent(
attempt: $attempt,
maxAttempts: $this->strategy->getMaxAttempts(),
lastException: $lastException,
context: $this->context,
timestamp: $this->clock->time()
));
}
private function dispatchSuccessEvent(RetryResult $result): void
{
if ($this->eventDispatcher === null) {
return;
}
$this->eventDispatcher->dispatch(new RetrySucceededEvent(
result: $result,
context: $this->context,
timestamp: $this->clock->time()
));
}
private function dispatchFailureEvent(RetryResult $result): void
{
if ($this->eventDispatcher === null) {
return;
}
$this->eventDispatcher->dispatch(new RetryFailedEvent(
result: $result,
context: $this->context,
timestamp: $this->clock->time()
));
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry;
use App\Framework\Core\ValueObjects\Duration;
use Throwable;
/**
* Ergebnis einer Retry-Operation mit Metadaten
*/
final readonly class RetryResult
{
public function __construct(
public mixed $result,
public bool $succeeded,
public int $totalAttempts,
public Duration $totalDuration,
public ?Throwable $lastException = null,
public array $attemptHistory = []
) {
}
public static function success(
mixed $result,
int $attempts,
Duration $duration,
array $history = []
): self {
return new self(
result: $result,
succeeded: true,
totalAttempts: $attempts,
totalDuration: $duration,
attemptHistory: $history
);
}
public static function failure(
Throwable $exception,
int $attempts,
Duration $duration,
array $history = []
): self {
return new self(
result: null,
succeeded: false,
totalAttempts: $attempts,
totalDuration: $duration,
lastException: $exception,
attemptHistory: $history
);
}
public function getResult(): mixed
{
if (! $this->succeeded) {
throw $this->lastException ?? new \RuntimeException('Operation failed without exception');
}
return $this->result;
}
public function wasSuccessful(): bool
{
return $this->succeeded;
}
public function getAttemptCount(): int
{
return $this->totalAttempts;
}
public function getTotalDuration(): Duration
{
return $this->totalDuration;
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry;
use App\Framework\Core\ValueObjects\Duration;
use Throwable;
/**
* Interface für Retry-Strategien
*
* Definiert wie und wann Operationen wiederholt werden sollen
*/
interface RetryStrategy
{
/**
* Entscheidet ob ein weiterer Retry-Versuch gemacht werden soll
*/
public function shouldRetry(int $currentAttempt, Throwable $exception): bool;
/**
* Bestimmt die Wartezeit vor dem nächsten Versuch
*/
public function getDelay(int $currentAttempt, Throwable $exception): Duration;
/**
* Maximale Anzahl von Versuchen
*/
public function getMaxAttempts(): int;
/**
* Prüft ob diese Exception retry-bar ist
*/
public function isRetryableException(Throwable $exception): bool;
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry;
use Throwable;
/**
* Interface für retry-bare Operationen
*/
interface RetryableOperation
{
/**
* Führt die Operation aus
*
* @throws Throwable Wenn die Operation fehlschlägt
*/
public function execute(): mixed;
/**
* Prüft ob die Operation bei diesem Fehler wiederholt werden kann
*/
public function canRetry(Throwable $exception): bool;
/**
* Optional: Bereitet die Operation für einen Retry vor
*/
public function prepareRetry(int $attempt, Throwable $lastException): void;
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry\Strategies;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Retry\RetryStrategy;
use Throwable;
/**
* Exponential Backoff Retry-Strategie
*
* Verdoppelt die Wartezeit bei jedem Versuch: 100ms -> 200ms -> 400ms -> 800ms
* Mit optionalem Jitter zur Vermeidung von Thundering Herd
*/
final readonly class ExponentialBackoffStrategy implements RetryStrategy
{
public function __construct(
private int $maxAttempts = 3,
private Duration $initialDelay = new Duration(100), // 100ms
private float $multiplier = 2.0,
private Duration $maxDelay = new Duration(10000), // 10s
private bool $useJitter = true,
private array $retryableExceptions = [
\RuntimeException::class,
\Exception::class,
]
) {
}
public function shouldRetry(int $currentAttempt, Throwable $exception): bool
{
if ($currentAttempt >= $this->maxAttempts) {
return false;
}
return $this->isRetryableException($exception);
}
public function getDelay(int $currentAttempt, Throwable $exception): Duration
{
if ($currentAttempt === 0) {
return Duration::zero();
}
// Exponential: delay = initial * (multiplier ^ (attempt - 1))
$delay = $this->initialDelay->toMilliseconds() *
pow($this->multiplier, $currentAttempt - 1);
// Maximal-Grenze
$delay = min($delay, $this->maxDelay->toMilliseconds());
// Jitter hinzufügen (±25% Varianz)
if ($this->useJitter) {
$jitter = (int)($delay * 0.25);
$delay = $delay + (random_int(-$jitter, $jitter));
}
return Duration::fromMilliseconds((int) $delay);
}
public function getMaxAttempts(): int
{
return $this->maxAttempts;
}
public function isRetryableException(Throwable $exception): bool
{
foreach ($this->retryableExceptions as $retryableClass) {
if ($exception instanceof $retryableClass) {
return true;
}
}
return false;
}
/**
* Factory Methods für häufige Szenarien
*/
public static function forDatabase(int $maxAttempts = 3): self
{
return new self(
maxAttempts: $maxAttempts,
initialDelay: Duration::fromMilliseconds(50),
multiplier: 2.0,
maxDelay: Duration::fromSeconds(5),
retryableExceptions: [
\PDOException::class,
'App\Framework\Database\Exception\DatabaseException',
]
);
}
public static function forHttpClient(int $maxAttempts = 3): self
{
return new self(
maxAttempts: $maxAttempts,
initialDelay: Duration::fromMilliseconds(200),
multiplier: 2.0,
maxDelay: Duration::fromSeconds(10),
retryableExceptions: [
'App\Framework\HttpClient\Exception\HttpClientException',
'App\Framework\HttpClient\Exception\CurlExecutionFailed',
]
);
}
public static function forCache(int $maxAttempts = 2): self
{
return new self(
maxAttempts: $maxAttempts,
initialDelay: Duration::fromMilliseconds(10),
multiplier: 3.0,
maxDelay: Duration::fromMilliseconds(1000),
useJitter: false, // Cache sollte schnell sein
retryableExceptions: [
\RedisException::class,
\RuntimeException::class,
]
);
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry\Strategies;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Retry\RetryStrategy;
use Throwable;
/**
* Fixed Retry-Strategie ohne Delay
*
* Versucht sofort erneut, ohne Wartezeit
* Gut für schnelle In-Memory-Operationen
*/
final readonly class FixedRetryStrategy implements RetryStrategy
{
public function __construct(
private int $maxAttempts = 3,
private array $retryableExceptions = [
\RuntimeException::class,
\Exception::class,
]
) {
}
public function shouldRetry(int $currentAttempt, Throwable $exception): bool
{
if ($currentAttempt >= $this->maxAttempts) {
return false;
}
return $this->isRetryableException($exception);
}
public function getDelay(int $currentAttempt, Throwable $exception): Duration
{
return Duration::zero(); // Kein Delay
}
public function getMaxAttempts(): int
{
return $this->maxAttempts;
}
public function isRetryableException(Throwable $exception): bool
{
foreach ($this->retryableExceptions as $retryableClass) {
if ($exception instanceof $retryableClass) {
return true;
}
}
return false;
}
/**
* Factory für häufige Szenarien
*/
public static function quick(int $maxAttempts = 2): self
{
return new self(maxAttempts: $maxAttempts);
}
public static function forInMemoryOperations(): self
{
return new self(
maxAttempts: 3,
retryableExceptions: [
\RuntimeException::class,
\LogicException::class,
]
);
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Framework\Retry\Strategies;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Retry\RetryStrategy;
use Throwable;
/**
* Linear Delay Retry-Strategie
*
* Konstante Wartezeit zwischen Versuchen: 500ms -> 500ms -> 500ms
* Gut für Services mit vorhersagbaren Recovery-Zeiten
*/
final readonly class LinearDelayStrategy implements RetryStrategy
{
public function __construct(
private int $maxAttempts = 3,
private Duration $delay = new Duration(500),
private array $retryableExceptions = [
\RuntimeException::class,
\Exception::class,
]
) {
}
public function shouldRetry(int $currentAttempt, Throwable $exception): bool
{
if ($currentAttempt >= $this->maxAttempts) {
return false;
}
return $this->isRetryableException($exception);
}
public function getDelay(int $currentAttempt, Throwable $exception): Duration
{
if ($currentAttempt === 0) {
return Duration::zero();
}
return $this->delay;
}
public function getMaxAttempts(): int
{
return $this->maxAttempts;
}
public function isRetryableException(Throwable $exception): bool
{
foreach ($this->retryableExceptions as $retryableClass) {
if ($exception instanceof $retryableClass) {
return true;
}
}
return false;
}
/**
* Factory Methods
*/
public static function fast(int $maxAttempts = 3): self
{
return new self(
maxAttempts: $maxAttempts,
delay: Duration::fromMilliseconds(100)
);
}
public static function medium(int $maxAttempts = 3): self
{
return new self(
maxAttempts: $maxAttempts,
delay: Duration::fromMilliseconds(500)
);
}
public static function slow(int $maxAttempts = 3): self
{
return new self(
maxAttempts: $maxAttempts,
delay: Duration::fromSeconds(2)
);
}
}