# Trakt Auth Resilience — Circuit Breaker + Rate Limiter

## Circuit Breaker (exponential backoff)

```kotlin
companion object {
    private const val REFRESH_WINDOW_MS = 24 * 60 * 60 * 1000L  // refresh 24h before expiry
    private val circuitBaseCooldownMs = 5 * 60_000L               // 5 min base
    private val circuitMaxCooldownMs = 60 * 60_000L               // 60 min max
    private val rateLimitWindowMs = 5 * 60_000L
    private val rateLimitMaxCalls = 900                           // under Trakt's 1,000 cap
}

@Volatile private var circuitOpenUntilMs = 0L
private val circuitFailures = AtomicInteger(0)

// On failure:
val cooldownMs = (circuitBaseCooldownMs shl (circuitFailures.get().coerceAtMost(5) - 1))
    .coerceAtMost(circuitMaxCooldownMs)
circuitOpenUntilMs = System.currentTimeMillis() + cooldownMs
circuitFailures.incrementAndGet()
```

## Rate Limiter (sliding window)

```kotlin
private val getRequestTimestamps = ArrayDeque<Long>()
private val rateLimitMutex = Mutex()

suspend fun checkRateLimit(): Boolean = rateLimitMutex.withLock {
    val now = System.currentTimeMillis()
    getRequestTimestamps.removeIf { now - it > rateLimitWindowMs }
    if (getRequestTimestamps.size >= rateLimitMaxCalls) {
        Log.w(TAG, "Rate limit reached (${getRequestTimestamps.size}/$rateLimitMaxCalls)")
        return false
    }
    getRequestTimestamps.addLast(now)
    true
}
```

## Token refresh (mutex-guarded)

```kotlin
private val tokenRefreshMutex = Mutex()

suspend fun refreshIfNeeded(): Boolean = tokenRefreshMutex.withLock {
    val expiresAt = getTokenExpiry() ?: return false
    if (System.currentTimeMillis() > expiresAt - REFRESH_WINDOW_MS) {
        // Refresh now
        val result = traktApi.refreshToken(...)
        if (result.isSuccessful) { saveToken(result.body()); true }
        else false
    } else true
}
```

## Device flow reuse (avoid hitting /oauth/device/code repeatedly)

```kotlin
private var cachedDeviceExpiresAt: Long = 0L

suspend fun startDeviceFlow(): FlowResult {
    if (System.currentTimeMillis() < cachedDeviceExpiresAt) {
        // reuse existing cached flow
        return Success(cachedDeviceCode!!, cachedUserCode!!, ...)
    }
    // otherwise, hit the API
}
```

## HTTP status classification

```kotlin
private val transientRetryStatusCodes = setOf(502, 503, 504, 520, 521, 522)
private val nonRetryableStatusCodes = setOf(400, 403, 404, 405, 409, 412, 420, 422, 423, 426)
```

Source: `TraktAuthManager.kt` (NuvioTV pattern)
