⚡ Circuit Breaker

Circuit Breaker chroni aplikację przed kaskadowymi awariami przez automatyczne blokowanie requestów do niestabilnych serwisów.

Jak działa?

Stany Circuit Breaker
CLOSED (normalny)
    ↓ (błędy przekraczają próg)
OPEN (blokuje requesty)
    ↓ (po timeout)
HALF-OPEN (testuje jeden request)
    ↓ sukces → CLOSED
    ↓ błąd → OPEN

Szybki start

import rip.nerd.kitsunenet.resilience.KNETCircuitBreaker

val circuitBreaker = KNETCircuitBreaker(
    failureThreshold = 5,      // 5 błędów otwiera circuit
    resetTimeoutMs = 30_000,   // 30s do half-open
    halfOpenRequests = 1       // 1 request w half-open
)

// Wykonaj z circuit breaker
val response = circuitBreaker.execute(client, request)

// Lub jako interceptor
val client = KNETClient.builder()
    .addInterceptor(circuitBreaker.asInterceptor())
    .build()

Konfiguracja

val circuitBreaker = KNETCircuitBreaker.builder()
    .failureThreshold(5)           // Liczba błędów do otwarcia
    .failureRateThreshold(0.5)     // Lub 50% error rate
    .slowCallThreshold(5_000)      // Request > 5s = slow
    .slowCallRateThreshold(0.8)    // 80% slow = open
    .minimumCalls(10)              // Min calls do oceny
    .resetTimeout(30_000)          // Czas w OPEN
    .halfOpenRequests(3)           // Requests w HALF-OPEN
    .recordExceptions(setOf(
        SocketTimeoutException::class,
        ConnectException::class,
        HttpException::class
    ))
    .ignoreExceptions(setOf(
        ClientException::class     // 4xx - nie liczą się
    ))
    .build()

Per-host Circuit Breaker

val circuitBreakerRegistry = KNETCircuitBreakerRegistry(
    defaultConfig = KNETCircuitBreaker.Config(
        failureThreshold = 5,
        resetTimeoutMs = 30_000
    )
)

// Automatyczny circuit breaker per host
val interceptor = KNETCircuitBreakerInterceptor(circuitBreakerRegistry)

val client = KNETClient.builder()
    .addInterceptor(interceptor)
    .build()

// Teraz każdy host ma własny circuit breaker:
// api.example.com - osobny
// cdn.example.com - osobny

Nasłuchiwanie eventów

circuitBreaker.onStateChange { oldState, newState ->
    Log.d("Circuit", "State: $oldState -> $newState")

    when (newState) {
        State.OPEN -> {
            showWarning("Serwis chwilowo niedostępny")
            analytics.log("circuit_opened")
        }
        State.CLOSED -> {
            hideWarning()
            analytics.log("circuit_closed")
        }
        State.HALF_OPEN -> {
            Log.d("Circuit", "Testing service...")
        }
    }
}

circuitBreaker.onError { request, error ->
    Log.w("Circuit", "Request failed: ${error.message}")
}

circuitBreaker.onSuccess { request, response ->
    Log.d("Circuit", "Request succeeded")
}

Fallback

val response = circuitBreaker.executeWithFallback(
    client = client,
    request = request,
    fallback = { error ->
        when (error) {
            is CircuitOpenException -> {
                // Zwróć cached data
                val cached = cache.get(request)
                cached ?: throw error
            }
            else -> {
                // Zwróć default response
                KNETResponse(
                    statusCode = 503,
                    statusMessage = "Service Unavailable",
                    headers = emptyMap(),
                    bodyString = """{"error": "Service temporarily unavailable"}"""
                )
            }
        }
    }
)

Manual control

// Sprawdź stan
val state = circuitBreaker.state
val isAvailable = circuitBreaker.isAvailable

// Metryki
val metrics = circuitBreaker.metrics
println("Failures: ${metrics.failureCount}")
println("Success: ${metrics.successCount}")
println("Error rate: ${metrics.errorRate}%")

// Reset manualny
circuitBreaker.reset()

// Wymuś otwarcie (np. maintenance)
circuitBreaker.forceOpen()

// Wymuś zamknięcie
circuitBreaker.forceClosed()

Integracja z Retry

// Circuit Breaker + Retry
val client = KNETClient.builder()
    // 1. Retry (wewnątrz circuit)
    .addInterceptor(KNETRetryInterceptor(maxRetries = 2))
    // 2. Circuit Breaker (na zewnątrz)
    .addInterceptor(circuitBreaker.asInterceptor())
    .build()

// Przepływ:
// Request → Circuit Breaker → Retry → HTTP
//                 ↑              ↓
//                 └──── fail ────┘

Przykład: Resilient API Client

class ResilientApiClient {

    private val circuitBreakers = KNETCircuitBreakerRegistry()

    private val client = KNETClient.builder()
        .addInterceptor(KNETLoggingInterceptor())
        .addInterceptor(KNETCircuitBreakerInterceptor(circuitBreakers))
        .addInterceptor(KNETRetryInterceptor(maxRetries = 2))
        .addInterceptor(KNETCacheInterceptor(cache))
        .build()

    suspend fun getData(url: String): Result<Data> {
        val circuitBreaker = circuitBreakers.get(URL(url).host)

        return try {
            if (!circuitBreaker.isAvailable) {
                // Circuit open - try cache
                val cached = cache.get(KNETRequest.get(url))
                if (cached != null) {
                    return Result.success(cached.json())
                }
                return Result.failure(CircuitOpenException())
            }

            val response = client.get(url)
            Result.success(response.json())

        } catch (e: CircuitOpenException) {
            // Service unavailable
            Result.failure(ServiceUnavailableException())
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    fun getCircuitState(host: String): State {
        return circuitBreakers.get(host).state
    }
}

📚 Zobacz też