TP-54316 | Retry Mechanism System (#9363)

Co-authored-by: Shivam Goyal <shivam.goyal@navi.com>
This commit is contained in:
Ujjwal Kumar
2024-01-17 00:24:23 +05:30
committed by GitHub
parent f299649b55
commit 9df03bec00
3 changed files with 256 additions and 0 deletions

View File

@@ -83,6 +83,8 @@ dependencies {
testImplementation libs.junit
testImplementation libs.kotlinx.coroutines.test
testImplementation libs.mockk
}

View File

@@ -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 <T> 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
}
}

View File

@@ -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)
}
}