Files
michaelschiemer/src/Framework/CircuitBreaker/HttpClientCircuitBreaker.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

222 lines
6.7 KiB
PHP

<?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);
}
}