diff --git a/navi-base/build.gradle b/navi-base/build.gradle index fad3b2ad50..8c3b8dbed7 100644 --- a/navi-base/build.gradle +++ b/navi-base/build.gradle @@ -83,6 +83,8 @@ dependencies { testImplementation libs.junit + testImplementation libs.kotlinx.coroutines.test + testImplementation libs.mockk } diff --git a/navi-base/src/main/java/com/navi/base/utils/Retry.kt b/navi-base/src/main/java/com/navi/base/utils/Retry.kt new file mode 100644 index 0000000000..12e294bd27 --- /dev/null +++ b/navi-base/src/main/java/com/navi/base/utils/Retry.kt @@ -0,0 +1,85 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.base.utils + +import kotlin.math.pow +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.delay + +/** + * Linear with retryIntervalInSeconds of 3 secs means at T = 0, 3, 6, 9, 12, ... execute will be + * called + * + * Exponential with retryIntervalInSeconds of 3 secs means 0, 3, 9, 27, 81, ... + */ +enum class RetryStrategy { + LINEAR, + EXPONENTIAL +} + +/** + * For retryCount n means, execute will be called at max n+1 times. 1 time for normal execution & + * then n times for retry + * + * @param retryCount should be >= 0. It says after 1st execution, how many times should we retry + */ +suspend fun retry( + retryCount: Int = 3, + retryIntervalInSeconds: Double = 0.0, + retryStrategy: RetryStrategy = RetryStrategy.LINEAR, + execute: suspend () -> T, + shouldRetry: (T) -> Boolean +): T { + + if (retryCount < 1) throw IllegalArgumentException("retryCount should be >= 0") + + if (retryIntervalInSeconds < 0) + throw IllegalArgumentException("retryIntervalInSeconds should be >= 0") + + // Default execute + var result = execute() + + // If result is success & retry is not needed, return result + if (!shouldRetry(result)) { + return result + } + + // Default execute has failed & retry should start + for (i in 1..retryCount) { + delay( + getDelayDuration( + retryIntervalInSeconds = retryIntervalInSeconds, + retryStrategy = retryStrategy, + retryCount = i + ) + ) + + result = execute() + + // If retry is success, return retryResult + if (!shouldRetry(result)) { + return result + } + } + + // All retry count has exhausted, return the last result + return result +} + +/** This function is made internal only for testing purpose & is not intended to be used directly */ +internal fun getDelayDuration( + retryIntervalInSeconds: Double, + retryStrategy: RetryStrategy, + retryCount: Int +): Duration { + return when (retryStrategy) { + RetryStrategy.LINEAR -> (retryIntervalInSeconds * retryCount).seconds + RetryStrategy.EXPONENTIAL -> (retryIntervalInSeconds.pow(retryCount)).seconds + } +} diff --git a/navi-base/src/test/java/com/navi/base/utils/RetryUtilTest.kt b/navi-base/src/test/java/com/navi/base/utils/RetryUtilTest.kt new file mode 100644 index 0000000000..1509ce9c51 --- /dev/null +++ b/navi-base/src/test/java/com/navi/base/utils/RetryUtilTest.kt @@ -0,0 +1,169 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.base.utils + +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +class RetryUtilTest { + + @Test + fun `linear retry strategy delay duration test`() = runTest { + var retryIntervalInSeconds = 3.0 + + assertEquals(3.seconds, getDelayDuration(retryIntervalInSeconds, RetryStrategy.LINEAR, 1)) + assertEquals(6.seconds, getDelayDuration(retryIntervalInSeconds, RetryStrategy.LINEAR, 2)) + assertEquals(9.seconds, getDelayDuration(retryIntervalInSeconds, RetryStrategy.LINEAR, 3)) + + retryIntervalInSeconds = 0.0 + + assertEquals(0.seconds, getDelayDuration(retryIntervalInSeconds, RetryStrategy.LINEAR, 1)) + assertEquals(0.seconds, getDelayDuration(retryIntervalInSeconds, RetryStrategy.LINEAR, 2)) + assertEquals(0.seconds, getDelayDuration(retryIntervalInSeconds, RetryStrategy.LINEAR, 3)) + + retryIntervalInSeconds = 1.0 + + assertEquals(1.seconds, getDelayDuration(retryIntervalInSeconds, RetryStrategy.LINEAR, 1)) + assertEquals(2.seconds, getDelayDuration(retryIntervalInSeconds, RetryStrategy.LINEAR, 2)) + assertEquals(3.seconds, getDelayDuration(retryIntervalInSeconds, RetryStrategy.LINEAR, 3)) + } + + @Test + fun `exponential retry strategy delay duration test`() = runTest { + var retryIntervalInSeconds = 3.0 + + assertEquals( + 3.seconds, + getDelayDuration(retryIntervalInSeconds, RetryStrategy.EXPONENTIAL, 1) + ) + assertEquals( + 9.seconds, + getDelayDuration(retryIntervalInSeconds, RetryStrategy.EXPONENTIAL, 2) + ) + assertEquals( + 27.seconds, + getDelayDuration(retryIntervalInSeconds, RetryStrategy.EXPONENTIAL, 3) + ) + + retryIntervalInSeconds = 0.0 + + assertEquals( + 0.seconds, + getDelayDuration(retryIntervalInSeconds, RetryStrategy.EXPONENTIAL, 1) + ) + assertEquals( + 0.seconds, + getDelayDuration(retryIntervalInSeconds, RetryStrategy.EXPONENTIAL, 2) + ) + assertEquals( + 0.seconds, + getDelayDuration(retryIntervalInSeconds, RetryStrategy.EXPONENTIAL, 3) + ) + + retryIntervalInSeconds = 1.0 + + assertEquals( + 1.seconds, + getDelayDuration(retryIntervalInSeconds, RetryStrategy.EXPONENTIAL, 1) + ) + assertEquals( + 3.seconds, + getDelayDuration(retryIntervalInSeconds, RetryStrategy.EXPONENTIAL, 2) + ) + assertEquals( + 9.seconds, + getDelayDuration(retryIntervalInSeconds, RetryStrategy.EXPONENTIAL, 3) + ) + } + + @Test + fun `test number of times execute is getting invoked for all execute failing & asking retry with retryCount as 3`() = + runTest { + var executeCount = 0 + val execute = { + executeCount++ + 1 + } + val shouldRetry: (Any) -> Boolean = { true } + + retry( + retryCount = 3, + retryIntervalInSeconds = 3.0, + retryStrategy = RetryStrategy.LINEAR, + execute = execute, + shouldRetry = shouldRetry + ) + + assertEquals(4, executeCount) + } + + @Test + fun `test number of times execute is getting invoked for 1st instance of execute getting success for retryCount as 3`() = + runTest { + var executeCount = 0 + val execute = { + executeCount++ + 1 + } + val shouldRetry: (Any) -> Boolean = { false } + + retry( + retryCount = 3, + retryIntervalInSeconds = 3.0, + retryStrategy = RetryStrategy.LINEAR, + execute = execute, + shouldRetry = shouldRetry + ) + + assertEquals(1, executeCount) + } + + @Test + fun `test number of times execute is getting invoked for 2nd instance of execute getting success for retryCount as 3`() = + runTest { + var executeCount = 0 + val execute = { + executeCount++ + 1 + } + val shouldRetry: (Any) -> Boolean = { executeCount == 1 } + + retry( + retryCount = 3, + retryIntervalInSeconds = 3.0, + retryStrategy = RetryStrategy.LINEAR, + execute = execute, + shouldRetry = shouldRetry + ) + + assertEquals(2, executeCount) + } + + @Test + fun `test number of times execute is getting invoked for 3rd instance of execute getting success for retryCount as 3`() = + runTest { + var executeCount = 0 + val execute = { + executeCount++ + 1 + } + val shouldRetry: (Any) -> Boolean = { executeCount <= 2 } + + retry( + retryCount = 3, + retryIntervalInSeconds = 3.0, + retryStrategy = RetryStrategy.LINEAR, + execute = execute, + shouldRetry = shouldRetry + ) + + assertEquals(3, executeCount) + } +}