- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
222 lines
6.7 KiB
PHP
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);
|
|
}
|
|
}
|