⏱️ Rate Limit Handler

KNETRateLimitHandler to inteligentny mechanizm obsługi rate limitów API. Automatycznie parsuje nagłówki rate limit, obsługuje retry przy błędzie 429 (Too Many Requests), i pozwala na pre-emptive throttling.

💡 Kiedy używać?

Używaj Rate Limit Handler gdy pracujesz z API, które ma limity zapytań (np. GitHub API, Twitter API, Stripe).

📦 Import

import rip.nerd.kitsunenet.util.KNETRateLimitHandler

🚀 Szybki start

Podstawowe użycie

// Utwórz handler
val handler = KNETRateLimitHandler()

// Wykonaj request z automatyczną obsługą rate limit
val response = handler.executeWithRateLimit(client, request)

// Handler automatycznie:
// ✅ Parsuje nagłówki X-RateLimit-* z odpowiedzi
// ✅ Retry przy 429 z odpowiednim opóźnieniem
// ✅ Respektuje nagłówek Retry-After

Z Builder Pattern

val handler = KNETRateLimitHandler.builder()
    .maxRetries(3)                    // Maksymalna liczba prób
    .defaultRetryDelayMs(1000)        // Domyślne opóźnienie retry
    .maxRetryDelayMs(60_000)          // Maksymalne opóźnienie
    .onRateLimited { host, delayMs ->
        // Callback wywoływany przy rate limit
        Log.w("API", "Rate limited na $host, czekam ${delayMs}ms")
        analytics.track("rate_limited", mapOf("host" to host))
    }
    .build()

📋 Obsługiwane nagłówki

Handler automatycznie parsuje następujące nagłówki z odpowiedzi API:

Nagłówek Opis Przykład
X-RateLimit-Limit Maksymalna liczba zapytań w oknie czasowym 1000
X-RateLimit-Remaining Pozostała liczba zapytań 423
X-RateLimit-Reset Timestamp resetu limitu (Unix) 1609459200
Retry-After Sekundy do ponowienia lub data HTTP 60 lub Wed, 21 Oct 2025 07:28:00 GMT
📝 Uwaga

Handler obsługuje też alternatywne nazwy: RateLimit-* (IETF draft) i X-Rate-Limit-*.

🔧 API Reference

executeWithRateLimit()

Wykonuje request z automatycznym retry przy rate limit.

suspend fun executeWithRateLimit(
    client: KNETClient,
    request: KNETRequest
): KNETResponse
Przykład
val handler = KNETRateLimitHandler(maxRetries = 5)

lifecycleScope.launch {
    try {
        val response = handler.executeWithRateLimit(client,
            KNETRequest.get("https://api.github.com/users/octocat")
        )

        // Sukces - przetwórz odpowiedź
        val user = response.json<GitHubUser>()
        updateUI(user)

    } catch (e: Exception) {
        // Wszystkie retry wyczerpane lub inny błąd
        showError("Nie udało się pobrać danych: ${e.message}")
    }
}

executeWithPreemptiveThrottle()

Czeka PRZED wysłaniem requestu jeśli limit jest niski.

suspend fun executeWithPreemptiveThrottle(
    client: KNETClient,
    request: KNETRequest,
    minRemaining: Int = 1  // Czekaj jeśli remaining < minRemaining
): KNETResponse
Przykład - unikaj rate limit
// Czekaj jeśli pozostało mniej niż 10 zapytań
val response = handler.executeWithPreemptiveThrottle(
    client,
    request,
    minRemaining = 10
)

// Dzięki temu unikasz błędu 429 - handler sam poczeka
// aż limit się zresetuje przed wysłaniem requestu

execute()

Wykonuje request i zwraca Result (bez wyjątków).

suspend fun execute(
    client: KNETClient,
    request: KNETRequest
): Result
Przykład z Result
val result = handler.execute(client, request)

when (result) {
    is KNETRateLimitHandler.Result.Success -> {
        val response = result.response
        val info = result.rateLimitInfo

        Log.d("API", "Sukces! Pozostało: ${info?.remaining}/${info?.limit}")
        processResponse(response)
    }

    is KNETRateLimitHandler.Result.RateLimited -> {
        Log.w("API", "Rate limited! Retry za: ${result.retryAfterMs}ms")
        showRateLimitWarning(result.retryAfterMs)
    }

    is KNETRateLimitHandler.Result.Error -> {
        Log.e("API", "Błąd: ${result.exception.message}")
        showError(result.exception)
    }
}

getRateLimitInfo()

Pobiera aktualne informacje o rate limit dla hosta.

fun getRateLimitInfo(host: String): RateLimitInfo?
Przykład - wyświetl stan limitu
val info = handler.getRateLimitInfo("api.github.com")

if (info != null) {
    println("Limit: ${info.limit}")
    println("Pozostało: ${info.remaining}")
    println("Reset za: ${info.resetInSeconds} sekund")
    println("Wykorzystanie: ${info.usagePercent}%")
    println("Wyczerpany: ${info.isExhausted}")

    // Pokaż w UI
    progressBar.max = info.limit
    progressBar.progress = info.remaining
    resetTimeText.text = "Reset za ${info.resetInSeconds}s"
} else {
    println("Brak danych o rate limit dla tego hosta")
}

canExecute()

Sprawdza czy można wykonać request bez czekania.

fun canExecute(host: String): Boolean
Przykład
if (handler.canExecute("api.example.com")) {
    // Można wykonać natychmiast
    val response = handler.executeWithRateLimit(client, request)
} else {
    // Limit wyczerpany - pokaż komunikat
    val waitTime = handler.estimateWaitTime("api.example.com")
    showMessage("Proszę czekać ${waitTime / 1000} sekund")
}

estimateWaitTime()

Szacuje czas oczekiwania do resetu limitu.

fun estimateWaitTime(host: String): Long // milisekundy

⚙️ RateLimitInfo

Struktura przechowująca informacje o rate limit:

data class RateLimitInfo(
    val host: String,           // Host API
    val limit: Int,             // Maksymalna liczba zapytań
    val remaining: Int,         // Pozostałe zapytania
    val resetTimestamp: Long,   // Timestamp resetu (ms)
    val retryAfterMs: Long?,    // Retry-After z 429 (opcjonalne)
    val updatedAt: Long         // Kiedy zaktualizowano
) {
    val resetInSeconds: Long    // Sekundy do resetu
    val isExhausted: Boolean    // Czy limit wyczerpany
    val usagePercent: Double    // % wykorzystania
    val isFresh: Boolean        // Czy dane są aktualne (<60s)
}

🎯 Presety

Gotowe konfiguracje dla typowych scenariuszy:

// Standardowy (3 retry, 1s opóźnienie)
val handler = KNETRateLimitHandler.standard()

// Agresywny (5 retry, 2s opóźnienie, max 2min)
val handler = KNETRateLimitHandler.aggressive()

// Bez retry (fail-fast)
val handler = KNETRateLimitHandler.noRetry()
Preset Max Retries Default Delay Max Delay Użycie
standard() 3 1s 60s Większość API
aggressive() 5 2s 120s Ważne operacje
noRetry() 0 - - Fail-fast

💡 Praktyczne przykłady

Integracja z GitHub API

class GitHubApiClient(
    private val token: String
) {
    private val client = KNETClient.builder()
        .addInterceptor(KNETHeaderInterceptor.bearer(token))
        .build()

    private val rateLimitHandler = KNETRateLimitHandler.builder()
        .maxRetries(3)
        .onRateLimited { host, delay ->
            Log.w("GitHub", "Rate limited, retry za ${delay}ms")
        }
        .build()

    suspend fun getUser(username: String): GitHubUser {
        val request = KNETRequest.get(
            "https://api.github.com/users/$username"
        )

        val response = rateLimitHandler.executeWithRateLimit(client, request)
        return response.json<GitHubUser>()
    }

    fun getRemainingRequests(): Int {
        return rateLimitHandler
            .getRateLimitInfo("api.github.com")
            ?.remaining ?: -1
    }
}

Wyświetlanie stanu limitu w UI

class RateLimitViewModel : ViewModel() {
    private val handler = KNETRateLimitHandler()

    private val _rateLimitState = MutableStateFlow<RateLimitState>(RateLimitState.Unknown)
    val rateLimitState = _rateLimitState.asStateFlow()

    fun checkRateLimit(host: String) {
        val info = handler.getRateLimitInfo(host)

        _rateLimitState.value = when {
            info == null -> RateLimitState.Unknown
            info.isExhausted -> RateLimitState.Exhausted(info.resetInSeconds)
            info.remaining < 10 -> RateLimitState.Low(info.remaining, info.limit)
            else -> RateLimitState.Ok(info.remaining, info.limit)
        }
    }
}

sealed class RateLimitState {
    object Unknown : RateLimitState()
    data class Ok(val remaining: Int, val limit: Int) : RateLimitState()
    data class Low(val remaining: Int, val limit: Int) : RateLimitState()
    data class Exhausted(val resetInSeconds: Long) : RateLimitState()
}

Batch requests z rate limit

suspend fun fetchAllUsers(userIds: List<String>): List<User> {
    val handler = KNETRateLimitHandler()
    val results = mutableListOf<User>()

    for (userId in userIds) {
        // Sprawdź czy możemy kontynuować
        if (!handler.canExecute("api.example.com")) {
            val waitTime = handler.estimateWaitTime("api.example.com")
            Log.d("API", "Czekam ${waitTime}ms na reset limitu")
            delay(waitTime)
        }

        val response = handler.executeWithRateLimit(
            client,
            KNETRequest.get("https://api.example.com/users/$userId")
        )

        results.add(response.json<User>())
    }

    return results
}

⚠️ Obsługa błędów

try {
    val response = handler.executeWithRateLimit(client, request)
    // Sukces
} catch (e: KNETError.HttpError) {
    when (e.statusCode) {
        429 -> {
            // Wszystkie retry wyczerpane
            showError("API limit wyczerpany. Spróbuj później.")
        }
        401 -> showError("Brak autoryzacji")
        403 -> showError("Brak dostępu")
        else -> showError("Błąd HTTP: ${e.statusCode}")
    }
} catch (e: KNETError.NetworkError) {
    showError("Błąd sieci: ${e.message}")
} catch (e: Exception) {
    showError("Nieoczekiwany błąd: ${e.message}")
}

📚 Zobacz też