实际开发中,经常需要"重试"这个逻辑,例如:Http请求,发送IM消息,播放视频,直播推流,拉流等等。一般框架本身会提供重试方案,但是他们不统一,可能不好自定义重试策略。
重试应该是一套独立的逻辑,它不关心处理什么任务,只关心重试的逻辑本身。得益于Kotlin
协程,可以轻松的封装重试逻辑。
封装
1.0版本
有bug的版本:
kotlin
suspend fun <T> retry(
/** 最大执行次数 */
maxCount: Int = 3,
/** 重试间隔 */
delay: Long = 5000,
/** 执行回调 */
block: suspend () -> T,
): Result<T> {
// 当前第几次执行
var currentCount = 0
while (true) {
// 增加次数
currentCount++
// 执行block
val result = runCatching {
block()
}
// 如果成功,返回结果
if (result.isSuccess) {
return result
}
if (currentCount >= maxCount) {
// 达到最大执行次数
val exception = checkNotNull(result.exceptionOrNull())
return Result.failure(RetryMaxCountException(exception))
} else {
// 延迟后继续执行
delay(delay)
continue
}
}
}
/** 达到最大执行次数异常 */
class RetryMaxCountException(cause: Throwable) : Exception(cause)
整体逻辑比较简单:循环执行block
代码块,如果成功的话就返回结果,如果失败的话就重试,达到最大次数时,返回失败结果,结果的异常是RetryMaxCountException
,并包含最后一次失败的真实异常。
上面的代码有bug:执行block
的时候捕获了所有异常,包括协程取消异常CancellationException
,导致协程不能被及时的取消,改进一下代码:
kotlin
val result = runCatching {
block()
}.onFailure { e ->
// 取消异常,继续传播
if (e is CancellationException) throw e
}
修改好了,但还是有问题:block
里面可能没有抛出取消异常,而调用retry
函数的协程已经被取消了,此时应该及时取消,而不是返回一个成功的结果,再改进一下代码:
kotlin
// 确保当前协程处于`active`状态,否则抛出取消异常
currentCoroutineContext().ensureActive()
if (result.isSuccess) {
return result
}
在每次block
执行完成之后,调用currentCoroutineContext().ensureActive()
确保当前协程处于active
状态,否则抛出取消异常。
1.1版本
1.0版本的雏形已经写好了,但是实际使用中,我们可能会根据当前是第几次重试,做一些额外的业务逻辑,所以要让block
能知道当前是第几次重试。
最直接的方法是给block
的lambda
增加一个当前第几次重试的参数,但是考虑到后面可能还会有更多扩展,我们可以定义一个RetryScope
:
kotlin
interface RetryScope {
/** 当前执行次数 */
val currentCount: Int
}
再把参数修改成这样子:
kotlin
suspend fun <T> retry(
// ...,
block: suspend RetryScope.() -> T,
): Result<T>
最后,当然要写一下RetryScope
的实现类,并在RetryScope
的作用域中调用block
。
实现类:
kotlin
private class RetryScopeImpl : RetryScope {
private var _count = 0
override val currentCount: Int get() = _count
fun increaseCount() = _count++
}
在RetryScope
的作用域中调用block
:
javascript
with(RetryScopeImpl()) {
while (true) {
// 增加次数
increaseCount()
// ...
}
}
修改好了,这样子我们在block
中就能直接调用currentCount
查询当前是第几次重试了。
1.2版本
目前为止,重试间隔是固定的,但有时候我们希望根据第几次重试,改变重试间隔,例如第一次是5秒,第二次是10秒,第三次是15秒,或者其他算法。
首先修改一下获取延迟间隔的参数,把delay
参数修改为一个lambda
:
kotlin
suspend fun <T> retry(
// ...,
/** 获取延迟毫秒 */
getDelay: RetryScope.() -> Long = { 5_000 },
// ...,
): Result<T>
调用getDelay
获取延迟间隔:
kotlin
if (currentCount >= maxCount) {
// ...
} else {
// 延迟后继续执行
delay(getDelay())
continue
}
这样子在getDelay
中也能查询到当前第几次重试了,并根据第几次重试灵活设置重试间隔。
1.3版本
需求又来了,有时候我们不希望在任何异常情况下都重试,也就是说失败的时候要根据具体的失败异常,来决定要不要继续重试。
很显然,我们需要新增一个失败的回调参数,通过回调的返回值决定要不要继续重试:
kotlin
suspend fun <T> retry(
// ...,
/** 失败回调 */
onFailure: RetryScope.(Throwable) -> Boolean = { true },
// ...,
): Result<T> {
// ...
val exception = checkNotNull(result.exceptionOrNull())
val shouldContinue = onFailure(exception).also{currentCoroutineContext().ensureActive() }
if (!shouldContinue) {
return result
}
// ...
}
在失败的时候,先通知onFailure
回调并把异常传给它,根据它的返回值,true
:继续执行下面的逻辑;false
:返回当前的失败结果。
注意:onFailure
执行完成之后,还是要调用currentCoroutineContext().ensureActive()
一下,因为我们不能确定onFailure
里面到底发生了什么。
到此为止,算是一个比较完整的代码了,看一下该函数完整代码:
kotlin
suspend fun <T> retry(
maxCount: Int = 3,
getDelay: RetryScope.() -> Long = { 5_000 },
onFailure: RetryScope.(Throwable) -> Boolean = { true },
block: suspend RetryScope.() -> T,
): Result<T> {
require(maxCount > 0)
with(RetryScopeImpl()) {
while (true) {
// 增加次数
increaseCount()
val result = runCatching {
block()
}.onFailure { e ->
if (e is CancellationException) throw e
}
currentCoroutineContext().ensureActive()
if (result.isSuccess) {
return result
}
val exception = checkNotNull(result.exceptionOrNull())
val shouldContinue = onFailure(exception).also { currentCoroutineContext().ensureActive() }
if (!shouldContinue) {
return result
}
if (currentCount >= maxCount) {
// 达到最大执行次数
return Result.failure(RetryMaxCountException(exception))
} else {
// 延迟后继续执行
delay(getDelay())
continue
}
}
}
}
扩展
用模拟代码演示一下使用:
kotlin
lifecycleScope.launch {
retry {
// 模拟请求数据
requestData()
}.onSuccess { data ->
// 处理成功逻辑
}.onFailure { error ->
// 处理失败逻辑
}
}
然而实际情况是,大部分需要重试的场景一般都需要网络,可以再封装一个netRetry
函数,要求网络已连接才执行:
kotlin
suspend fun <T> netRetry(
maxCount: Int = 3,
getDelay: RetryScope.() -> Long = { 5_000 },
onFailure: RetryScope.(Throwable) -> Boolean = { shouldRetry(it) },
block: suspend RetryScope.() -> T,
): Result<T> {
return retry(
maxCount = maxCount,
getDelay = getDelay,
onFailure = onFailure,
block = {
// 如果网络不通畅,则挂起,等待网络通畅恢复
fAwaitNetwork()
block()
},
)
}
private fun shouldRetry(throwable: Throwable): Boolean {
if (throwable is SocketTimeoutException) return true
return !FNetwork.currentNetwork.isConnected
}
有了前面的封装,这里的代码就比较简单,主要是挂起函数fAwaitNetwork
,这个函数调用的时候会先检查网络是否已连接,如果已连接则不会挂起,如果网络未连接的话会挂起,直到网络已连接。
shouldRetry
的返回值表示是否需要继续执行后面的逻辑,读者可以根据实际业务修改。
关于监听网络,和这个fAwaitNetwork
函数的具体实现,可以查看这篇文章
结束
完整代码在这里:retry-ktx
感谢你的阅读,如果有问题欢迎一起交流学习。