Как повторить попытку экспоненциального отката на сопрограммах kotlin

Я использую сопрограммы kotlin для сетевого запроса с использованием метода расширения для вызова класса в модификации, подобной этой

public suspend fun <T : Any> Call<T>.await(): T {

  return suspendCancellableCoroutine { continuation -> 

    enqueue(object : Callback<T> {

        override fun onResponse(call: Call<T>?, response: Response<T?>) {
            if (response.isSuccessful) {
                val body = response.body()
                if (body == null) {
                    continuation.resumeWithException(
                            NullPointerException("Response body is null")
                    )
                } else {
                    continuation.resume(body)
                }
            } else {
                continuation.resumeWithException(HttpException(response))
            }
        }

        override fun onFailure(call: Call<T>, t: Throwable) {
            // Don't bother with resuming the continuation if it is already cancelled.
            if (continuation.isCancelled) return
            continuation.resumeWithException(t)
        }
    })

      registerOnCompletion(continuation)
  }
}

затем с вызывающей стороны я использую вышеуказанный метод, подобный этому

private fun getArticles()  = launch(UI) {

    loading.value = true
    try {
        val networkResult = api.getArticle().await()
        articles.value =  networkResult

    }catch (e: Throwable){
        e.printStackTrace()
        message.value = e.message

    }finally {
        loading.value = false
    }

}

Я хочу экспоненциально повторить этот вызов API в некоторых случаях, например (IOException), как я могу этого добиться?


person shakil.k    schedule 22.10.2017    source источник


Ответы (3)


Я бы посоветовал написать вспомогательную функцию высшего порядка для логики повтора. Для начала вы можете использовать следующую реализацию:

suspend fun <T> retryIO(
    times: Int = Int.MAX_VALUE,
    initialDelay: Long = 100, // 0.1 second
    maxDelay: Long = 1000,    // 1 second
    factor: Double = 2.0,
    block: suspend () -> T): T
{
    var currentDelay = initialDelay
    repeat(times - 1) {
        try {
            return block()
        } catch (e: IOException) {
            // you can log an error here and/or make a more finer-grained
            // analysis of the cause to see if retry is needed
        }
        delay(currentDelay)
        currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
    }
    return block() // last attempt
}

Использовать эту функцию очень просто:

val networkResult = retryIO { api.getArticle().await() }

Вы можете изменять параметры повтора в каждом конкретном случае, например:

val networkResult = retryIO(times = 3) { api.doSomething().await() }

Вы также можете полностью изменить реализацию retryIO в соответствии с потребностями вашего приложения. Например, вы можете жестко запрограммировать все параметры повторных попыток, избавиться от ограничения на количество повторных попыток, изменить значения по умолчанию и т. Д.

person Roman Elizarov    schedule 23.10.2017
comment
Это было то, что было в моей голове уже несколько дней. Приятно видеть, что решение не сложнее того, что я себе представлял. Я также спрашивал себя, имеет ли смысл определять эту вспомогательную функцию как встроенную. И последнее, но не менее важное: как можно изменить a, если вы хотите выполнить повторную попытку только после того, как попросите пользователя сделать это (например, в диалоге)? - person Fatih Coşkun; 27.10.2017
comment
Также намного чище, чем решение Rx: -O - person kenyee; 07.11.2017
comment
если kotlin.github.io/kotlinx .coroutines / kotlinx-coroutines-core / также будет выдавать текущее количество повторных попыток, которое можно было бы использовать для выполнения чистого (экспоненциального) отката. - person ligi; 03.11.2019

Вы можете попробовать этот простой, но очень гибкий подход с простым использованием:

РЕДАКТИРОВАТЬ: добавлено более сложное решение в отдельном ответе.

class Completion(private val retry: (Completion) -> Unit) {
    fun operationFailed() {
        retry.invoke(this)
    }
}

fun retryOperation(retries: Int, 
                   dispatcher: CoroutineDispatcher = Dispatchers.Default, 
                   operation: Completion.() -> Unit
) {
    var tryNumber = 0

    val completion = Completion {
        tryNumber++
        if (tryNumber < retries) {
            GlobalScope.launch(dispatcher) {
                delay(TimeUnit.SECONDS.toMillis(tryNumber.toLong()))
                operation.invoke(it)
            }
        }
    }

    operation.invoke(completion)
}

Используйте это так:

retryOperation(3) {
    if (!tryStuff()) {
        // this will trigger a retry after tryNumber seconds
        operationFailed()
    }
}

Очевидно, что на этом можно построить больше.

person Sir Codesalot    schedule 19.05.2020

Вот более сложная и удобная версия моего предыдущего ответа, надеюсь, это кому-то поможет:

class RetryOperation internal constructor(
    private val retries: Int,
    private val initialIntervalMilli: Long = 1000,
    private val retryStrategy: RetryStrategy = RetryStrategy.LINEAR,
    private val retry: suspend RetryOperation.() -> Unit
) {
    var tryNumber: Int = 0
        internal set

    suspend fun operationFailed() {
        tryNumber++
        if (tryNumber < retries) {
            delay(calculateDelay(tryNumber, initialIntervalMilli, retryStrategy))
            retry.invoke(this)
        }
    }
}

enum class RetryStrategy {
    CONSTANT, LINEAR, EXPONENTIAL
}

suspend fun retryOperation(
    retries: Int = 100,
    initialDelay: Long = 0,
    initialIntervalMilli: Long = 1000,
    retryStrategy: RetryStrategy = RetryStrategy.LINEAR,
    operation: suspend RetryOperation.() -> Unit
) {
    val retryOperation = RetryOperation(
        retries,
        initialIntervalMilli,
        retryStrategy,
        operation,
    )

    delay(initialDelay)

    operation.invoke(retryOperation)
}

internal fun calculateDelay(tryNumber: Int, initialIntervalMilli: Long, retryStrategy: RetryStrategy): Long {
    return when (retryStrategy) {
        RetryStrategy.CONSTANT -> initialIntervalMilli
        RetryStrategy.LINEAR -> initialIntervalMilli * tryNumber
        RetryStrategy.EXPONENTIAL -> 2.0.pow(tryNumber).toLong()
    }
}

Использование:

coroutineScope.launch {
    retryOperation(3) {
        if (!tryStuff()) {
            Log.d(TAG, "Try number $tryNumber")
            operationFailed()
        }
    }
}
person Sir Codesalot    schedule 19.05.2021