Files
michaelschiemer/src/Framework/ErrorBoundaries/CircuitBreaker/BoundaryCircuitBreakerManager.php
Michael Schiemer 55a330b223 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
2025-08-11 20:13:26 +02:00

204 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\ErrorBoundaries\CircuitBreaker;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\ErrorBoundaries\BoundaryConfig;
use App\Framework\Logging\Logger;
use App\Framework\StateManagement\StateManager;
/**
* Circuit breaker manager for error boundaries using generic state management
*/
final readonly class BoundaryCircuitBreakerManager
{
public function __construct(
/**
* @var StateManager<BoundaryCircuitBreakerState>
*/
private StateManager $stateManager,
private ?Logger $logger = null,
) {
}
/**
* Check if circuit is open for given boundary
*/
public function isCircuitOpen(string $boundaryName, BoundaryConfig $config): bool
{
$state = $this->getCircuitState($boundaryName);
if ($state->isOpen()) {
// Check if circuit should transition to half-open
if ($this->shouldTransitionToHalfOpen($state, $config)) {
$newState = $state->transitionToHalfOpen();
$this->stateManager->setState($boundaryName, $newState);
$this->log('info', "Circuit transitioned to HALF_OPEN for boundary: {$boundaryName}");
return false; // Allow operations in half-open state
}
return true;
}
return false;
}
/**
* Record a successful operation
*/
public function recordSuccess(string $boundaryName, BoundaryConfig $config): void
{
$newState = $this->stateManager->updateState(
$boundaryName,
function (?BoundaryCircuitBreakerState $currentState) use ($config, $boundaryName): BoundaryCircuitBreakerState {
$state = $currentState ?? new BoundaryCircuitBreakerState();
$newState = $state->recordSuccess();
// If in half-open state and enough successes, close the circuit
if ($state->isHalfOpen() && $newState->meetsSuccessThreshold($config->successThreshold ?? 3)) {
$newState = $newState->transitionToClosed();
$this->log('info', "Circuit transitioned to CLOSED for boundary: {$boundaryName}");
}
return $newState;
}
);
}
/**
* Record a failed operation
*/
public function recordFailure(string $boundaryName, BoundaryConfig $config): void
{
$newState = $this->stateManager->updateState(
$boundaryName,
function (?BoundaryCircuitBreakerState $currentState) use ($config, $boundaryName): BoundaryCircuitBreakerState {
$state = $currentState ?? new BoundaryCircuitBreakerState();
$newState = $state->recordFailure();
// Check if failure threshold is exceeded
if ($newState->exceedsFailureThreshold($config->circuitBreakerThreshold)) {
$newState = $newState->transitionToOpen();
$this->log('warning', "Circuit transitioned to OPEN for boundary: {$boundaryName} after {$config->circuitBreakerThreshold} failures");
}
// If in half-open state, any failure should open the circuit
if ($state->isHalfOpen()) {
$newState = $newState->transitionToOpen();
$this->log('warning', "Circuit transitioned to OPEN for boundary: {$boundaryName} due to failure in HALF_OPEN state");
}
return $newState;
}
);
}
/**
* Get current circuit state
*/
public function getCircuitState(string $boundaryName): BoundaryCircuitBreakerState
{
return $this->stateManager->getState($boundaryName) ?? new BoundaryCircuitBreakerState();
}
/**
* Reset circuit breaker state
*/
public function resetCircuit(string $boundaryName): void
{
$newState = new BoundaryCircuitBreakerState(); // Default closed state
$this->stateManager->setState($boundaryName, $newState);
$this->log('info', "Circuit reset for boundary: {$boundaryName}");
}
/**
* Get all circuit breaker states
*/
public function getAllCircuitStates(): array
{
$states = $this->stateManager->getAllStates();
$result = [];
foreach ($states as $boundaryName => $state) {
$result[$boundaryName] = $this->getCircuitHealth($boundaryName);
}
return $result;
}
/**
* Check circuit health for monitoring
*/
public function getCircuitHealth(string $boundaryName): array
{
$state = $this->getCircuitState($boundaryName);
return [
'boundary_name' => $boundaryName,
'state' => $state->state->value,
'state_description' => $state->state->getDescription(),
'failure_count' => $state->failureCount,
'success_count' => $state->successCount,
'last_failure_time' => $state->lastFailureTime?->toIsoString(),
'opened_at' => $state->openedAt?->toIsoString(),
'half_open_attempts' => $state->halfOpenAttempts,
'is_healthy' => $state->isClosed(),
'severity' => $state->state->getSeverity(),
];
}
/**
* Get state manager statistics
*/
public function getStatistics(): array
{
return $this->stateManager->getStatistics()->toArray();
}
/**
* Increment half-open attempts
*/
public function incrementHalfOpenAttempts(string $boundaryName): void
{
$this->stateManager->updateState(
$boundaryName,
function (?BoundaryCircuitBreakerState $currentState): BoundaryCircuitBreakerState {
$state = $currentState ?? new BoundaryCircuitBreakerState();
return $state->incrementHalfOpenAttempts();
}
);
}
private function shouldTransitionToHalfOpen(BoundaryCircuitBreakerState $state, BoundaryConfig $config): bool
{
if (! $state->isOpen() || $state->openedAt === null) {
return false;
}
$timeSinceOpened = Timestamp::now()->diff($state->openedAt);
return $timeSinceOpened->isGreaterThan($config->circuitBreakerTimeout);
}
private function log(string $level, string $message, array $context = []): void
{
if ($this->logger === null) {
return;
}
$context['component'] = 'BoundaryCircuitBreakerManager';
match ($level) {
'debug' => $this->logger->debug("[ErrorBoundary] {$message}", $context),
'info' => $this->logger->info("[ErrorBoundary] {$message}", $context),
'warning' => $this->logger->warning("[ErrorBoundary] {$message}", $context),
'error' => $this->logger->error("[ErrorBoundary] {$message}", $context),
default => $this->logger->info("[ErrorBoundary] {$message}", $context),
};
}
}