🔄 Idempotency Manager

KNETIdempotencyManager automatycznie zarządza kluczami idempotentności dla bezpiecznego retry requestów mutujących (POST/PUT/PATCH).

💡 Co to idempotency?

Idempotentny request to taki, który może być bezpiecznie powtórzony bez zmiany wyniku. Klucz idempotentności pozwala serwerowi wykryć duplikaty i zwrócić oryginalną odpowiedź zamiast wykonywać operację ponownie.

📦 Import

import rip.nerd.kitsunenet.util.KNETIdempotencyManager
import rip.nerd.kitsunenet.util.withIdempotencyKey
import rip.nerd.kitsunenet.util.withDeterministicIdempotencyKey

🚀 Szybki start

val idempotencyManager = KNETIdempotencyManager.create()

// Dodaj jako interceptor - automatyczne klucze
val client = KNETClient.builder()
    .addInterceptor(idempotencyManager.interceptor())
    .build()

// Teraz każdy POST/PUT/PATCH ma automatyczny klucz
val response = client.post("https://api.example.com/payments", mapOf(
    "amount" to 100.00,
    "currency" to "PLN"
))
// Request ma header: X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

🔧 Konfiguracja

val manager = KNETIdempotencyManager.builder()
    // Nazwa header
    .headerName("Idempotency-Key")  // Default: X-Idempotency-Key

    // Custom generator kluczy
    .keyGenerator {
        "${System.currentTimeMillis()}_${UUID.randomUUID()}"
    }

    // Które metody HTTP
    .includeMethods("POST", "PUT", "PATCH", "DELETE")

    // Wykluczone ścieżki
    .excludePaths("/health", "/status", "/metrics")

    // Cache odpowiedzi (dla retry)
    .enableResponseCache(true)
    .responseCacheTtlMs(3600_000)  // 1 godzina

    .build()

🎯 Ręczne klucze

Extension functions

// Własny klucz
val request = KNETRequest.post(url, data)
    .withIdempotencyKey("order-123-create")

// Deterministyczny klucz (powtarzalny)
val request = KNETRequest.post(url, data)
    .withDeterministicIdempotencyKey()

Manager API

// Generuj deterministyczny klucz z danych
val key = idempotencyManager.generateDeterministicKey(
    userId,       // "user_123"
    "create_order",
    orderId       // "order_456"
)
// key = "a1b2c3d4..." (MD5 hash)

// Lub z mapy
val key = idempotencyManager.generateDeterministicKey(mapOf(
    "user_id" to userId,
    "action" to "payment",
    "amount" to 100.00
))

// Ustaw ręcznie
val requestWithKey = idempotencyManager.setKey(request, key)

📊 Statystyki

val stats = idempotencyManager.getStats()

println("Keys generated: ${stats.keysGenerated}")
println("Keys reused: ${stats.keysReused}")
println("Cache hits: ${stats.cacheHits}")
println("Active cache entries: ${stats.activeCacheEntries}")

💡 Praktyczne przykłady

Płatności

class PaymentService(private val client: KNETClient) {

    private val idempotency = KNETIdempotencyManager.builder()
        .headerName("Stripe-Idempotency-Key")
        .enableResponseCache(true)
        .responseCacheTtlMs(24 * 60 * 60 * 1000) // 24h
        .build()

    suspend fun createPayment(
        orderId: String,
        amount: Double,
        currency: String
    ): PaymentResult {
        // Deterministyczny klucz - ten sam dla retry
        val key = idempotency.generateDeterministicKey(
            orderId, amount, currency
        )

        val request = KNETRequest.post(
            url = "https://api.stripe.com/v1/charges",
            data = mapOf(
                "amount" to (amount * 100).toInt(),
                "currency" to currency,
                "source" to "tok_visa"
            )
        ).withIdempotencyKey(key)

        // Bezpieczne retry - ten sam klucz = ta sama odpowiedź
        return retry(3) {
            client.request(request).json()
        }
    }
}

Tworzenie zamówień

class OrderService(private val client: KNETClient) {

    private val idempotency = KNETIdempotencyManager.withCache()

    suspend fun createOrder(userId: String, items: List<CartItem>): Order {
        // Klucz oparty na zawartości koszyka
        val key = idempotency.generateDeterministicKey(
            userId,
            items.map { "${it.productId}:${it.quantity}" }.sorted().joinToString(",")
        )

        val request = KNETRequest.post(
            url = "$baseUrl/orders",
            data = mapOf(
                "user_id" to userId,
                "items" to items.map { it.toMap() }
            )
        ).withIdempotencyKey(key)

        return client.request(request).json()
    }
}

Z Circuit Breaker

val client = KNETClient.builder()
    .addInterceptor(KNETIdempotencyManager.create().interceptor())
    .addInterceptor(KNETCircuitBreakerInterceptor(circuitBreaker))
    .addInterceptor(KNETRetryInterceptor(maxRetries = 3))
    .build()

// Bezpieczna kombinacja:
// 1. Idempotency key dodany
// 2. Retry z tym samym kluczem
// 3. Circuit breaker chroni przed cascade failure

Webhook retry

class WebhookSender(private val client: KNETClient) {

    private val idempotency = KNETIdempotencyManager.builder()
        .headerName("X-Webhook-ID")
        .build()

    suspend fun sendWebhook(
        webhookId: String,
        event: String,
        payload: Map<String, Any>
    ) {
        // Użyj webhook ID jako klucz - odbiorca może deduplikować
        val request = KNETRequest.post(
            url = webhookUrl,
            data = mapOf(
                "event" to event,
                "payload" to payload,
                "timestamp" to System.currentTimeMillis()
            )
        ).withIdempotencyKey(webhookId)

        // Retry z tym samym ID
        retryWithBackoff(maxAttempts = 5) {
            client.request(request)
        }
    }
}

⚠️ Dobre praktyki

Ważne
  • Klucze powinny być unikalne dla logicznie różnych operacji
  • Używaj deterministycznych kluczy dla retry (nie random)
  • Serwer musi wspierać idempotency keys
  • Ustaw odpowiedni TTL dla cache odpowiedzi

❌ Źle

// Random klucz przy każdym retry - nie działa!
val key = UUID.randomUUID().toString()
retry(3) {
    val request = KNETRequest.post(url, data)
        .withIdempotencyKey(key)  // ❌ Nowy klucz przy każdym retry
    client.request(request)
}

✅ Dobrze

// Ten sam klucz dla wszystkich retry
val key = idempotency.generateDeterministicKey(orderId, amount)

retry(3) {
    val request = KNETRequest.post(url, data)
        .withIdempotencyKey(key)  // ✅ Ten sam klucz
    client.request(request)
}

🔗 API Reference

KNETIdempotencyManager

Metoda Opis
interceptor() Tworzy interceptor dla KNETClient
getOrCreateKey(request) Generuje lub pobiera klucz
generateDeterministicKey(...) Generuje powtarzalny klucz
setKey(request, key) Ustawia klucz na request
getCachedResponse(key) Pobiera cached response
clearAll() Czyści wszystkie cache
cleanup() Czyści wygasłe wpisy
getStats() Statystyki użycia

Companion functions

Metoda Opis
create() Domyślna konfiguracja
withCache(ttlMs) Z cache odpowiedzi
builder() Custom konfiguracja

Extensions

Extension Opis
KNETRequest.withIdempotencyKey(key) Dodaje własny klucz
KNETRequest.withDeterministicIdempotencyKey() Generuje klucz z danych requestu

📚 Zobacz też