feat: add retry with exponential backoff to all repositories

- New: RetryHelper.kt — retryOnTransientError(maxRetries=2, baseDelay=1s)
- Wraps in RefreshBalanceUseCase — single dispatch point for all 5 services
- Retries on Network and 5xx ServerError
- 401/403 (InvalidCredential), 429 (RateLimited), ParseError — no retry
This commit is contained in:
WaterRain 2026-07-06 23:57:58 +00:00
parent f2e5eedaa4
commit 220a74492a
No known key found for this signature in database
2 changed files with 47 additions and 5 deletions

View File

@ -0,0 +1,39 @@
package com.rainy.token.data.repository
import kotlinx.coroutines.delay
import kotlin.math.pow
/**
* 通用重试包装。
*
* 对 [RepositoryError.Network] 和 [RepositoryError.ServerError] 执行指数退避重试。
* 401/403(InvalidCredential)、429(RateLimited)、ParseError 等不重试。
*
* @param maxRetries 最大重试次数(不含首次),默认 2
* @param baseDelayMs 退避基数,默认 1000ms(第二次 = 2000ms)
* @param block 实际请求逻辑
*/
suspend fun <T> retryOnTransientError(
maxRetries: Int = 2,
baseDelayMs: Long = 1000L,
block: suspend () -> Result<T>
): Result<T> {
var lastResult = block()
if (lastResult.isSuccess) return lastResult
repeat(maxRetries) { attempt ->
val error = lastResult.exceptionOrNull()
val shouldRetry = when (error) {
is RepositoryError.Network -> true
is RepositoryError.ServerError -> error.code in 500..599
else -> false
}
if (!shouldRetry) return lastResult
val backoffMs = baseDelayMs * (2.0.pow(attempt).toLong())
delay(backoffMs)
lastResult = block()
if (lastResult.isSuccess) return lastResult
}
return lastResult
}

View File

@ -5,6 +5,7 @@ import com.rainy.token.data.repository.CommandCodeGoRepository
import com.rainy.token.data.repository.DeepSeekRepository
import com.rainy.token.data.repository.OllamaRepository
import com.rainy.token.data.repository.OpenCodeGoRepository
import com.rainy.token.data.repository.retryOnTransientError
import com.rainy.token.domain.model.ServiceBalance
import com.rainy.token.domain.service.ServiceType
import javax.inject.Inject
@ -19,6 +20,8 @@ import javax.inject.Provider
* 注入同一 UseCase 时的"could not be resolved"误报(KSP 已知 issue)。
* Provider 让 Hilt 推迟创建 Repository 实例到第一次 .get() 时,KSP 不需要在
* 编译期解析所有构造器签名。
* - **重试策略**:对 Network 和 5xx ServerError 自动指数退避重试(最多 2 次),
* 401/403/429/ParseError 不重试。
*/
class RefreshBalanceUseCase @Inject constructor(
private val deepSeekRepositoryProvider: Provider<DeepSeekRepository>,
@ -28,10 +31,10 @@ class RefreshBalanceUseCase @Inject constructor(
private val ollamaRepositoryProvider: Provider<OllamaRepository>
) {
suspend operator fun invoke(service: ServiceType): Result<ServiceBalance> = when (service) {
ServiceType.DEEPSEEK -> deepSeekRepositoryProvider.get().fetchBalance()
ServiceType.OPENCODE_GO -> openCodeGoRepositoryProvider.get().fetchBalance()
ServiceType.COMMANDCODE_GO -> commandCodeGoRepositoryProvider.get().fetchBalance()
ServiceType.CODEX -> codexRepositoryProvider.get().fetchBalance()
ServiceType.OLLAMA -> ollamaRepositoryProvider.get().fetchBalance()
ServiceType.DEEPSEEK -> retryOnTransientError { deepSeekRepositoryProvider.get().fetchBalance() }
ServiceType.OPENCODE_GO -> retryOnTransientError { openCodeGoRepositoryProvider.get().fetchBalance() }
ServiceType.COMMANDCODE_GO -> retryOnTransientError { commandCodeGoRepositoryProvider.get().fetchBalance() }
ServiceType.CODEX -> retryOnTransientError { codexRepositoryProvider.get().fetchBalance() }
ServiceType.OLLAMA -> retryOnTransientError { ollamaRepositoryProvider.get().fetchBalance() }
}
}