🔄 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 |