Circuit Breaker chroni aplikację przed kaskadowymi awariami przez automatyczne blokowanie requestów do niestabilnych serwisów.
CLOSED (normalny)
↓ (błędy przekraczają próg)
OPEN (blokuje requesty)
↓ (po timeout)
HALF-OPEN (testuje jeden request)
↓ sukces → CLOSED
↓ błąd → OPEN
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()
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()
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
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")
}
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"}"""
)
}
}
}
)
// 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()
// 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 ────┘
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
}
}