⚡ 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 - requesty przechodzą)
    ↓ (błędy przekraczają próg)
OPEN (blokuje wszystkie requesty)
    ↓ (po resetTimeoutMs)
HALF_OPEN (testuje ograniczone requesty)
    ↓ sukces (successThreshold osiągnięty) → CLOSED
    ↓ błąd → OPEN

Jako interceptor

import rip.nerd.kitsunenet.interceptor.KNETCircuitBreakerInterceptor

// Prosty interceptor - jeden circuit dla wszystkich requestów
val interceptor = KNETCircuitBreakerInterceptor(
    failureThreshold = 5,    // Otwiera po 5 błędach
    successThreshold = 2,    // Zamyka po 2 sukcesach w HALF_OPEN
    timeoutMs = 60_000,      // Pozostaje OPEN przez 60s
    failureStatusCodes = setOf(500, 502, 503, 504)
)

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

// Sprawdź stan
interceptor.currentCircuitState()   // CLOSED, OPEN, HALF_OPEN
interceptor.reset()                 // Resetuj do CLOSED

Samodzielny (Per-klucz)

import rip.nerd.kitsunenet.util.KNETCircuitBreaker

// Circuit breaker z osobnymi obwodami per klucz (np. per host)
val circuitBreaker = KNETCircuitBreaker(
    failureThreshold = 5,
    successThreshold = 2,
    resetTimeoutMs = 30_000,
    halfOpenMaxRequests = 3
)

// Wykonaj przez circuit breaker
val result = circuitBreaker.execute("api.example.com") {
    client.get("https://api.example.com/users")
}

when (result) {
    is KNETCircuitBreaker.Result.Success -> obsluzOdpowiedz(result.value)
    is KNETCircuitBreaker.Result.Failure -> obsluzBlad(result.error)
    is KNETCircuitBreaker.Result.Rejected -> pokazSerwisNiedostepny()
}

// Bezpośrednie wykonanie requestu HTTP
val result = circuitBreaker.executeRequest(
    client = client,
    request = KNETRequest("https://api.example.com/users"),
    key = "api.example.com"  // Opcjonalnie: domyślnie hostname
)

Zarządzanie stanem

// Sprawdź stan
val state = circuitBreaker.getState("api.example.com")
println("Stan: $state")   // CLOSED, OPEN, HALF_OPEN

// Statystyki
val stats = circuitBreaker.getStats("api.example.com")
stats?.let {
    println("Błędy: ${it.failureCount}")
    println("Sukcesy: ${it.successCount}")
    println("Od ostatniego błędu: ${it.timeSinceLastFailure}ms")
}

// Wszystkie stany
circuitBreaker.getAllStates().forEach { (host, state) ->
    println("$host: $state")
}

// Ręczne sterowanie
circuitBreaker.forceOpen("api.example.com")
circuitBreaker.forceClose("api.example.com")
circuitBreaker.reset("api.example.com")
circuitBreaker.resetAll()

// Wbudowane konfiguracje
KNETCircuitBreaker.default      // failureThreshold=5, resetTimeout=30s
KNETCircuitBreaker.aggressive() // failureThreshold=3, resetTimeout=10s
KNETCircuitBreaker.tolerant()   // failureThreshold=10, resetTimeout=60s

Integracja z Retry

// Circuit Breaker + Retry (zalecana kolejność)
val client = KNETClient.builder()
    .addInterceptor(KNETCircuitBreakerInterceptor(failureThreshold = 5))  // 1. Circuit Breaker (zewnętrzny)
    .addInterceptor(KNETRetryInterceptor(maxRetries = 2))                 // 2. Retry (wewnętrzny)
    .build()

// Przepływ:
// Request → Circuit Breaker → Retry → HTTP
//                ↑               ↓ (przy błędzie)
//                └─── zliczane ──┘

Przykład: Odporny klient API

class ResilientApiClient(context: Context) {

    private val circuitBreaker = KNETCircuitBreaker(
        failureThreshold = 5,
        resetTimeoutMs = 30_000
    )

    private val cache = KNETResponseCache.longLived()

    private val client = KNETClient.builder()
        .addInterceptor(KNETLoggingInterceptor.basic())
        .addInterceptor(KNETRetryInterceptor.simple(maxRetries = 2))
        .addInterceptor(KNETCacheInterceptor.mediumLived())
        .build()

    suspend fun getData(url: String): Data? {
        val host = java.net.URL(url).host

        val result = circuitBreaker.execute(host) {
            client.get(url)
        }

        return when (result) {
            is KNETCircuitBreaker.Result.Success -> result.value.json()
            is KNETCircuitBreaker.Result.Rejected -> {
                // Circuit otwarty - spróbuj z cache
                cache.get(url)?.json()
            }
            is KNETCircuitBreaker.Result.Failure -> throw result.error
        }
    }
}

📚 Zobacz też