🔄 Retry & Backoff

KNET oferuje różne strategie automatycznego ponawiania requestów przy błędach.

Simple Retry

import rip.nerd.kitsunenet.interceptor.KNETRetryInterceptor

val client = KNETClient.builder()
    .addInterceptor(KNETRetryInterceptor(
        maxRetries = 3,
        retryDelayMs = 1000
    ))
    .build()

// Request będzie ponawiany do 3 razy
// z 1s delay między próbami

Exponential Backoff

val client = KNETClient.builder()
    .addInterceptor(KNETExponentialBackoffInterceptor(
        maxRetries = 5,
        initialDelayMs = 1000,    // 1s
        maxDelayMs = 30_000,      // Max 30s
        multiplier = 2.0          // x2 każdy retry
    ))
    .build()

// Delays: 1s → 2s → 4s → 8s → 16s

Jitter (losowość)

val client = KNETClient.builder()
    .addInterceptor(KNETExponentialBackoffInterceptor(
        maxRetries = 5,
        initialDelayMs = 1000,
        maxDelayMs = 30_000,
        multiplier = 2.0,
        jitter = true,       // Dodaj losowość
        jitterFactor = 0.5   // ±50%
    ))
    .build()

// Delays z jitter: ~1s → ~2s → ~4s → ...
// Zapobiega "thundering herd" problem

Konfiguracja - co ponawiać

val interceptor = KNETRetryInterceptor(
    maxRetries = 3,
    retryDelayMs = 1000,

    // Statusy HTTP do retry
    retryOnStatusCodes = setOf(
        408,  // Request Timeout
        429,  // Too Many Requests
        500,  // Internal Server Error
        502,  // Bad Gateway
        503,  // Service Unavailable
        504   // Gateway Timeout
    ),

    // Wyjątki do retry
    retryOnExceptions = setOf(
        SocketTimeoutException::class,
        ConnectException::class,
        UnknownHostException::class,
        SSLException::class
    ),

    // Metody HTTP do retry (idempotentne)
    retryMethods = setOf("GET", "HEAD", "OPTIONS", "PUT", "DELETE"),

    // Nie ponawiaj POST (nie idempotentne)
    // chyba że request ma X-Idempotency-Key
    retryPostWithIdempotencyKey = true
)

Retry condition

val interceptor = KNETRetryInterceptor(
    maxRetries = 3,
    shouldRetry = { request, response, exception, attempt ->
        when {
            // Nie retry przy 4xx (client error)
            response?.statusCode in 400..499 -> false

            // Retry przy server errors
            response?.statusCode in 500..599 -> true

            // Retry przy network errors
            exception is IOException -> true

            // Nie retry przy 3+ próbie dla slow endpoints
            request.url.contains("/slow") && attempt >= 2 -> false

            else -> true
        }
    }
)

Retry callbacks

val interceptor = KNETRetryInterceptor(
    maxRetries = 3,
    onRetry = { request, attempt, delay, error ->
        Log.w("Retry", "Attempt $attempt for ${request.url}")
        Log.w("Retry", "Error: ${error.message}")
        Log.w("Retry", "Waiting ${delay}ms...")

        // Analytics
        analytics.log("api_retry", mapOf(
            "url" to request.url,
            "attempt" to attempt,
            "error" to error.javaClass.simpleName
        ))
    },
    onMaxRetriesExceeded = { request, error ->
        Log.e("Retry", "Max retries exceeded for ${request.url}")
        analytics.log("api_max_retries_exceeded")
    }
)

Custom delay strategy

val interceptor = KNETRetryInterceptor(
    maxRetries = 5,
    delayStrategy = { attempt, lastError, response ->
        when {
            // Rate limit - użyj Retry-After
            response?.statusCode == 429 -> {
                val retryAfter = response.headers["Retry-After"]
                    ?.toLongOrNull()?.times(1000)
                retryAfter ?: (attempt * 5000L)
            }

            // Timeout - szybki retry
            lastError is SocketTimeoutException -> {
                500L  // 0.5s
            }

            // Connection error - wolniejszy retry
            lastError is ConnectException -> {
                attempt * 2000L  // 2s, 4s, 6s...
            }

            // Default exponential
            else -> {
                minOf(1000L * (1 shl (attempt - 1)), 30_000L)
            }
        }
    }
)

Per-request retry

// Nadpisz config dla konkretnego requesta
val request = KNETRequest.get(url)
    .withRetry(maxRetries = 5)
    .withRetryDelay(2000)

// Lub wyłącz retry
val request = KNETRequest.post(url, data)
    .noRetry()

Przykład: Resilient fetch

class ApiService(private val client: KNETClient) {

    // Konfiguracja per-endpoint
    suspend fun getUsers(): List<User> {
        // Szybki retry dla prostych GET
        return withRetry(maxRetries = 3, delay = 500) {
            client.get("$baseUrl/users").jsonList()
        }
    }

    suspend fun createOrder(order: Order): Order {
        // Dłuższy retry dla ważnych operacji
        return withRetry(
            maxRetries = 5,
            delay = 2000,
            exponential = true
        ) {
            client.post("$baseUrl/orders", order.toJson()).json()
        }
    }

    private suspend fun <T> withRetry(
        maxRetries: Int,
        delay: Long,
        exponential: Boolean = false,
        block: suspend () -> T
    ): T {
        var lastError: Exception? = null
        var currentDelay = delay

        repeat(maxRetries) { attempt ->
            try {
                return block()
            } catch (e: Exception) {
                lastError = e
                Log.w("API", "Retry ${attempt + 1}/$maxRetries: ${e.message}")

                if (attempt < maxRetries - 1) {
                    delay(currentDelay)
                    if (exponential) {
                        currentDelay = minOf(currentDelay * 2, 30_000)
                    }
                }
            }
        }

        throw lastError ?: Exception("Max retries exceeded")
    }
}

Porównanie strategii

Strategia Delays Użycie
Simple 1s, 1s, 1s Szybkie błędy serwera
Exponential 1s, 2s, 4s, 8s Przeciążony serwer
Exp + Jitter ~1s, ~2s, ~4s Wiele klientów naraz
Linear 1s, 2s, 3s, 4s Stopniowe odciążenie

📚 Zobacz też