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