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,504 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\CircuitBreaker\Events\CircuitBreakerClosed;
use App\Framework\CircuitBreaker\Events\CircuitBreakerEventPublisher;
use App\Framework\CircuitBreaker\Events\CircuitBreakerHalfOpened;
use App\Framework\CircuitBreaker\Events\CircuitBreakerOpened;
use App\Framework\CircuitBreaker\Registry\ServiceRegistry;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Clock;
use App\Framework\Logging\Logger;
use Throwable;
/**
* Circuit Breaker Pattern Implementation
*
* Schützt das System vor wiederholten Fehlern durch temporäres Blockieren von Requests
* nach einer bestimmten Anzahl von Fehlern.
*
* States:
* - CLOSED: Normal operation, alle Requests werden durchgelassen
* - OPEN: Service ist als fehlerhaft markiert, alle Requests werden abgelehnt
* - HALF_OPEN: Test-Phase, limitierte Requests werden durchgelassen
*/
final readonly class CircuitBreaker implements CircuitBreakerInterface
{
private const int DEFAULT_FAILURE_THRESHOLD = 5;
private const int DEFAULT_RECOVERY_TIMEOUT = 60; // seconds
private const int DEFAULT_HALF_OPEN_MAX_ATTEMPTS = 3;
private const int DEFAULT_SUCCESS_THRESHOLD = 3;
private string $cachePrefix;
public function __construct(
private Cache $cache,
private Clock $clock,
private ?Logger $logger = null,
private ?CircuitBreakerEventPublisher $eventPublisher = null,
private ?ServiceRegistry $serviceRegistry = null,
private string $namespace = 'default'
) {
$this->cachePrefix = 'circuit_breaker_' . $this->namespace . ':';
}
/**
* Prüft ob der Circuit Breaker für einen Service offen ist
*
* @throws CircuitBreakerException wenn der Circuit Breaker offen ist
*/
public function check(string $service, ?CircuitBreakerConfig $config = null): void
{
// Register service in registry
$this->registerService($service);
$config ??= $this->getDefaultConfig();
$state = $this->getState($service, $config);
if ($state === CircuitState::OPEN) {
$metrics = $this->getMetrics($service);
// Prüfen ob Recovery Timeout abgelaufen ist
if ($this->isRecoveryTimeoutExpired($service, $config)) {
$this->transitionToHalfOpen($service, $config);
$this->log('info', "Circuit breaker for '{$service}' transitioned to HALF_OPEN");
return;
}
// Circuit ist offen und Timeout noch nicht abgelaufen
$retryAfter = $this->calculateRetryAfter($service, $config);
throw new CircuitBreakerException(
service: $service,
state: CircuitState::OPEN,
failureCount: $metrics->failureCount,
retryAfterSeconds: $retryAfter
);
}
if ($state === CircuitState::HALF_OPEN) {
$halfOpenAttempts = $this->getHalfOpenAttempts($service);
if ($halfOpenAttempts >= $config->halfOpenMaxAttempts) {
// Zu viele Versuche im HALF_OPEN State, zurück zu OPEN
$this->transitionToOpen($service, 'Max half-open attempts exceeded', $config);
throw new CircuitBreakerException(
service: $service,
state: CircuitState::OPEN,
failureCount: $this->getMetrics($service)->failureCount,
retryAfterSeconds: $config->recoveryTimeout->toCacheSeconds()
);
}
// Request im HALF_OPEN State erlauben
$this->incrementHalfOpenAttempts($service, $config);
}
}
/**
* Markiert einen erfolgreichen Aufruf
*/
public function recordSuccess(string $service, ?CircuitBreakerConfig $config = null): void
{
$config ??= $this->getDefaultConfig();
$state = $this->getState($service, $config);
if ($state === CircuitState::HALF_OPEN) {
$successCount = $this->incrementSuccessCount($service, $config);
if ($successCount >= $config->successThreshold) {
// Genug erfolgreiche Aufrufe, Circuit schließen
$this->transitionToClosed($service, $config);
$this->log('info', "Circuit breaker for '{$service}' transitioned to CLOSED after {$successCount} successes");
}
} else {
// Im CLOSED State Success Count zurücksetzen
$this->resetMetrics($service);
}
}
/**
* Markiert einen fehlgeschlagenen Aufruf
*/
public function recordFailure(string $service, Throwable $exception, ?CircuitBreakerConfig $config = null): void
{
$config ??= $this->getDefaultConfig();
// Check if this exception should trigger the circuit breaker
if (! $config->shouldTriggerOnException($exception, $service)) {
$this->log('debug', "Exception ignored by failure predicate for service '{$service}': " . get_class($exception));
return;
}
$state = $this->getState($service, $config);
if ($state === CircuitState::HALF_OPEN) {
// Fehler im HALF_OPEN State führt sofort zu OPEN
$this->transitionToOpen($service, 'Failure in HALF_OPEN state: ' . $exception->getMessage(), $config);
return;
}
if ($state === CircuitState::CLOSED) {
$failureCount = $this->incrementFailureCount($service, $config);
if ($failureCount >= $config->failureThreshold) {
// Failure Threshold erreicht, Circuit öffnen
$this->transitionToOpen($service, 'Failure threshold exceeded: ' . $exception->getMessage(), $config);
$this->log('warning', "Circuit breaker for '{$service}' opened after {$failureCount} failures");
}
}
}
/**
* Führt eine Operation mit Circuit Breaker Schutz aus
*
* @template T
* @param callable(): T $operation
* @return T
* @throws Throwable
*/
public function execute(string $service, callable $operation, ?CircuitBreakerConfig $config = null): mixed
{
$this->check($service, $config);
try {
$result = $operation();
$this->recordSuccess($service, $config);
return $result;
} catch (Throwable $e) {
$this->recordFailure($service, $e, $config);
throw $e;
}
}
/**
* Gibt den aktuellen Zustand des Circuit Breakers zurück
*/
public function getState(string $service, ?CircuitBreakerConfig $config = null): CircuitState
{
$config ??= $this->getDefaultConfig();
$stateData = $this->cache->get($this->getStateKey($service));
if (! $stateData->isHit) {
return CircuitState::CLOSED;
}
// Ensure $stateData->value is an array and has a 'state' key
if (! is_array($stateData->value) || ! isset($stateData->value['state'])) {
$this->log('warning', "Invalid state data format in cache for service '{$service}'");
return CircuitState::CLOSED;
}
$state = CircuitState::from($stateData->value['state']);
// Prüfen ob OPEN State abgelaufen ist
if ($state === CircuitState::OPEN && $this->isRecoveryTimeoutExpired($service, $config)) {
return CircuitState::HALF_OPEN;
}
return $state;
}
/**
* Gibt detaillierte Metriken für einen Service zurück
*/
public function getMetrics(string $service): CircuitBreakerMetrics
{
$state = $this->getState($service);
$failureCountItem = $this->cache->get($this->getFailureCountKey($service));
$successCountItem = $this->cache->get($this->getSuccessCountKey($service));
$halfOpenAttemptsItem = $this->cache->get($this->getHalfOpenAttemptsKey($service));
$lastFailureTimeItem = $this->cache->get($this->getLastFailureTimeKey($service));
$openedAtItem = $this->cache->get($this->getOpenedAtKey($service));
return new CircuitBreakerMetrics(
state: $state,
failureCount: $failureCountItem->isHit ? $failureCountItem->value : 0,
successCount: $successCountItem->isHit ? $successCountItem->value : 0,
halfOpenAttempts: $halfOpenAttemptsItem->isHit ? $halfOpenAttemptsItem->value : 0,
lastFailureTime: $lastFailureTimeItem->isHit
? Timestamp::fromFloat($lastFailureTimeItem->value)
: null,
openedAt: $openedAtItem->isHit
? Timestamp::fromFloat($openedAtItem->value)
: null,
);
}
/**
* Setzt den Circuit Breaker für einen Service zurück
*/
public function reset(string $service): void
{
$keys = [
$this->getStateKey($service),
$this->getFailureCountKey($service),
$this->getSuccessCountKey($service),
$this->getHalfOpenAttemptsKey($service),
$this->getLastFailureTimeKey($service),
$this->getOpenedAtKey($service),
];
foreach ($keys as $key) {
$this->cache->forget($key);
}
$this->log('info', "Circuit breaker for '{$service}' has been reset");
}
/**
* Setzt alle Circuit Breaker zurück
*/
public function resetAll(): void
{
// Implementation hängt vom Cache-Backend ab
// Hier würde man alle Keys mit dem Prefix löschen
$this->log('info', 'All circuit breakers have been reset');
}
private function transitionToOpen(string $service, string $reason = '', ?CircuitBreakerConfig $config = null): void
{
$config ??= $this->getDefaultConfig();
$previousState = $this->getState($service, $config);
$now = Timestamp::fromClock($this->clock);
// EMERGENCY: Limit reason string to prevent memory explosion
$limitedReason = strlen($reason) > 1000
? substr($reason, 0, 1000) . '... (truncated for memory safety)'
: $reason;
$this->cache->set(CacheItem::forSet($this->getStateKey($service), [
'state' => CircuitState::OPEN->value,
'opened_at' => $now->toFloat(),
'reason' => $limitedReason,
], $config->metricsRetentionTime));
$this->cache->set(CacheItem::forSet($this->getOpenedAtKey($service), $now->toFloat(), $config->metricsRetentionTime));
$this->cache->set(CacheItem::forSet($this->getLastFailureTimeKey($service), $now->toFloat(), $config->metricsRetentionTime));
// Publish event
if ($this->eventPublisher !== null) {
$metrics = $this->getMetrics($service);
$event = new CircuitBreakerOpened(
service : $service,
namespace : $this->cachePrefix,
previousState: $previousState,
metrics : $metrics,
reason : $reason,
occurredAt : $now
);
$this->eventPublisher->publish($event);
}
}
private function transitionToHalfOpen(string $service, ?CircuitBreakerConfig $config = null): void
{
$config ??= $this->getDefaultConfig();
$previousState = $this->getState($service, $config);
$now = Timestamp::fromClock($this->clock);
$this->cache->set(CacheItem::forSet($this->getStateKey($service), [
'state' => CircuitState::HALF_OPEN->value,
'transitioned_at' => $now->toFloat(),
], $config->metricsRetentionTime));
// Reset half-open attempts counter
$this->cache->forget($this->getHalfOpenAttemptsKey($service));
$this->cache->forget($this->getSuccessCountKey($service));
// Publish event
if ($this->eventPublisher !== null) {
$metrics = $this->getMetrics($service);
$event = new CircuitBreakerHalfOpened(
service : $service,
namespace : $this->cachePrefix,
previousState: $previousState,
metrics : $metrics,
reason : 'Recovery timeout expired',
occurredAt : $now
);
$this->eventPublisher->publish($event);
}
}
private function transitionToClosed(string $service, ?CircuitBreakerConfig $config = null): void
{
$config ??= $this->getDefaultConfig();
$previousState = $this->getState($service, $config);
$metrics = $this->getMetrics($service);
$now = Timestamp::fromClock($this->clock);
$this->resetMetrics($service);
$this->log('info', "Circuit breaker for '{$service}' is now CLOSED");
// Publish event
if ($this->eventPublisher !== null) {
$event = new CircuitBreakerClosed(
service : $service,
namespace : $this->cachePrefix,
previousState: $previousState,
metrics : $metrics,
reason : 'Sufficient successful attempts',
occurredAt : $now
);
$this->eventPublisher->publish($event);
}
}
private function incrementFailureCount(string $service, ?CircuitBreakerConfig $config = null): int
{
$config ??= $this->getDefaultConfig();
$key = $this->getFailureCountKey($service);
$cacheItem = $this->cache->get($key);
$count = ($cacheItem->isHit ? $cacheItem->value : 0) + 1;
$this->cache->set(CacheItem::forSet($key, $count, $config->metricsRetentionTime));
// Update last failure time
$now = Timestamp::fromClock($this->clock);
$this->cache->set(CacheItem::forSet($this->getLastFailureTimeKey($service), $now->toFloat(), $config->metricsRetentionTime));
return $count;
}
private function incrementSuccessCount(string $service, ?CircuitBreakerConfig $config = null): int
{
$config ??= $this->getDefaultConfig();
$key = $this->getSuccessCountKey($service);
$cacheItem = $this->cache->get($key);
$count = ($cacheItem->isHit ? $cacheItem->value : 0) + 1;
$this->cache->set(CacheItem::forSet($key, $count, $config->metricsRetentionTime));
return $count;
}
private function incrementHalfOpenAttempts(string $service, ?CircuitBreakerConfig $config = null): int
{
$config ??= $this->getDefaultConfig();
$key = $this->getHalfOpenAttemptsKey($service);
$cacheItem = $this->cache->get($key);
$count = ($cacheItem->isHit ? $cacheItem->value : 0) + 1;
$this->cache->set(CacheItem::forSet($key, $count, $config->metricsRetentionTime));
return $count;
}
private function getHalfOpenAttempts(string $service): int
{
$cacheItem = $this->cache->get($this->getHalfOpenAttemptsKey($service));
if (! $cacheItem->isHit) {
return 0;
}
// Ensure we return an integer, handle case where value is false or invalid
$value = $cacheItem->value;
return is_int($value) ? $value : 0;
}
private function isRecoveryTimeoutExpired(string $service, CircuitBreakerConfig $config): bool
{
$metrics = $this->getMetrics($service);
return $metrics->hasRecoveryTimeoutExpired($config->recoveryTimeout);
}
private function calculateRetryAfter(string $service, CircuitBreakerConfig $config): int
{
$metrics = $this->getMetrics($service);
return $metrics->getRetryAfterDuration($config->recoveryTimeout)->toCacheSeconds();
}
private function resetMetrics(string $service): void
{
$this->cache->forget($this->getStateKey($service));
$this->cache->forget($this->getFailureCountKey($service));
$this->cache->forget($this->getSuccessCountKey($service));
$this->cache->forget($this->getHalfOpenAttemptsKey($service));
}
private function getDefaultConfig(): CircuitBreakerConfig
{
static $defaultConfig = null;
if ($defaultConfig === null) {
$defaultConfig = new CircuitBreakerConfig(
failureThreshold: self::DEFAULT_FAILURE_THRESHOLD,
recoveryTimeout: Duration::fromSeconds(self::DEFAULT_RECOVERY_TIMEOUT),
halfOpenMaxAttempts: self::DEFAULT_HALF_OPEN_MAX_ATTEMPTS,
successThreshold: self::DEFAULT_SUCCESS_THRESHOLD
);
}
return $defaultConfig;
}
// Cache Keys
private function getStateKey(string $service): CacheKey
{
return CacheKey::fromString($this->cachePrefix . $service . ':state');
}
private function getFailureCountKey(string $service): CacheKey
{
return CacheKey::fromString($this->cachePrefix . $service . ':failures');
}
private function getSuccessCountKey(string $service): CacheKey
{
return CacheKey::fromString($this->cachePrefix . $service . ':successes');
}
private function getHalfOpenAttemptsKey(string $service): CacheKey
{
return CacheKey::fromString($this->cachePrefix . $service . ':half_open_attempts');
}
private function getLastFailureTimeKey(string $service): CacheKey
{
return CacheKey::fromString($this->cachePrefix . $service . ':last_failure');
}
private function getOpenedAtKey(string $service): CacheKey
{
return CacheKey::fromString($this->cachePrefix . $service . ':opened_at');
}
private function registerService(string $service): void
{
if ($this->serviceRegistry !== null) {
$this->serviceRegistry->registerService($service, $this->namespace);
}
}
private function log(string $level, string $message): void
{
if ($this->logger !== null) {
match($level) {
'debug' => $this->logger->debug($message),
'info' => $this->logger->info($message),
'warning' => $this->logger->warning($message),
'error' => $this->logger->error($message),
default => $this->logger->info($message),
};
}
}
}

View File

@@ -0,0 +1,224 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker;
use App\Framework\CircuitBreaker\FailurePredicate\FailurePredicate;
use App\Framework\CircuitBreaker\FailurePredicate\FailurePredicateFactory;
use App\Framework\Core\ValueObjects\Duration;
/**
* Konfiguration für Circuit Breaker
*/
final readonly class CircuitBreakerConfig
{
public Duration $recoveryTimeout;
public Duration $metricsRetentionTime;
public Duration $slidingWindowSize;
public function __construct(
/**
* Anzahl der Fehler bevor der Circuit Breaker öffnet
*/
public int $failureThreshold = 5,
/**
* Zeit bis der Circuit Breaker von OPEN zu HALF_OPEN wechselt
*/
?Duration $recoveryTimeout = null,
/**
* Maximale Anzahl von Versuchen im HALF_OPEN State
*/
public int $halfOpenMaxAttempts = 3,
/**
* Anzahl erfolgreicher Aufrufe im HALF_OPEN State um zu CLOSED zu wechseln
*/
public int $successThreshold = 3,
/**
* Zeitfenster für Metriken-Aufbewahrung
*/
?Duration $metricsRetentionTime = null,
/**
* Sliding Window Größe für Failure Rate Berechnung
*/
?Duration $slidingWindowSize = null,
/**
* Failure predicate für erweiterte Fehlerbehandlung
*/
public ?FailurePredicate $failurePredicate = null,
) {
// Set defaults for nullable Duration fields
$this->recoveryTimeout = $recoveryTimeout ?? Duration::fromSeconds(60);
$this->metricsRetentionTime = $metricsRetentionTime ?? Duration::fromHours(1);
$this->slidingWindowSize = $slidingWindowSize ?? Duration::fromMinutes(5);
if ($this->failureThreshold < 1) {
throw new \InvalidArgumentException('Failure threshold must be at least 1');
}
if ($this->recoveryTimeout->isZero()) {
throw new \InvalidArgumentException('Recovery timeout must be greater than zero');
}
if ($this->halfOpenMaxAttempts < 1) {
throw new \InvalidArgumentException('Half-open max attempts must be at least 1');
}
if ($this->successThreshold < 1) {
throw new \InvalidArgumentException('Success threshold must be at least 1');
}
if ($this->metricsRetentionTime->isZero()) {
throw new \InvalidArgumentException('Metrics retention time must be greater than zero');
}
if ($this->slidingWindowSize->isZero()) {
throw new \InvalidArgumentException('Sliding window size must be greater than zero');
}
}
/**
* Prüft ob eine Exception den Circuit Breaker triggern soll
*/
public function shouldTriggerOnException(\Throwable $exception, string $service = 'default'): bool
{
if ($this->failurePredicate === null) {
// Default behavior: trigger on all exceptions
return true;
}
return $this->failurePredicate->shouldTrigger($exception, $service);
}
/**
* Erstellt eine Konfiguration für externe Services
*/
public static function forExternalService(
int $failureThreshold = 5,
?Duration $recoveryTimeout = null
): self {
return new self(
failureThreshold: $failureThreshold,
recoveryTimeout: $recoveryTimeout ?? Duration::fromMinutes(5),
halfOpenMaxAttempts: 3,
successThreshold: 2,
metricsRetentionTime: Duration::fromHours(2),
slidingWindowSize: Duration::fromMinutes(10),
failurePredicate: FailurePredicateFactory::forExternalService()
);
}
/**
* Erstellt eine Konfiguration für Datenbank-Verbindungen
*/
public static function forDatabase(
int $failureThreshold = 3,
?Duration $recoveryTimeout = null
): self {
return new self(
failureThreshold: $failureThreshold,
recoveryTimeout: $recoveryTimeout ?? Duration::fromSeconds(30),
halfOpenMaxAttempts: 2,
successThreshold: 2,
metricsRetentionTime: Duration::fromMinutes(30),
slidingWindowSize: Duration::fromMinutes(2),
failurePredicate: FailurePredicateFactory::forDatabase()
);
}
/**
* Erstellt eine strenge Konfiguration für kritische Services
*/
public static function strict(
int $failureThreshold = 3,
?Duration $recoveryTimeout = null
): self {
return new self(
failureThreshold: $failureThreshold,
recoveryTimeout: $recoveryTimeout ?? Duration::fromMinutes(10),
halfOpenMaxAttempts: 1,
successThreshold: 5,
metricsRetentionTime: Duration::fromHours(4),
slidingWindowSize: Duration::fromMinutes(15),
failurePredicate: FailurePredicateFactory::criticalErrorsOnly()
);
}
/**
* Komfortmethode für Duration-basierte Konfiguration
*/
public static function withDurations(
int $failureThreshold,
Duration $recoveryTimeout,
Duration $slidingWindow,
?Duration $metricsRetention = null,
?FailurePredicate $failurePredicate = null
): self {
return new self(
failureThreshold: $failureThreshold,
recoveryTimeout: $recoveryTimeout,
metricsRetentionTime: $metricsRetention ?? $recoveryTimeout->multiply(2),
slidingWindowSize: $slidingWindow,
failurePredicate: $failurePredicate
);
}
/**
* Erstellt eine Konfiguration mit custom FailurePredicate
*/
public static function withFailurePredicate(
FailurePredicate $failurePredicate,
int $failureThreshold = 5,
?Duration $recoveryTimeout = null
): self {
return new self(
failureThreshold: $failureThreshold,
recoveryTimeout: $recoveryTimeout ?? Duration::fromMinutes(2),
failurePredicate: $failurePredicate
);
}
/**
* Erstellt eine Konfiguration für HTTP Client
*/
public static function forHttpClient(
int $failureThreshold = 5,
?Duration $recoveryTimeout = null
): self {
return new self(
failureThreshold: $failureThreshold,
recoveryTimeout: $recoveryTimeout ?? Duration::fromMinutes(3),
halfOpenMaxAttempts: 2,
successThreshold: 2,
metricsRetentionTime: Duration::fromHours(1),
slidingWindowSize: Duration::fromMinutes(5),
failurePredicate: FailurePredicateFactory::forHttpClient()
);
}
/**
* Erstellt eine Konfiguration die nur Timeout-Fehler behandelt
*/
public static function forTimeoutErrors(
int $failureThreshold = 3,
?Duration $recoveryTimeout = null
): self {
return new self(
failureThreshold: $failureThreshold,
recoveryTimeout: $recoveryTimeout ?? Duration::fromMinutes(1),
halfOpenMaxAttempts: 2,
successThreshold: 2,
metricsRetentionTime: Duration::fromMinutes(30),
slidingWindowSize: Duration::fromMinutes(2),
failurePredicate: FailurePredicateFactory::timeoutErrors()
);
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
/**
* Exception thrown when Circuit Breaker is in OPEN state
*/
final class CircuitBreakerException extends FrameworkException
{
public function __construct(
public readonly string $service,
public readonly CircuitState $state,
public readonly int $failureCount,
public readonly int $retryAfterSeconds,
?\Throwable $previous = null
) {
$message = "Circuit breaker for service '{$service}' is {$state->value}. Service unavailable after {$failureCount} failures.";
$context = new ExceptionContext(
operation: 'circuit_breaker_check',
component: 'CircuitBreaker',
data: [
'service' => $service,
'state' => $state->value,
'failure_count' => $failureCount,
'retry_after_seconds' => $retryAfterSeconds,
],
metadata: [
'requires_alert' => true,
'recoverable' => true,
'error_code' => ErrorCode::SERVICE_CIRCUIT_OPEN->value,
'http_status' => 503,
'additional_headers' => [
'Retry-After' => (string) $retryAfterSeconds,
],
]
);
parent::__construct(
message: $message,
context: $context,
code: 503,
previous: $previous,
errorCode: ErrorCode::SERVICE_CIRCUIT_OPEN,
retryAfter: $retryAfterSeconds
);
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker;
use App\Framework\Cache\Cache;
use App\Framework\Cache\Driver\NullCache;
use App\Framework\Cache\GeneralCache;
use App\Framework\DateTime\Clock;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\HttpClient\HttpClient;
use App\Framework\Logging\Logger;
use App\Framework\Serializer\Json\JsonSerializer;
/**
* Initializer für Circuit Breaker Services
*/
final readonly class CircuitBreakerInitializer
{
#[Initializer]
public function initializeCircuitBreaker(Container $container): void
{
// Circuit Breaker Manager with EMERGENCY NullCache to prevent memory issues
$container->bind(CircuitBreakerManager::class, function (Container $container) {
// Create a GeneralCache with NullCache driver to implement Cache interface
$nullCache = new GeneralCache(
adapter: new NullCache(),
serializer: new JsonSerializer()
);
return CircuitBreakerManager::withDefaults(
cache: $nullCache, // EMERGENCY: Use NullCache wrapped in GeneralCache
clock: $container->get(Clock::class),
logger: $container->get(Logger::class)
);
});
// Standard Circuit Breaker
$container->bind(CircuitBreaker::class, function (Container $container) {
return $container->get(CircuitBreakerManager::class)->getCircuitBreaker();
});
// Database Circuit Breaker
$container->bind(DatabaseCircuitBreaker::class, function (Container $container) {
return new DatabaseCircuitBreaker(
circuitBreaker: $container->get(CircuitBreaker::class),
config: CircuitBreakerConfig::forDatabase()
);
});
// HTTP Client Circuit Breaker
$container->bind(HttpClientCircuitBreaker::class, function (Container $container) {
return new HttpClientCircuitBreaker(
circuitBreaker: $container->get(CircuitBreaker::class),
httpClient: $container->get(HttpClient::class)
);
});
// Circuit Breaker Middlewares
$container->bind('circuit_breaker_middleware_api', function (Container $container) {
return CircuitBreakerMiddleware::forApi(
$container->get(CircuitBreaker::class)
);
});
$container->bind('circuit_breaker_middleware_admin', function (Container $container) {
return CircuitBreakerMiddleware::forAdmin(
$container->get(CircuitBreaker::class)
);
});
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker;
/**
* Interface for Circuit Breaker implementations
*
* Provides a contract for circuit breaker functionality
* that can be implemented by different circuit breaker implementations
* or test doubles.
*/
interface CircuitBreakerInterface
{
/**
* Get metrics for a service
*
* @param string $service Service name
* @return CircuitBreakerMetrics Service metrics
*/
public function getMetrics(string $service): CircuitBreakerMetrics;
}

View File

@@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker;
use App\Framework\Cache\Cache;
use App\Framework\CircuitBreaker\Registry\ServiceRegistry;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\Logging\Logger;
/**
* Manager für Circuit Breaker Monitoring und Management
*/
final class CircuitBreakerManager
{
/**
* @var array<string, CircuitBreaker>
*/
private array $circuitBreakers;
/**
* @var array<string, CircuitBreakerConfig>
*/
private array $configurations;
public function __construct(
private readonly Cache $cache,
private readonly Clock $clock,
private readonly ?Logger $logger = null,
private readonly ?ServiceRegistry $serviceRegistry = null,
array $configurations = []
) {
$this->configurations = $configurations;
$this->circuitBreakers = [];
}
/**
* Holt oder erstellt einen Circuit Breaker für einen Service
*/
public function getCircuitBreaker(string $namespace = 'default'): CircuitBreaker
{
if (! isset($this->circuitBreakers[$namespace])) {
$this->circuitBreakers[$namespace] = new CircuitBreaker(
cache: $this->cache,
clock: $this->clock,
logger: $this->logger,
eventPublisher: null, // TODO: Add event publisher injection
serviceRegistry: $this->serviceRegistry,
namespace: $namespace
);
}
return $this->circuitBreakers[$namespace];
}
/**
* Gibt den aktuellen Status aller Services zurück
*/
public function getAllServicesStatus(): array
{
$status = [];
foreach ($this->circuitBreakers as $namespace => $circuitBreaker) {
$status[$namespace] = [];
// Alle Services in diesem Namespace finden
$services = $this->getServicesInNamespace($namespace);
foreach ($services as $service) {
$metrics = $circuitBreaker->getMetrics($service);
$status[$namespace][$service] = [
'state' => $metrics->state->value,
'failure_count' => $metrics->failureCount,
'success_count' => $metrics->successCount,
'half_open_attempts' => $metrics->halfOpenAttempts,
'last_failure_time' => $metrics->lastFailureTime?->toTimestamp(),
'opened_at' => $metrics->openedAt?->toTimestamp(),
'health_status' => $this->determineHealthStatus($metrics),
'configuration' => $this->getServiceConfiguration($service),
'summary' => $metrics->getSummary(),
];
}
}
return $status;
}
/**
* Gibt aggregierte Statistiken zurück
*/
public function getGlobalStatistics(): array
{
$stats = [
'total_services' => 0,
'healthy_services' => 0,
'degraded_services' => 0,
'failed_services' => 0,
'circuit_breakers_open' => 0,
'circuit_breakers_half_open' => 0,
'circuit_breakers_closed' => 0,
];
$allStatus = $this->getAllServicesStatus();
foreach ($allStatus as $namespace => $services) {
foreach ($services as $service => $serviceStatus) {
$stats['total_services']++;
match ($serviceStatus['state']) {
'closed' => $stats['circuit_breakers_closed']++,
'open' => $stats['circuit_breakers_open']++,
'half_open' => $stats['circuit_breakers_half_open']++,
};
match ($serviceStatus['health_status']) {
'healthy' => $stats['healthy_services']++,
'degraded' => $stats['degraded_services']++,
'failed' => $stats['failed_services']++,
};
}
}
return $stats;
}
/**
* Setzt alle Circuit Breaker zurück
*/
public function resetAll(): void
{
foreach ($this->circuitBreakers as $circuitBreaker) {
$circuitBreaker->resetAll();
}
if ($this->logger) {
$this->logger->info('All circuit breakers have been reset by CircuitBreakerManager');
}
}
/**
* Setzt einen spezifischen Service zurück
*/
public function resetService(string $service, string $namespace = 'default'): void
{
$circuitBreaker = $this->getCircuitBreaker($namespace);
$circuitBreaker->reset($service);
if ($this->logger) {
$this->logger->info("Circuit breaker for service '{$service}' in namespace '{$namespace}' has been reset");
}
}
/**
* Führt Health Checks für alle konfigurierten Services durch
*/
public function performHealthChecks(): array
{
$results = [];
$allStatus = $this->getAllServicesStatus();
foreach ($allStatus as $namespace => $services) {
$results[$namespace] = [];
foreach ($services as $service => $serviceStatus) {
$healthCheckResult = $this->performServiceHealthCheck($service, $namespace);
$results[$namespace][$service] = $healthCheckResult;
// Logging für kritische Zustände
if ($healthCheckResult['status'] === 'failed' && $serviceStatus['state'] === 'open') {
if ($this->logger) {
$this->logger->critical(
"Service '{$service}' in namespace '{$namespace}' is failing and circuit breaker is open",
$healthCheckResult
);
}
}
}
}
return $results;
}
/**
* Exportiert Konfiguration für Monitoring-Tools
*/
public function exportConfiguration(): array
{
return [
'circuit_breakers' => array_keys($this->circuitBreakers),
'configurations' => $this->configurations,
'timestamp' => $this->clock->now()->format('c'),
'version' => '1.0',
];
}
/**
* Importiert Konfiguration
*/
public function importConfiguration(array $config): void
{
if (isset($config['configurations'])) {
foreach ($config['configurations'] as $service => $serviceConfig) {
$this->configurations[$service] = new CircuitBreakerConfig(...$serviceConfig);
}
}
}
/**
* Findet alle Services in einem Namespace
*/
private function getServicesInNamespace(string $namespace): array
{
if ($this->serviceRegistry !== null) {
return $this->serviceRegistry->discoverServices($namespace);
}
// Fallback: Returniere die konfigurierten Services
$services = [];
foreach ($this->configurations as $service => $config) {
$services[] = $service;
}
return $services;
}
/**
* Bestimmt den Health Status basierend auf Metriken
*/
private function determineHealthStatus(CircuitBreakerMetrics $metrics): string
{
return match ($metrics->state) {
CircuitState::CLOSED => 'healthy',
CircuitState::HALF_OPEN => 'degraded',
CircuitState::OPEN => 'failed',
};
}
/**
* Holt die Konfiguration für einen Service
*/
private function getServiceConfiguration(string $service): ?array
{
$config = $this->configurations[$service] ?? null;
if ($config === null) {
return null;
}
return [
'failure_threshold' => $config->failureThreshold,
'recovery_timeout' => $config->recoveryTimeout->toHumanReadable(),
'half_open_max_attempts' => $config->halfOpenMaxAttempts,
'success_threshold' => $config->successThreshold,
'metrics_retention_time' => $config->metricsRetentionTime->toHumanReadable(),
'sliding_window_size' => $config->slidingWindowSize->toHumanReadable(),
];
}
/**
* Führt Health Check für einen Service durch
*/
private function performServiceHealthCheck(string $service, string $namespace): array
{
$circuitBreaker = $this->getCircuitBreaker($namespace);
$metrics = $circuitBreaker->getMetrics($service);
$healthCheck = [
'service' => $service,
'namespace' => $namespace,
'status' => $this->determineHealthStatus($metrics),
'checked_at' => $this->clock->now()->format('c'),
'metrics' => $metrics->toArray(),
'summary' => $metrics->getSummary(),
];
// Zusätzliche Health Check Logik hier implementieren
// z.B. ping external service, check database connection etc.
return $healthCheck;
}
/**
* Factory für Standard-Setup
*/
public static function withDefaults(
Cache $cache,
Clock $clock,
?Logger $logger = null,
?ServiceRegistry $serviceRegistry = null
): self {
$configs = [
'database' => CircuitBreakerConfig::forDatabase(),
'api' => new CircuitBreakerConfig(
failureThreshold: 5,
recoveryTimeout: Duration::fromMinutes(1),
halfOpenMaxAttempts: 3,
successThreshold: 2,
metricsRetentionTime: Duration::fromHours(1),
slidingWindowSize: Duration::fromMinutes(5)
),
'external_service' => CircuitBreakerConfig::forExternalService(),
];
return new self($cache, $clock, $logger, $serviceRegistry, $configs);
}
}

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Value Object für Circuit Breaker Metriken
*
* Immutable Datenstruktur die alle relevanten Metriken
* eines Circuit Breakers enthält.
*/
final readonly class CircuitBreakerMetrics
{
public function __construct(
public CircuitState $state,
public int $failureCount,
public int $successCount,
public int $halfOpenAttempts,
public ?Timestamp $lastFailureTime,
public ?Timestamp $openedAt,
) {
}
/**
* Prüft ob der Circuit Breaker verfügbar ist (CLOSED oder HALF_OPEN)
*/
public function isAvailable(): bool
{
return $this->state !== CircuitState::OPEN;
}
/**
* Prüft ob der Circuit Breaker blockiert (OPEN)
*/
public function isBlocked(): bool
{
return $this->state === CircuitState::OPEN;
}
/**
* Prüft ob sich der Circuit Breaker in der Test-Phase befindet
*/
public function isInTestPhase(): bool
{
return $this->state === CircuitState::HALF_OPEN;
}
/**
* Berechnet die Dauer seit dem Öffnen
*/
public function getUptimeDuration(): ?Duration
{
if ($this->openedAt === null) {
return null;
}
$now = Timestamp::now();
return $now->diff($this->openedAt);
}
/**
* Berechnet die Dauer seit dem letzten Fehler
*/
public function getTimeSinceLastFailure(): ?Duration
{
if ($this->lastFailureTime === null) {
return null;
}
$now = Timestamp::now();
return $now->diff($this->lastFailureTime);
}
/**
* Prüft ob genug Zeit seit dem letzten Fehler vergangen ist
*/
public function hasRecoveryTimeoutExpired(Duration $recoveryTimeout): bool
{
if ($this->openedAt === null) {
return true;
}
$uptime = $this->getUptimeDuration();
return $uptime !== null && $uptime->greaterThan($recoveryTimeout);
}
/**
* Berechnet die Zeit bis zum nächsten Retry-Versuch
*/
public function getRetryAfterDuration(Duration $recoveryTimeout): Duration
{
if ($this->openedAt === null) {
return Duration::zero();
}
$uptime = $this->getUptimeDuration();
if ($uptime === null || $uptime->greaterThan($recoveryTimeout)) {
return Duration::zero();
}
try {
return $recoveryTimeout->subtract($uptime);
} catch (\InvalidArgumentException) {
return Duration::zero();
}
}
/**
* Gibt eine zusammenfassende Beschreibung des aktuellen Zustands zurück
*/
public function getSummary(): string
{
return match ($this->state) {
CircuitState::CLOSED => sprintf(
'CLOSED - %d failures recorded',
$this->failureCount
),
CircuitState::OPEN => sprintf(
'OPEN - %d failures, opened %s ago',
$this->failureCount,
$this->getUptimeDuration()?->toHumanReadable() ?? 'unknown'
),
CircuitState::HALF_OPEN => sprintf(
'HALF_OPEN - %d/%d test attempts, %d successes',
$this->halfOpenAttempts,
$this->halfOpenAttempts + $this->successCount,
$this->successCount
),
};
}
/**
* Konvertiert zu Array-Format (für Backward-Compatibility)
*/
public function toArray(): array
{
return [
'state' => $this->state->value,
'failure_count' => $this->failureCount,
'success_count' => $this->successCount,
'half_open_attempts' => $this->halfOpenAttempts,
'last_failure_time' => $this->lastFailureTime?->toTimestamp(),
'opened_at' => $this->openedAt?->toTimestamp(),
];
}
/**
* Erstellt Metriken aus Array-Daten (für Migration)
*/
public static function fromArray(CircuitState $state, array $data): self
{
return new self(
state: $state,
failureCount: $data['failure_count'] ?? 0,
successCount: $data['success_count'] ?? 0,
halfOpenAttempts: $data['half_open_attempts'] ?? 0,
lastFailureTime: isset($data['last_failure_time'])
? Timestamp::fromFloat($data['last_failure_time'])
: null,
openedAt: isset($data['opened_at'])
? Timestamp::fromFloat($data['opened_at'])
: null,
);
}
}

View File

@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Http\Headers;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\Next;
use App\Framework\Http\Request;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Status;
use Throwable;
/**
* Middleware zur Integration von Circuit Breaker Pattern in HTTP-Requests
*/
final readonly class CircuitBreakerMiddleware implements HttpMiddleware
{
/**
* Services die durch Circuit Breaker geschützt werden sollen
* @var array<string, CircuitBreakerConfig>
*/
private array $protectedServices;
public function __construct(
private CircuitBreaker $circuitBreaker,
array $protectedServices = []
) {
$this->protectedServices = $protectedServices;
}
public function __invoke(MiddlewareContext $context, Next $next, RequestStateManager $stateManager): MiddlewareContext
{
$request = $context->request;
$route = $stateManager->get('route_name') ?? $request->path;
// Prüfen ob Route durch Circuit Breaker geschützt werden soll
$service = $this->getServiceForRoute($route);
if ($service === null) {
return $next($context);
}
$config = $this->protectedServices[$service] ?? null;
try {
// Circuit Breaker prüfen bevor Request verarbeitet wird
$this->circuitBreaker->check($service, $config);
// Request normal verarbeiten
$resultContext = $next($context);
// Erfolg nur bei 2xx Status Codes prüfen wenn Response vorhanden
if ($resultContext->hasResponse()) {
$response = $resultContext->response;
$statusCode = $response?->status->value;
if ($statusCode >= 200 && $statusCode < 300) {
$this->circuitBreaker->recordSuccess($service, $config);
} elseif ($statusCode >= 500) {
// 5xx Fehler als Circuit Breaker Fehler behandeln
$exception = new \Exception("HTTP {$statusCode} response");
$this->circuitBreaker->recordFailure($service, $exception, $config);
}
}
return $resultContext;
} catch (CircuitBreakerException $e) {
// Circuit Breaker ist offen - Service Unavailable Response
$response = $this->createCircuitOpenResponse($e);
return $context->withResponse($response);
} catch (Throwable $e) {
// Andere Exceptions als Circuit Breaker Fehler behandeln
$this->circuitBreaker->recordFailure($service, $e, $config);
throw $e;
}
}
/**
* Bestimmt welcher Service für eine Route zuständig ist
*/
private function getServiceForRoute(string $route): ?string
{
// Einfache Route-zu-Service Mapping
// Kann durch komplexere Logik ersetzt werden
if (str_starts_with($route, '/api/')) {
return 'api';
}
if (str_starts_with($route, '/admin/')) {
return 'admin';
}
// Weitere Service-Mappings...
foreach ($this->protectedServices as $service => $config) {
if (str_contains($route, $service)) {
return $service;
}
}
return null;
}
/**
* Erstellt Response wenn Circuit Breaker offen ist
*/
private function createCircuitOpenResponse(CircuitBreakerException $e): HttpResponse
{
$body = json_encode([
'error' => true,
'code' => 503,
'message' => 'Service temporarily unavailable. Please try again later.',
'service' => $e->service,
'retryAfter' => $e->retryAfterSeconds,
'timestamp' => date('c'),
], JSON_PRETTY_PRINT);
return new HttpResponse(
status: Status::SERVICE_UNAVAILABLE,
headers: new Headers(
[
'Content-Type' => 'application/json; charset=utf-8',
'Retry-After' => (string) $e->retryAfterSeconds,
'X-Circuit-Breaker' => 'open',
'X-Service' => $e->service,
],
),
body: $body
);
}
/**
* Factory method für Standard-API-Schutz
*/
public static function forApi(CircuitBreaker $circuitBreaker): self
{
return new self($circuitBreaker, [
'api' => CircuitBreakerConfig::forExternalService(
failureThreshold: 10,
recoveryTimeout: Duration::fromSeconds(30)
),
]);
}
/**
* Factory method für Admin-Bereich-Schutz
*/
public static function forAdmin(CircuitBreaker $circuitBreaker): self
{
return new self($circuitBreaker, [
'admin' => new CircuitBreakerConfig(
failureThreshold: 5,
recoveryTimeout: Duration::fromSeconds(60)
),
]);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker;
/**
* Circuit Breaker States nach dem Circuit Breaker Pattern
*
* CLOSED: Normal operation, requests allowed
* OPEN: Failures exceeded threshold, requests blocked
* HALF_OPEN: Testing if service recovered
*/
enum CircuitState: string
{
case CLOSED = 'closed';
case OPEN = 'open';
case HALF_OPEN = 'half_open';
}

View File

@@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\Commands;
use App\Framework\CircuitBreaker\CircuitBreakerManager;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ExitCode;
/**
* Console Command für Circuit Breaker Management
*/
final readonly class CircuitBreakerCommand
{
public function __construct(
private CircuitBreakerManager $circuitBreakerManager
) {
}
#[ConsoleCommand(
name: 'circuit-breaker',
description: 'Manage circuit breakers - status, reset, health checks'
)]
public function execute(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$args = $input->getArguments();
$command = $args[0] ?? 'status';
return match ($command) {
'status' => $this->handleStatus($input, $output),
'stats' => $this->handleStats($input, $output),
'health' => $this->handleHealth($input, $output),
'reset' => $this->handleReset($input, $output),
'config' => $this->handleConfig($input, $output),
'export' => $this->handleExport($input, $output),
default => $this->handleStatus($input, $output),
};
}
private function handleStatus(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$args = $input->getArguments();
$specificService = $args[1] ?? null;
$allStatus = $this->circuitBreakerManager->getAllServicesStatus();
if (empty($allStatus)) {
$output->writeLine('<info>No circuit breakers configured</info>');
return ExitCode::SUCCESS;
}
$output->writeLine('<info>Circuit Breaker Status</info>');
$output->writeLine('');
foreach ($allStatus as $namespace => $services) {
if (empty($services)) {
continue;
}
$output->writeLine("<comment>Namespace: {$namespace}</comment>");
$output->writeLine(str_repeat('-', 50));
foreach ($services as $service => $status) {
if ($specificService && $service !== $specificService) {
continue;
}
$stateColor = match ($status['state']) {
'closed' => 'info',
'half_open' => 'warning',
'open' => 'error',
default => 'comment',
};
$output->writeLine(sprintf(
" <{$stateColor}>%s</{$stateColor}> - State: <{$stateColor}>%s</{$stateColor}> | Failures: %d | Successes: %d",
$service,
strtoupper($status['state']),
$status['failure_count'],
$status['success_count']
));
if ($status['last_failure_time']) {
$lastFailure = date('Y-m-d H:i:s', $status['last_failure_time']);
$output->writeLine(" Last failure: {$lastFailure}");
}
if ($status['configuration']) {
$config = $status['configuration'];
$output->writeLine(sprintf(
" Config: Threshold=%d, Timeout=%ds, HalfOpen=%d, Success=%d",
$config['failure_threshold'],
$config['recovery_timeout_seconds'],
$config['half_open_max_attempts'],
$config['success_threshold']
));
}
$output->writeLine('');
}
}
return ExitCode::SUCCESS;
}
private function handleStats(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$stats = $this->circuitBreakerManager->getGlobalStatistics();
$output->writeLine('<info>Global Circuit Breaker Statistics</info>');
$output->writeLine('');
$output->writeLine(sprintf('Total Services: <comment>%d</comment>', $stats['total_services']));
$output->writeLine(sprintf('Healthy Services: <info>%d</info>', $stats['healthy_services']));
$output->writeLine(sprintf('Degraded Services: <warning>%d</warning>', $stats['degraded_services']));
$output->writeLine(sprintf('Failed Services: <error>%d</error>', $stats['failed_services']));
$output->writeLine('');
$output->writeLine('Circuit Breaker States:');
$output->writeLine(sprintf(' Closed: <info>%d</info>', $stats['circuit_breakers_closed']));
$output->writeLine(sprintf(' Half-Open: <warning>%d</warning>', $stats['circuit_breakers_half_open']));
$output->writeLine(sprintf(' Open: <error>%d</error>', $stats['circuit_breakers_open']));
return ExitCode::SUCCESS;
}
private function handleHealth(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$output->writeLine('<info>Performing health checks...</info>');
$output->writeLine('');
$healthResults = $this->circuitBreakerManager->performHealthChecks();
foreach ($healthResults as $namespace => $services) {
if (empty($services)) {
continue;
}
$output->writeLine("<comment>Namespace: {$namespace}</comment>");
foreach ($services as $service => $result) {
$statusColor = match ($result['status']) {
'healthy' => 'info',
'degraded' => 'warning',
'failed' => 'error',
default => 'comment',
};
$output->writeLine(sprintf(
" <{$statusColor}>%s</{$statusColor}> - %s (checked at %s)",
$service,
ucfirst($result['status']),
$result['checked_at']
));
}
$output->writeLine('');
}
return ExitCode::SUCCESS;
}
private function handleReset(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$args = $input->getArguments();
$service = $args[1] ?? null;
if ($service) {
$namespace = $args[2] ?? 'default';
$this->circuitBreakerManager->resetService($service, $namespace);
$output->writeLine("<info>Circuit breaker for service '{$service}' in namespace '{$namespace}' has been reset</info>");
} else {
$confirm = $input->getOption('force') || $this->confirmReset($input, $output);
if ($confirm) {
$this->circuitBreakerManager->resetAll();
$output->writeLine('<info>All circuit breakers have been reset</info>');
} else {
$output->writeLine('<comment>Reset cancelled</comment>');
return ExitCode::FAILURE;
}
}
return ExitCode::SUCCESS;
}
private function handleConfig(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$config = $this->circuitBreakerManager->exportConfiguration();
$output->writeLine('<info>Circuit Breaker Configuration</info>');
$output->writeLine('');
$output->writeLine(sprintf('Circuit Breakers: %s', implode(', ', $config['circuit_breakers'])));
$output->writeLine(sprintf('Generated: %s', $config['timestamp']));
$output->writeLine(sprintf('Version: %s', $config['version']));
$output->writeLine('');
if (! empty($config['configurations'])) {
$output->writeLine('<comment>Service Configurations:</comment>');
foreach ($config['configurations'] as $service => $serviceConfig) {
$output->writeLine(" {$service}:");
$output->writeLine(" Failure Threshold: {$serviceConfig->failureThreshold}");
$output->writeLine(" Recovery Timeout: {$serviceConfig->recoveryTimeoutSeconds}s");
$output->writeLine(" Half-Open Max Attempts: {$serviceConfig->halfOpenMaxAttempts}");
$output->writeLine(" Success Threshold: {$serviceConfig->successThreshold}");
$output->writeLine('');
}
}
return ExitCode::SUCCESS;
}
private function handleExport(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$config = $this->circuitBreakerManager->exportConfiguration();
$json = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$filename = $input->getOption('output') ?? 'circuit-breaker-config.json';
if (file_put_contents($filename, $json) !== false) {
$output->writeLine("<info>Configuration exported to {$filename}</info>");
return ExitCode::SUCCESS;
} else {
$output->writeLine("<error>Failed to write configuration to {$filename}</error>");
return ExitCode::FAILURE;
}
}
private function confirmReset(ConsoleInput $input, ConsoleOutput $output): bool
{
$output->write('<warning>This will reset ALL circuit breakers. Are you sure? (y/N): </warning>');
$handle = fopen('php://stdin', 'r');
$response = trim(fgets($handle));
fclose($handle);
return strtolower($response) === 'y' || strtolower($response) === 'yes';
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Exception\DatabaseException;
use PDOException;
use Throwable;
/**
* Circuit Breaker speziell für Datenbankverbindungen
*/
final readonly class DatabaseCircuitBreaker
{
private const string SERVICE_NAME = 'database';
public function __construct(
private CircuitBreaker $circuitBreaker,
private ?CircuitBreakerConfig $config = null
) {
}
/**
* Führt eine Datenbankoperation mit Circuit Breaker Schutz aus
*
* @template T
* @param callable(ConnectionInterface): T $operation
* @return T
* @throws CircuitBreakerException|DatabaseException|Throwable
*/
public function execute(ConnectionInterface $connection, callable $operation): mixed
{
$config = $this->config ?? $this->getDefaultDatabaseConfig();
return $this->circuitBreaker->execute(
service: self::SERVICE_NAME,
operation: function () use ($connection, $operation) {
try {
return $operation($connection);
} catch (PDOException $e) {
// PDO Exceptions in DatabaseException wrappen für einheitliche Behandlung
throw new DatabaseException(
"Database operation failed: " . $e->getMessage(),
previous: $e
);
}
},
config: $config
);
}
/**
* Prüft die Datenbankverbindung
*/
public function healthCheck(ConnectionInterface $connection): bool
{
try {
$this->execute($connection, function ($conn) {
// Einfache Query für Health Check
$conn->query('SELECT 1');
return true;
});
return true;
} catch (Throwable) {
return false;
}
}
/**
* Gibt den aktuellen Status des Database Circuit Breakers zurück
*/
public function getStatus(): array
{
$metrics = $this->circuitBreaker->getMetrics(self::SERVICE_NAME);
return [
'service' => self::SERVICE_NAME,
'state' => $metrics['state'],
'failure_count' => $metrics['failure_count'],
'success_count' => $metrics['success_count'],
'last_failure_time' => $metrics['last_failure_time'],
'health_status' => $metrics['state'] === 'closed' ? 'healthy' : 'degraded',
];
}
/**
* Setzt den Database Circuit Breaker zurück
*/
public function reset(): void
{
$this->circuitBreaker->reset(self::SERVICE_NAME);
}
/**
* Standard-Konfiguration für Datenbank Circuit Breaker
*/
private function getDefaultDatabaseConfig(): CircuitBreakerConfig
{
return new CircuitBreakerConfig(
failureThreshold: 3,
recoveryTimeout: Duration::fromSeconds(30),
halfOpenMaxAttempts: 2,
successThreshold: 2
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\Events;
use App\Framework\CircuitBreaker\CircuitBreakerMetrics;
use App\Framework\CircuitBreaker\CircuitState;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Event fired when a circuit breaker closes (transitions to CLOSED state)
*/
final readonly class CircuitBreakerClosed implements CircuitBreakerEvent
{
public string $eventType;
public string $severity;
public CircuitState $newState;
public Timestamp $occurredAt;
public function __construct(
public string $service,
public string $namespace,
public CircuitState $previousState,
public CircuitBreakerMetrics $metrics,
public ?string $reason = null,
?Timestamp $occurredAt = null,
) {
$this->eventType = 'circuit_breaker_closed';
$this->severity = 'info';
$this->newState = CircuitState::CLOSED;
$this->occurredAt = $occurredAt ?? Timestamp::now();
}
public function getDescription(): string
{
$successes = $this->metrics->successCount;
$reason = $this->reason ? " (Reason: {$this->reason})" : '';
return "Circuit breaker for '{$this->service}' closed after {$successes} successful attempts{$reason}";
}
public function toArray(): array
{
return [
'event_type' => $this->eventType,
'service' => $this->service,
'namespace' => $this->namespace,
'previous_state' => $this->previousState->value,
'new_state' => $this->newState->value,
'reason' => $this->reason,
'occurred_at' => $this->occurredAt->toIsoString(),
'severity' => $this->severity,
'description' => $this->getDescription(),
'metrics' => $this->metrics->toArray(),
];
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\Events;
use App\Framework\CircuitBreaker\CircuitBreakerMetrics;
use App\Framework\CircuitBreaker\CircuitState;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Interface for all Circuit Breaker events
*/
interface CircuitBreakerEvent
{
public string $service {get;}
public string $namespace {get;}
public CircuitState $previousState {get;}
public CircuitState $newState {get;}
public CircuitBreakerMetrics $metrics {get;}
public Timestamp $occurredAt {get;}
public ?string $reason {get;}
public string $eventType {get;}
public string $severity {get;}
public function getDescription(): string;
public function toArray(): array;
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\Events;
use App\Framework\EventBus\EventBus;
use App\Framework\Logging\Logger;
/**
* Publishes Circuit Breaker events to the event bus and logs them
*/
final readonly class CircuitBreakerEventPublisher
{
public function __construct(
private EventBus $eventBus,
private ?Logger $logger = null,
) {
}
/**
* Publish a circuit breaker event
*/
public function publish(CircuitBreakerEvent $event): void
{
// Publish to event bus for other components to handle
$this->eventBus->dispatch($event);
// Log the event
$this->logEvent($event);
}
private function logEvent(CircuitBreakerEvent $event): void
{
if ($this->logger === null) {
return;
}
$message = $event->getDescription();
$context = $event->toArray();
match ($event->severity) {
'critical' => $this->logger->critical($message, $context),
'error' => $this->logger->error($message, $context),
'warning' => $this->logger->warning($message, $context),
'info' => $this->logger->info($message, $context),
'debug' => $this->logger->debug($message, $context),
default => $this->logger->notice($message, $context),
};
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\Events;
use App\Framework\CircuitBreaker\CircuitBreakerMetrics;
use App\Framework\CircuitBreaker\CircuitState;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Event fired when a circuit breaker transitions to HALF_OPEN state
*/
final readonly class CircuitBreakerHalfOpened implements CircuitBreakerEvent
{
public string $eventType;
public string $severity;
public CircuitState $newState;
public Timestamp $occurredAt;
public function __construct(
public string $service,
public string $namespace,
public CircuitState $previousState,
public CircuitBreakerMetrics $metrics,
public ?string $reason = null,
?Timestamp $occurredAt = null,
) {
$this->eventType = 'circuit_breaker_half_opened';
$this->severity = 'info';
$this->newState = CircuitState::HALF_OPEN;
$this->occurredAt = $occurredAt ?? Timestamp::now();
}
public function getDescription(): string
{
$uptime = $this->metrics->getUptimeDuration()?->toHumanReadable() ?? 'unknown';
$reason = $this->reason ? " (Reason: {$this->reason})" : '';
return "Circuit breaker for '{$this->service}' transitioned to HALF_OPEN after {$uptime} recovery time{$reason}";
}
public function toArray(): array
{
return [
'event_type' => $this->eventType,
'service' => $this->service,
'namespace' => $this->namespace,
'previous_state' => $this->previousState->value,
'new_state' => $this->newState->value,
'reason' => $this->reason,
'occurred_at' => $this->occurredAt->toIsoString(),
'severity' => $this->severity,
'description' => $this->getDescription(),
'metrics' => $this->metrics->toArray(),
];
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\Events;
use App\Framework\CircuitBreaker\CircuitBreakerMetrics;
use App\Framework\CircuitBreaker\CircuitState;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Event fired when a circuit breaker opens (transitions to OPEN state)
*/
final readonly class CircuitBreakerOpened implements CircuitBreakerEvent
{
public string $eventType;
public string $severity;
public CircuitState $newState;
public Timestamp $occurredAt;
public function __construct(
public string $service,
public string $namespace,
public CircuitState $previousState,
public CircuitBreakerMetrics $metrics,
public ?string $reason = null,
?Timestamp $occurredAt = null,
) {
$this->eventType = 'circuit_breaker_opened';
$this->severity = 'warning';
$this->newState = CircuitState::OPEN;
$this->occurredAt = $occurredAt ?? Timestamp::now();
}
public function getDescription(): string
{
$failures = $this->metrics->failureCount;
$reason = $this->reason ? " (Reason: {$this->reason})" : '';
return "Circuit breaker for '{$this->service}' opened after {$failures} failures{$reason}";
}
public function toArray(): array
{
return [
'event_type' => $this->eventType,
'service' => $this->service,
'namespace' => $this->namespace,
'previous_state' => $this->previousState->value,
'new_state' => $this->newState->value,
'reason' => $this->reason,
'occurred_at' => $this->occurredAt->toIsoString(),
'severity' => $this->severity,
'description' => $this->getDescription(),
'metrics' => $this->metrics->toArray(),
];
}
}

View File

@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\Examples;
use App\Framework\CircuitBreaker\CircuitBreakerConfig;
use App\Framework\CircuitBreaker\FailurePredicate\FailurePredicateFactory;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\HttpClient\Exception\ServerErrorException;
use App\Framework\Validation\Exceptions\ValidationException;
/**
* Usage examples for the new FailurePredicate system
*/
final class FailurePredicateUsageExample
{
/**
* Example 1: Using factory methods for common scenarios
*/
public function exampleFactoryUsage(): void
{
// For external services - triggers on server errors, excludes validation errors
$externalServiceConfig = CircuitBreakerConfig::forExternalService();
// For database operations - only triggers on database-related exceptions
$databaseConfig = CircuitBreakerConfig::forDatabase();
// For HTTP clients - comprehensive HTTP error handling
$httpClientConfig = CircuitBreakerConfig::forHttpClient();
// Only timeout errors
$timeoutConfig = CircuitBreakerConfig::forTimeoutErrors();
// Critical errors only (500+ status codes, framework errors)
$criticalConfig = CircuitBreakerConfig::strict();
}
/**
* Example 2: Using the fluent builder for custom predicates
*/
public function exampleBuilderUsage(): void
{
// Complex predicate: Include server errors OR specific exceptions, exclude validation
$customPredicate = FailurePredicateFactory::builder()
->includeServerErrors() // 5xx HTTP status codes
->includeExceptions([
ServerErrorException::class,
\PDOException::class,
])
->excludeExceptions([
ValidationException::class,
\InvalidArgumentException::class,
])
->includeStatusCodes([429, 503, 504]) // Rate limiting and gateway errors
->addCallback(
fn (\Throwable $e, string $service) => str_contains($e->getMessage(), 'timeout'),
'Timeout detection'
)
->or() // Use OR logic instead of AND
->build();
$config = CircuitBreakerConfig::withFailurePredicate(
failurePredicate: $customPredicate,
failureThreshold: 3,
recoveryTimeout: Duration::fromMinutes(2)
);
}
/**
* Example 3: Service-specific predicates
*/
public function exampleServiceSpecificPredicates(): void
{
// Different predicates for different services
$paymentServicePredicate = FailurePredicateFactory::builder()
->includeServerErrors()
->includeStatusCodes([429, 502, 503, 504])
->excludeExceptions([ValidationException::class])
->and()
->build();
$emailServicePredicate = FailurePredicateFactory::builder()
->includeExceptions([
\App\Framework\HttpClient\Exception\CurlExecutionFailed::class,
ServerErrorException::class,
])
->addCallback(
fn (\Throwable $e, string $service) => $service === 'email-service' &&
str_contains($e->getMessage(), 'rate limit'),
'Email service rate limiting'
)
->or()
->build();
$paymentConfig = CircuitBreakerConfig::withFailurePredicate($paymentServicePredicate);
$emailConfig = CircuitBreakerConfig::withFailurePredicate($emailServicePredicate);
}
/**
* Example 4: Advanced combinations with logical operators
*/
public function exampleAdvancedCombinations(): void
{
// Predicate that triggers when:
// (Server errors OR specific exceptions) AND NOT validation errors
$complexPredicate = FailurePredicateFactory::builder()
->includeServerErrors()
->includeExceptions([
ServerErrorException::class,
\RuntimeException::class,
])
->and() // AND logic for above conditions
->build();
// Combine with NOT predicate for validation errors
$validationExclusion = FailurePredicateFactory::builder()
->includeExceptions([
ValidationException::class,
\InvalidArgumentException::class,
])
->build();
// This creates: (ServerErrors OR RuntimeException) AND NOT ValidationErrors
$finalPredicate = FailurePredicateFactory::builder()
->addPredicate($complexPredicate)
->addPredicate(\App\Framework\CircuitBreaker\FailurePredicate\CompositeFailurePredicate::not([$validationExclusion]))
->and()
->build();
$config = CircuitBreakerConfig::withFailurePredicate($finalPredicate);
}
/**
* Example 5: Message-based predicates
*/
public function exampleMessageBasedPredicates(): void
{
// Trigger only on timeout-related messages
$timeoutPredicate = FailurePredicateFactory::builder()
->addCallback(
\App\Framework\CircuitBreaker\FailurePredicate\CallbackFailurePredicate::messageContains('timeout')
->shouldTrigger(...),
'Timeout message detection'
)
->addCallback(
\App\Framework\CircuitBreaker\FailurePredicate\CallbackFailurePredicate::messageMatches('/connection.*failed/i')
->shouldTrigger(...),
'Connection failure pattern'
)
->or()
->build();
$config = CircuitBreakerConfig::withFailurePredicate($timeoutPredicate);
}
/**
* Example 6: Testing and debugging predicates
*/
public function exampleTestingPredicates(): void
{
// Never trigger (useful for testing)
$neverTrigger = FailurePredicateFactory::never();
// Always trigger (useful for testing)
$alwaysTrigger = FailurePredicateFactory::always();
// Custom test predicate
$testPredicate = FailurePredicateFactory::builder()
->addCallback(
fn (\Throwable $e, string $service) =>
$service === 'test-service' && get_class($e) === \RuntimeException::class,
'Test-specific predicate'
)
->build();
$testConfig = CircuitBreakerConfig::withFailurePredicate($testPredicate);
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\FailurePredicate;
use Throwable;
/**
* Predicate that uses a callback function for custom logic
*/
final readonly class CallbackFailurePredicate implements FailurePredicate
{
/**
* @param callable(Throwable, string): bool $callback
*/
public function __construct(
private mixed $callback,
private string $identifier = 'callback',
private string $description = 'Custom callback predicate',
) {
if (! is_callable($this->callback)) {
throw new \InvalidArgumentException('Callback must be callable');
}
}
public function shouldTrigger(Throwable $exception, string $service): bool
{
try {
return (bool) ($this->callback)($exception, $service);
} catch (Throwable $e) {
// If callback throws, don't trigger circuit breaker
return false;
}
}
public function getIdentifier(): string
{
return $this->identifier;
}
public function getDescription(): string
{
return $this->description;
}
/**
* Create predicate from closure
*/
public static function from(callable $callback, string $description = 'Custom callback'): self
{
return new self($callback, 'callback', $description);
}
/**
* Create predicate that checks exception message
*/
public static function messageContains(string $substring): self
{
return new self(
callback: fn (Throwable $e, string $service) => str_contains($e->getMessage(), $substring),
identifier: 'message_contains',
description: "Message contains: '{$substring}'"
);
}
/**
* Create predicate that checks exception message with regex
*/
public static function messageMatches(string $pattern): self
{
return new self(
callback: fn (Throwable $e, string $service) => preg_match($pattern, $e->getMessage()) === 1,
identifier: 'message_regex',
description: "Message matches: {$pattern}"
);
}
/**
* Create predicate based on service name
*/
public static function forService(string $targetService): self
{
return new self(
callback: fn (Throwable $e, string $service) => $service === $targetService,
identifier: 'service_specific',
description: "Only for service: {$targetService}"
);
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\FailurePredicate;
use Throwable;
/**
* Composite predicate that combines multiple predicates with logical operators
*/
final readonly class CompositeFailurePredicate implements FailurePredicate
{
public function __construct(
/**
* @var array<FailurePredicate>
*/
private array $predicates,
private LogicalOperator $operator = LogicalOperator::AND,
) {
}
public function shouldTrigger(Throwable $exception, string $service): bool
{
if (empty($this->predicates)) {
return false;
}
return match ($this->operator) {
LogicalOperator::AND => $this->evaluateAnd($exception, $service),
LogicalOperator::OR => $this->evaluateOr($exception, $service),
LogicalOperator::NOT => $this->evaluateNot($exception, $service),
};
}
public function getIdentifier(): string
{
$identifiers = array_map(fn (FailurePredicate $p) => $p->getIdentifier(), $this->predicates);
return 'composite(' . $this->operator->value . ':' . implode(',', $identifiers) . ')';
}
public function getDescription(): string
{
$descriptions = array_map(fn (FailurePredicate $p) => $p->getDescription(), $this->predicates);
return match ($this->operator) {
LogicalOperator::AND => '(' . implode(' AND ', $descriptions) . ')',
LogicalOperator::OR => '(' . implode(' OR ', $descriptions) . ')',
LogicalOperator::NOT => 'NOT (' . implode(', ', $descriptions) . ')',
};
}
/**
* Create AND composite predicate
*
* @param array<FailurePredicate> $predicates
*/
public static function and(array $predicates): self
{
return new self($predicates, LogicalOperator::AND);
}
/**
* Create OR composite predicate
*
* @param array<FailurePredicate> $predicates
*/
public static function or(array $predicates): self
{
return new self($predicates, LogicalOperator::OR);
}
/**
* Create NOT composite predicate
*
* @param array<FailurePredicate> $predicates
*/
public static function not(array $predicates): self
{
return new self($predicates, LogicalOperator::NOT);
}
private function evaluateAnd(Throwable $exception, string $service): bool
{
foreach ($this->predicates as $predicate) {
if (! $predicate->shouldTrigger($exception, $service)) {
return false;
}
}
return true;
}
private function evaluateOr(Throwable $exception, string $service): bool
{
foreach ($this->predicates as $predicate) {
if ($predicate->shouldTrigger($exception, $service)) {
return true;
}
}
return false;
}
private function evaluateNot(Throwable $exception, string $service): bool
{
foreach ($this->predicates as $predicate) {
if ($predicate->shouldTrigger($exception, $service)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\FailurePredicate;
use Throwable;
/**
* Predicate that matches specific exception types
*/
final readonly class ExceptionTypeFailurePredicate implements FailurePredicate
{
/**
* @param array<class-string<Throwable>> $includedExceptions
* @param array<class-string<Throwable>> $excludedExceptions
*/
public function __construct(
private array $includedExceptions = [],
private array $excludedExceptions = [],
private bool $includeAllByDefault = true,
) {
}
public function shouldTrigger(Throwable $exception, string $service): bool
{
// Check excluded exceptions first
foreach ($this->excludedExceptions as $excludedClass) {
if ($exception instanceof $excludedClass) {
return false;
}
}
// If no included exceptions specified, use default behavior
if (empty($this->includedExceptions)) {
return $this->includeAllByDefault;
}
// Check if exception matches included types
foreach ($this->includedExceptions as $includedClass) {
if ($exception instanceof $includedClass) {
return true;
}
}
return false;
}
public function getIdentifier(): string
{
return 'exception_type';
}
public function getDescription(): string
{
$included = empty($this->includedExceptions)
? ($this->includeAllByDefault ? 'all' : 'none')
: implode(', ', array_map(fn ($class) => class_basename($class), $this->includedExceptions));
$excluded = empty($this->excludedExceptions)
? 'none'
: implode(', ', array_map(fn ($class) => class_basename($class), $this->excludedExceptions));
return "Include: {$included}, Exclude: {$excluded}";
}
/**
* Create predicate that only triggers on specific exception types
*
* @param array<class-string<Throwable>> $exceptionTypes
*/
public static function only(array $exceptionTypes): self
{
return new self(
includedExceptions: $exceptionTypes,
includeAllByDefault: false
);
}
/**
* Create predicate that triggers on all exceptions except specified types
*
* @param array<class-string<Throwable>> $exceptionTypes
*/
public static function except(array $exceptionTypes): self
{
return new self(
excludedExceptions: $exceptionTypes,
includeAllByDefault: true
);
}
/**
* Create predicate that triggers on all exceptions
*/
public static function all(): self
{
return new self(includeAllByDefault: true);
}
/**
* Create predicate that never triggers
*/
public static function none(): self
{
return new self(includeAllByDefault: false);
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\FailurePredicate;
use Throwable;
/**
* Interface for determining if an exception should trigger circuit breaker failure
*/
interface FailurePredicate
{
/**
* Determine if the exception should trigger a circuit breaker failure
*/
public function shouldTrigger(Throwable $exception, string $service): bool;
/**
* Get predicate identifier for logging/debugging
*/
public function getIdentifier(): string;
/**
* Get human-readable description
*/
public function getDescription(): string;
}

View File

@@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\FailurePredicate;
use Throwable;
/**
* Fluent builder for creating failure predicates
*/
final class FailurePredicateBuilder
{
/**
* @var array<class-string<Throwable>>
*/
private array $includedExceptions = [];
/**
* @var array<class-string<Throwable>>
*/
private array $excludedExceptions = [];
/**
* @var array<int>
*/
private array $includedStatusCodes = [];
/**
* @var array<int>
*/
private array $excludedStatusCodes = [];
/**
* @var array<FailurePredicate>
*/
private array $additionalPredicates = [];
private bool $includeServerErrors = false;
private bool $includeClientErrors = false;
private LogicalOperator $operator = LogicalOperator::AND;
/**
* Include specific exception types
*
* @param array<class-string<Throwable>> $exceptionTypes
*/
public function includeExceptions(array $exceptionTypes): self
{
$this->includedExceptions = array_merge($this->includedExceptions, $exceptionTypes);
return $this;
}
/**
* Exclude specific exception types
*
* @param array<class-string<Throwable>> $exceptionTypes
*/
public function excludeExceptions(array $exceptionTypes): self
{
$this->excludedExceptions = array_merge($this->excludedExceptions, $exceptionTypes);
return $this;
}
/**
* Include specific HTTP status codes
*
* @param array<int> $statusCodes
*/
public function includeStatusCodes(array $statusCodes): self
{
$this->includedStatusCodes = array_merge($this->includedStatusCodes, $statusCodes);
return $this;
}
/**
* Exclude specific HTTP status codes
*
* @param array<int> $statusCodes
*/
public function excludeStatusCodes(array $statusCodes): self
{
$this->excludedStatusCodes = array_merge($this->excludedStatusCodes, $statusCodes);
return $this;
}
/**
* Include server errors (5xx)
*/
public function includeServerErrors(): self
{
$this->includeServerErrors = true;
return $this;
}
/**
* Include client errors (4xx)
*/
public function includeClientErrors(): self
{
$this->includeClientErrors = true;
return $this;
}
/**
* Add custom predicate
*/
public function addPredicate(FailurePredicate $predicate): self
{
$this->additionalPredicates[] = $predicate;
return $this;
}
/**
* Add callback predicate
*/
public function addCallback(callable $callback, string $description = 'Custom callback'): self
{
$this->additionalPredicates[] = new CallbackFailurePredicate($callback, 'callback', $description);
return $this;
}
/**
* Set logical operator for combining predicates
*/
public function withOperator(LogicalOperator $operator): self
{
$this->operator = $operator;
return $this;
}
/**
* Use AND operator (default)
*/
public function and(): self
{
$this->operator = LogicalOperator::AND;
return $this;
}
/**
* Use OR operator
*/
public function or(): self
{
$this->operator = LogicalOperator::OR;
return $this;
}
/**
* Build the failure predicate
*/
public function build(): FailurePredicate
{
$predicates = [];
// Add exception type predicate if needed
if (! empty($this->includedExceptions) || ! empty($this->excludedExceptions)) {
$predicates[] = new ExceptionTypeFailurePredicate(
includedExceptions: $this->includedExceptions,
excludedExceptions: $this->excludedExceptions,
includeAllByDefault: empty($this->includedExceptions)
);
}
// Add HTTP status predicate if needed
if (! empty($this->includedStatusCodes) || ! empty($this->excludedStatusCodes) ||
$this->includeServerErrors || $this->includeClientErrors) {
$predicates[] = new HttpStatusFailurePredicate(
includedStatusCodes: $this->includedStatusCodes,
excludedStatusCodes: $this->excludedStatusCodes,
includeServerErrors: $this->includeServerErrors,
includeClientErrors: $this->includeClientErrors
);
}
// Add additional predicates
$predicates = array_merge($predicates, $this->additionalPredicates);
// Return appropriate predicate
if (empty($predicates)) {
return ExceptionTypeFailurePredicate::all();
}
if (count($predicates) === 1) {
return $predicates[0];
}
return new CompositeFailurePredicate($predicates, $this->operator);
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\FailurePredicate;
use App\Framework\HttpClient\Exception\ServerErrorException;
use App\Framework\Validation\Exceptions\ValidationException;
/**
* Factory for creating common failure predicates
*/
final class FailurePredicateFactory
{
/**
* Create predicate for external service calls
*/
public static function forExternalService(): FailurePredicate
{
return CompositeFailurePredicate::and([
HttpStatusFailurePredicate::serverErrors(),
ExceptionTypeFailurePredicate::except([ValidationException::class]),
]);
}
/**
* Create predicate for database operations
*/
public static function forDatabase(): FailurePredicate
{
return ExceptionTypeFailurePredicate::only([
\PDOException::class,
\App\Framework\Database\Exceptions\DatabaseException::class,
]);
}
/**
* Create predicate for HTTP client operations
*/
public static function forHttpClient(): FailurePredicate
{
return CompositeFailurePredicate::or([
ExceptionTypeFailurePredicate::only([
ServerErrorException::class,
\App\Framework\HttpClient\Exception\CurlExecutionFailed::class,
]),
HttpStatusFailurePredicate::forStatusCodes([502, 503, 504, 429]),
]);
}
/**
* Create predicate that excludes validation errors
*/
public static function excludeValidationErrors(): FailurePredicate
{
return ExceptionTypeFailurePredicate::except([
ValidationException::class,
\InvalidArgumentException::class,
]);
}
/**
* Create predicate for critical errors only
*/
public static function criticalErrorsOnly(): FailurePredicate
{
return CompositeFailurePredicate::or([
HttpStatusFailurePredicate::forStatusCodes([500, 502, 503, 504]),
ExceptionTypeFailurePredicate::only([
\Error::class,
\App\Framework\Exception\FrameworkException::class,
]),
]);
}
/**
* Create predicate for timeout-related errors
*/
public static function timeoutErrors(): FailurePredicate
{
return CompositeFailurePredicate::or([
CallbackFailurePredicate::messageContains('timeout'),
CallbackFailurePredicate::messageContains('timed out'),
HttpStatusFailurePredicate::forStatusCodes([408, 504]),
]);
}
/**
* Create predicate that never triggers (for testing/debugging)
*/
public static function never(): FailurePredicate
{
return ExceptionTypeFailurePredicate::none();
}
/**
* Create predicate that always triggers
*/
public static function always(): FailurePredicate
{
return ExceptionTypeFailurePredicate::all();
}
/**
* Create custom predicate with fluent builder
*/
public static function builder(): FailurePredicateBuilder
{
return new FailurePredicateBuilder();
}
}

View File

@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\FailurePredicate;
use App\Framework\HttpClient\Exception\ClientErrorException;
use App\Framework\HttpClient\Exception\ServerErrorException;
use Throwable;
/**
* Predicate that matches HTTP status codes
*/
final readonly class HttpStatusFailurePredicate implements FailurePredicate
{
/**
* @param array<int> $includedStatusCodes
* @param array<int> $excludedStatusCodes
*/
public function __construct(
private array $includedStatusCodes = [],
private array $excludedStatusCodes = [],
private bool $includeServerErrors = true,
private bool $includeClientErrors = false,
) {
}
public function shouldTrigger(Throwable $exception, string $service): bool
{
$statusCode = $this->extractStatusCode($exception);
if ($statusCode === null) {
// Not an HTTP exception, use default behavior
return false;
}
// Check excluded status codes first
if (in_array($statusCode, $this->excludedStatusCodes, true)) {
return false;
}
// Check included status codes
if (! empty($this->includedStatusCodes)) {
return in_array($statusCode, $this->includedStatusCodes, true);
}
// Default behavior based on status code ranges
if ($statusCode >= 500 && $statusCode < 600) {
return $this->includeServerErrors;
}
if ($statusCode >= 400 && $statusCode < 500) {
return $this->includeClientErrors;
}
return false;
}
public function getIdentifier(): string
{
return 'http_status';
}
public function getDescription(): string
{
$parts = [];
if (! empty($this->includedStatusCodes)) {
$parts[] = 'Include: ' . implode(', ', $this->includedStatusCodes);
} else {
$ranges = [];
if ($this->includeServerErrors) {
$ranges[] = '5xx';
}
if ($this->includeClientErrors) {
$ranges[] = '4xx';
}
if (! empty($ranges)) {
$parts[] = 'Include: ' . implode(', ', $ranges);
}
}
if (! empty($this->excludedStatusCodes)) {
$parts[] = 'Exclude: ' . implode(', ', $this->excludedStatusCodes);
}
return empty($parts) ? 'No HTTP status matching' : implode(', ', $parts);
}
/**
* Create predicate for specific status codes
*
* @param array<int> $statusCodes
*/
public static function forStatusCodes(array $statusCodes): self
{
return new self(includedStatusCodes: $statusCodes);
}
/**
* Create predicate for server errors (5xx)
*/
public static function serverErrors(): self
{
return new self(includeServerErrors: true, includeClientErrors: false);
}
/**
* Create predicate for client errors (4xx)
*/
public static function clientErrors(): self
{
return new self(includeServerErrors: false, includeClientErrors: true);
}
/**
* Create predicate for both client and server errors
*/
public static function allHttpErrors(): self
{
return new self(includeServerErrors: true, includeClientErrors: true);
}
/**
* Create predicate excluding specific status codes
*
* @param array<int> $statusCodes
*/
public static function except(array $statusCodes): self
{
return new self(
excludedStatusCodes: $statusCodes,
includeServerErrors: true,
includeClientErrors: true
);
}
private function extractStatusCode(Throwable $exception): ?int
{
if ($exception instanceof ServerErrorException || $exception instanceof ClientErrorException) {
return method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : null;
}
// Try to extract from exception message (fallback)
if (preg_match('/HTTP (\d{3})/', $exception->getMessage(), $matches)) {
return (int) $matches[1];
}
return null;
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\FailurePredicate;
/**
* Logical operators for composite predicates
*/
enum LogicalOperator: string
{
case AND = 'and';
case OR = 'or';
case NOT = 'not';
}

View File

@@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\HttpClient\ClientRequest;
use App\Framework\HttpClient\ClientResponse;
use App\Framework\HttpClient\Exception\ClientErrorException;
use App\Framework\HttpClient\Exception\HttpClientException;
use App\Framework\HttpClient\Exception\ServerErrorException;
use App\Framework\HttpClient\HttpClient;
use Throwable;
/**
* Circuit Breaker für HTTP Client Anfragen an externe Services
*/
final readonly class HttpClientCircuitBreaker
{
/**
* @var array<string, CircuitBreakerConfig>
*/
private array $serviceConfigs;
public function __construct(
private CircuitBreaker $circuitBreaker,
private HttpClient $httpClient,
array $serviceConfigs = []
) {
$this->serviceConfigs = $serviceConfigs;
}
/**
* Führt HTTP Request mit Circuit Breaker Schutz aus
*
* @throws CircuitBreakerException|HttpClientException
*/
public function request(ClientRequest $request, ?string $serviceName = null): ClientResponse
{
$serviceName ??= $this->extractServiceName($request);
$config = $this->serviceConfigs[$serviceName] ?? $this->getDefaultConfig();
return $this->circuitBreaker->execute(
service: $serviceName,
operation: function () use ($request) {
$response = $this->httpClient->request($request);
// 5xx Status Codes als Fehler behandeln
if ($response->getStatusCode() >= 500) {
throw new ServerErrorException(
"Server error: HTTP {$response->getStatusCode()}",
$response->getStatusCode()
);
}
// 4xx Status Codes normalerweise nicht als Circuit Breaker Fehler behandeln
// außer bei spezifischen Codes wie 429 (Rate Limit)
if ($response->getStatusCode() === 429) {
throw new ClientErrorException(
"Rate limit exceeded: HTTP 429",
429
);
}
return $response;
},
config: $config
);
}
/**
* Führt GET Request mit Circuit Breaker Schutz aus
*/
public function get(string $url, array $headers = [], ?string $serviceName = null): ClientResponse
{
$request = new ClientRequest('GET', $url, $headers);
return $this->request($request, $serviceName);
}
/**
* Führt POST Request mit Circuit Breaker Schutz aus
*/
public function post(string $url, mixed $body = null, array $headers = [], ?string $serviceName = null): ClientResponse
{
$request = new ClientRequest('POST', $url, $headers, $body);
return $this->request($request, $serviceName);
}
/**
* Führt PUT Request mit Circuit Breaker Schutz aus
*/
public function put(string $url, mixed $body = null, array $headers = [], ?string $serviceName = null): ClientResponse
{
$request = new ClientRequest('PUT', $url, $headers, $body);
return $this->request($request, $serviceName);
}
/**
* Führt DELETE Request mit Circuit Breaker Schutz aus
*/
public function delete(string $url, array $headers = [], ?string $serviceName = null): ClientResponse
{
$request = new ClientRequest('DELETE', $url, $headers);
return $this->request($request, $serviceName);
}
/**
* Gibt Status aller konfigurierten Services zurück
*/
public function getServicesStatus(): array
{
$status = [];
foreach (array_keys($this->serviceConfigs) as $serviceName) {
$metrics = $this->circuitBreaker->getMetrics($serviceName);
$status[$serviceName] = [
'state' => $metrics['state'],
'failure_count' => $metrics['failure_count'],
'success_count' => $metrics['success_count'],
'health_status' => $metrics['state'] === 'closed' ? 'healthy' : 'degraded',
'last_failure_time' => $metrics['last_failure_time'],
];
}
return $status;
}
/**
* Health Check für einen spezifischen Service
*/
public function healthCheck(string $serviceName, string $healthCheckUrl): bool
{
try {
$response = $this->get($healthCheckUrl, [], $serviceName);
return $response->getStatusCode() >= 200 && $response->getStatusCode() < 300;
} catch (Throwable) {
return false;
}
}
/**
* Setzt Circuit Breaker für einen Service zurück
*/
public function resetService(string $serviceName): void
{
$this->circuitBreaker->reset($serviceName);
}
/**
* Setzt alle Service Circuit Breaker zurück
*/
public function resetAll(): void
{
foreach (array_keys($this->serviceConfigs) as $serviceName) {
$this->circuitBreaker->reset($serviceName);
}
}
/**
* Extrahiert Service-Name aus der URL
*/
private function extractServiceName(ClientRequest $request): string
{
$url = $request->getUrl();
$parsedUrl = parse_url($url);
// Service-Name aus Host extrahieren
$host = $parsedUrl['host'] ?? 'unknown';
// Subdomain als Service-Name verwenden falls vorhanden
$parts = explode('.', $host);
if (count($parts) > 2) {
return $parts[0]; // z.B. "api" aus "api.example.com"
}
// Sonst den ganzen Host als Service-Name verwenden
return str_replace(['.', '-'], '_', $host);
}
/**
* Standard-Konfiguration für HTTP Services
*/
private function getDefaultConfig(): CircuitBreakerConfig
{
return new CircuitBreakerConfig(
failureThreshold: 5,
recoveryTimeout: Duration::fromSeconds(60),
halfOpenMaxAttempts: 3,
successThreshold: 2
);
}
/**
* Factory für spezifische Services
*/
public static function withServices(
CircuitBreaker $circuitBreaker,
HttpClient $httpClient,
array $services
): self {
$configs = [];
foreach ($services as $serviceName => $config) {
if (is_array($config)) {
$configs[$serviceName] = new CircuitBreakerConfig(...$config);
} elseif ($config instanceof CircuitBreakerConfig) {
$configs[$serviceName] = $config;
} else {
$configs[$serviceName] = CircuitBreakerConfig::forExternalService();
}
}
return new self($circuitBreaker, $httpClient, $configs);
}
}

View File

@@ -0,0 +1,211 @@
# Circuit Breaker Pattern Implementation
Die Circuit Breaker Implementierung schützt das System vor wiederholten Fehlern durch temporäres Blockieren von Requests nach einer bestimmten Anzahl von Fehlern.
## Komponenten
### CircuitState Enum
- `CLOSED`: Normal operation, alle Requests werden durchgelassen
- `OPEN`: Service ist als fehlerhaft markiert, alle Requests werden abgelehnt
- `HALF_OPEN`: Test-Phase, limitierte Requests werden durchgelassen
### CircuitBreaker (Hauptklasse)
Zentrale Circuit Breaker Implementierung mit:
- Automatische State-Übergänge basierend auf Fehlern/Erfolgen
- Konfigurierbare Schwellenwerte und Timeouts
- Cache-basierte Persistierung des Zustands
- Retry-Logic mit exponential backoff
### CircuitBreakerConfig
Konfiguration für Circuit Breaker:
```php
new CircuitBreakerConfig(
failureThreshold: 5, // Fehler bis Circuit öffnet
recoveryTimeoutSeconds: 60, // Zeit bis HALF_OPEN Test
halfOpenMaxAttempts: 3, // Max Versuche im HALF_OPEN
successThreshold: 3 // Erfolge bis Circuit schließt
);
```
### Spezialisierte Circuit Breaker
#### DatabaseCircuitBreaker
Schutz für Datenbankoperationen:
```php
$result = $databaseCircuitBreaker->execute($connection, function($conn) {
return $conn->query('SELECT * FROM users');
});
```
#### HttpClientCircuitBreaker
Schutz für externe HTTP-Services:
```php
$response = $httpCircuitBreaker->get('https://api.example.com/data', [], 'api_service');
```
### CircuitBreakerMiddleware
HTTP-Middleware für automatischen Schutz von Routes:
```php
// Automatischer API-Schutz
$middleware = CircuitBreakerMiddleware::forApi($circuitBreaker);
// Custom Service-Mapping
$middleware = new CircuitBreakerMiddleware($circuitBreaker, [
'payment_api' => CircuitBreakerConfig::strict(),
'notification_service' => CircuitBreakerConfig::forExternalService()
]);
```
### CircuitBreakerManager
Zentrale Verwaltung und Monitoring:
```php
$manager = CircuitBreakerManager::withDefaults($cache, $clock, $logger);
// Status aller Services
$status = $manager->getAllServicesStatus();
// Globale Statistiken
$stats = $manager->getGlobalStatistics();
// Health Checks
$health = $manager->performHealthChecks();
```
## Verwendung
### Grundlegende Verwendung
```php
$circuitBreaker = new CircuitBreaker($cache, $clock, $logger);
// Operation ausführen
try {
$result = $circuitBreaker->execute('my_service', function() {
// Potentiell fehlerhafte Operation
return $externalService->getData();
});
} catch (CircuitBreakerException $e) {
// Circuit ist offen, Service nicht verfügbar
return $fallbackData;
}
```
### Mit Konfiguration
```php
$config = new CircuitBreakerConfig(
failureThreshold: 3,
recoveryTimeoutSeconds: 30
);
$circuitBreaker->execute('critical_service', $operation, $config);
```
### Manuelle Überwachung
```php
// Status prüfen
$state = $circuitBreaker->getState('my_service');
// Metriken abrufen
$metrics = $circuitBreaker->getMetrics('my_service');
// Circuit zurücksetzen
$circuitBreaker->reset('my_service');
```
## Console Commands
### Status anzeigen
```bash
# Alle Services
php console.php circuit-breaker status
# Spezifischer Service
php console.php circuit-breaker status database
# Globale Statistiken
php console.php circuit-breaker stats
```
### Health Checks
```bash
php console.php circuit-breaker health
```
### Reset Circuit Breaker
```bash
# Spezifischer Service
php console.php circuit-breaker reset database
# Alle Services (mit Bestätigung)
php console.php circuit-breaker reset
# Alle Services (ohne Bestätigung)
php console.php circuit-breaker reset --force
```
### Konfiguration
```bash
# Aktuelle Konfiguration anzeigen
php console.php circuit-breaker config
# Konfiguration exportieren
php console.php circuit-breaker export
php console.php circuit-breaker export --output=my-config.json
```
## Vorkonfigurierte Service-Typen
### Datenbank Services
```php
$config = CircuitBreakerConfig::forDatabase(
failureThreshold: 3,
recoveryTimeoutSeconds: 30
);
```
### Externe Services
```php
$config = CircuitBreakerConfig::forExternalService(
failureThreshold: 5,
recoveryTimeoutSeconds: 300
);
```
### Kritische Services
```php
$config = CircuitBreakerConfig::strict(
failureThreshold: 3,
recoveryTimeoutSeconds: 600
);
```
## Error Codes
Circuit Breaker Exceptions verwenden dedizierte Error Codes:
- `SVC001`: Circuit Breaker ist offen
- `SVC002`: Circuit Breaker ist half-open
- `SVC003`: Service Health Check fehlgeschlagen
- `SVC004`: Service ist degradiert
## Integration mit Error Handling
Circuit Breaker Exceptions werden automatisch in das Framework Error Handling integriert:
- OWASP-konforme Logging
- Structured Error Context
- HTTP 503 Responses mit Retry-After Header
- Security Event Tracking bei kritischen Services
## Best Practices
1. **Service-spezifische Konfiguration**: Verschiedene Services benötigen unterschiedliche Schwellenwerte
2. **Monitoring**: Überwachen Sie Circuit Breaker Status und Metriken
3. **Fallback-Strategien**: Implementieren Sie Fallback-Logic für geöffnete Circuits
4. **Graceful Degradation**: Reduzieren Sie Funktionalität anstatt kompletten Ausfall
5. **Health Checks**: Implementieren Sie aktive Health Checks für bessere Recovery
## Performance
- Cache-basierte State-Persistierung für minimalen Overhead
- Efficient key-based service separation
- Configurable TTL für automatische Cleanup
- Batch operations für bulk resets
- Lock-free state transitions für high concurrency

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\Registry;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
/**
* Cache-based implementation of ServiceRegistry
*/
final class CacheBasedServiceRegistry implements ServiceRegistry
{
private const string REGISTRY_PREFIX = 'circuit_breaker_registry:';
private const string NAMESPACE_KEY = 'namespaces';
private const string SERVICES_SUFFIX = ':services';
private Duration $registryTtl;
public function __construct(
private readonly Cache $cache,
?Duration $registryTtl = null,
) {
$this->registryTtl = $registryTtl ?? Duration::fromHours(24);
}
public function discoverServices(string $namespace): array
{
$servicesKey = $this->getServicesKey($namespace);
$cacheItem = $this->cache->get($servicesKey);
if (! $cacheItem->isHit) {
return [];
}
$services = $cacheItem->value;
return is_array($services) ? $services : [];
}
public function registerService(string $service, string $namespace): void
{
// Register namespace
$this->registerNamespace($namespace);
// Add service to namespace
$servicesKey = $this->getServicesKey($namespace);
$services = $this->discoverServices($namespace);
if (! in_array($service, $services, true)) {
$services[] = $service;
$this->cache->set($servicesKey, $services, $this->registryTtl);
}
}
public function unregisterService(string $service, string $namespace): void
{
$servicesKey = $this->getServicesKey($namespace);
$services = $this->discoverServices($namespace);
$filteredServices = array_values(array_filter(
$services,
fn (string $s) => $s !== $service
));
if (empty($filteredServices)) {
$this->cache->forget($servicesKey);
$this->unregisterNamespaceIfEmpty($namespace);
} else {
$this->cache->set($servicesKey, $filteredServices, $this->registryTtl);
}
}
public function isServiceRegistered(string $service, string $namespace): bool
{
$services = $this->discoverServices($namespace);
return in_array($service, $services, true);
}
public function getNamespaces(): array
{
$namespacesKey = $this->getNamespacesKey();
$cacheItem = $this->cache->get($namespacesKey);
if (! $cacheItem->isHit) {
return [];
}
$namespaces = $cacheItem->value;
return is_array($namespaces) ? $namespaces : [];
}
public function getAllServices(): array
{
$result = [];
$namespaces = $this->getNamespaces();
foreach ($namespaces as $namespace) {
$services = $this->discoverServices($namespace);
$result[$namespace] = $services;
}
return $result;
}
public function clearNamespace(string $namespace): void
{
$servicesKey = $this->getServicesKey($namespace);
$this->cache->forget($servicesKey);
$this->unregisterNamespace($namespace);
}
public function clearAll(): void
{
$namespaces = $this->getNamespaces();
foreach ($namespaces as $namespace) {
$this->clearNamespace($namespace);
}
$namespacesKey = $this->getNamespacesKey();
$this->cache->forget($namespacesKey);
}
private function registerNamespace(string $namespace): void
{
$namespacesKey = $this->getNamespacesKey();
$namespaces = $this->getNamespaces();
if (! in_array($namespace, $namespaces, true)) {
$namespaces[] = $namespace;
$this->cache->set($namespacesKey, $namespaces, $this->registryTtl);
}
}
private function unregisterNamespace(string $namespace): void
{
$namespacesKey = $this->getNamespacesKey();
$namespaces = $this->getNamespaces();
$filteredNamespaces = array_values(array_filter(
$namespaces,
fn (string $ns) => $ns !== $namespace
));
if (empty($filteredNamespaces)) {
$this->cache->forget($namespacesKey);
} else {
$this->cache->set($namespacesKey, $filteredNamespaces, $this->registryTtl);
}
}
private function unregisterNamespaceIfEmpty(string $namespace): void
{
$services = $this->discoverServices($namespace);
if (empty($services)) {
$this->unregisterNamespace($namespace);
}
}
private function getNamespacesKey(): CacheKey
{
return CacheKey::fromString(self::REGISTRY_PREFIX . self::NAMESPACE_KEY);
}
private function getServicesKey(string $namespace): CacheKey
{
return CacheKey::fromString(self::REGISTRY_PREFIX . $namespace . self::SERVICES_SUFFIX);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\Registry;
/**
* Interface for discovering and managing services in circuit breaker namespaces
*/
interface ServiceRegistry
{
/**
* Discover all services in a given namespace
*/
public function discoverServices(string $namespace): array;
/**
* Register a service in a namespace
*/
public function registerService(string $service, string $namespace): void;
/**
* Unregister a service from a namespace
*/
public function unregisterService(string $service, string $namespace): void;
/**
* Check if a service is registered in a namespace
*/
public function isServiceRegistered(string $service, string $namespace): bool;
/**
* Get all registered namespaces
*/
public function getNamespaces(): array;
/**
* Get all services across all namespaces
*/
public function getAllServices(): array;
/**
* Clear all registered services in a namespace
*/
public function clearNamespace(string $namespace): void;
/**
* Clear all registered services
*/
public function clearAll(): void;
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace App\Framework\CircuitBreaker\SlidingWindow;
use App\Framework\CircuitBreaker\CircuitBreakerConfig;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\SlidingWindow\Aggregator\BooleanResult;
use App\Framework\SlidingWindow\SlidingWindow;
/**
* Circuit Breaker specific sliding window wrapper
*/
final readonly class CircuitBreakerSlidingWindow
{
public function __construct(
private SlidingWindow $slidingWindow,
) {
}
/**
* Record a successful operation
*/
public function recordSuccess(?Timestamp $timestamp = null): void
{
$timestamp ??= Timestamp::now();
$this->slidingWindow->record(true, $timestamp);
}
/**
* Record a failed operation
*/
public function recordFailure(?Timestamp $timestamp = null): void
{
$timestamp ??= Timestamp::now();
$this->slidingWindow->record(false, $timestamp);
}
/**
* Check if failure threshold is exceeded
*/
public function isFailureThresholdExceeded(CircuitBreakerConfig $config): bool
{
$stats = $this->slidingWindow->getStats();
$booleanData = $stats->getAggregatedValue('boolean');
if (! $booleanData instanceof BooleanResult) {
return false;
}
// Check minimum requests threshold
if (! $booleanData->hasMinimumRequests($config->successThreshold)) {
return false;
}
// Calculate failure threshold as percentage
$failureThreshold = $config->failureThreshold / 100.0; // Convert to 0.0-1.0 range
return $booleanData->isFailureRateExceeded($failureThreshold);
}
/**
* Get current failure rate
*/
public function getFailureRate(): float
{
$stats = $this->slidingWindow->getStats();
$booleanData = $stats->getAggregatedValue('boolean');
if (! $booleanData instanceof BooleanResult) {
return 0.0;
}
return $booleanData->failureRate;
}
/**
* Get success rate
*/
public function getSuccessRate(): float
{
$stats = $this->slidingWindow->getStats();
$booleanData = $stats->getAggregatedValue('boolean');
if (! $booleanData instanceof BooleanResult) {
return 0.0;
}
return $booleanData->successRate;
}
/**
* Get total request count in window
*/
public function getTotalRequests(): int
{
$stats = $this->slidingWindow->getStats();
return $stats->totalCount;
}
/**
* Clear the sliding window
*/
public function clear(): void
{
$this->slidingWindow->clear();
}
/**
* Get the underlying sliding window
*/
public function getSlidingWindow(): SlidingWindow
{
return $this->slidingWindow;
}
}