|
| 1 | +package com.theapache64.stackzy.data.util |
| 2 | + |
| 3 | +import kotlinx.coroutines.delay |
| 4 | +import kotlin.math.pow |
| 5 | +import kotlin.math.roundToInt |
| 6 | +import kotlin.time.Duration.Companion.seconds |
| 7 | +import kotlin.time.ExperimentalTime |
| 8 | + |
| 9 | + |
| 10 | +/* |
| 11 | +Generic class which hold Success and Error Response |
| 12 | +*/ |
| 13 | +sealed class Result<T> { |
| 14 | + data class Success<T>(val data: T) : Result<T>() |
| 15 | + data class Error<T>(val exception: Exception) : Result<T>() |
| 16 | +} |
| 17 | + |
| 18 | +private const val DEFAULT_RETRY_LIMIT = 2 |
| 19 | +private const val DEFAULT_BACKOFF_MULTIPLIER = 2 |
| 20 | +private const val DEFAULT_BACKOFF_BASE = 2 |
| 21 | +private const val DEFAULT_IS_LOG_ENABLED = false |
| 22 | + |
| 23 | + |
| 24 | +/** |
| 25 | + * Generic method to fire given [block] with exponential backOff based on given condition ([retryIf]). |
| 26 | + * |
| 27 | + * Exception from [block]: |
| 28 | + * if during retry, it'll be passed through [retryIf]. |
| 29 | + * if in the last retry, propagated to call site. |
| 30 | + */ |
| 31 | +@OptIn(ExperimentalTime::class) |
| 32 | +suspend fun <T> withExponentialBackOff( |
| 33 | + retryLimit: Int = DEFAULT_RETRY_LIMIT, |
| 34 | + backoffMultiplier: Int = DEFAULT_BACKOFF_MULTIPLIER, |
| 35 | + backOffBase: Int = DEFAULT_BACKOFF_BASE, |
| 36 | + isLogEnabled: Boolean = DEFAULT_IS_LOG_ENABLED, |
| 37 | + retryIf: (retryResult: Result<T>, retryCount: Int) -> Boolean, |
| 38 | + block: suspend (retryCount: Int, previousException: Exception?) -> T |
| 39 | +): T { |
| 40 | + var retryCount = 0 |
| 41 | + var result: Result<T> |
| 42 | + var previousException: Exception? = null |
| 43 | + do { |
| 44 | + if (retryCount != 0) { |
| 45 | + // API failed. backoff needed |
| 46 | + val temp = (backOffBase * backoffMultiplier.toDouble().pow(retryCount)) / 2 |
| 47 | + val jitter = (0..temp.roundToInt()).random() // randomness to avoid parallel retry |
| 48 | + val sleep = (temp + jitter).toLong().seconds |
| 49 | + if (isLogEnabled) { |
| 50 | + println("Sleeping for $sleep (temp: $temp + jitter: $jitter) - Retry: $retryCount/$retryLimit") |
| 51 | + } |
| 52 | + delay(sleep) |
| 53 | + } |
| 54 | + // If there's any exception happened while executing the block, we'll pass it to retryIf block |
| 55 | + // to decide if we should retry |
| 56 | + result = try { |
| 57 | + Result.Success(block(retryCount, previousException)) |
| 58 | + } catch (e: Exception) { |
| 59 | + previousException = e |
| 60 | + Result.Error(e) |
| 61 | + } |
| 62 | + retryCount++ |
| 63 | + } while (retryIf(result, retryCount)) |
| 64 | + |
| 65 | + // final result |
| 66 | + return when (result) { |
| 67 | + is Result.Error -> throw result.exception |
| 68 | + is Result.Success -> result.data |
| 69 | + } |
| 70 | +} |
0 commit comments