/
Retry.kt
89 lines (84 loc) · 3.06 KB
/
Retry.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package io.kotest.assertions
import io.kotest.mpp.bestName
import kotlin.reflect.KClass
import kotlin.time.Duration
import kotlin.time.DurationUnit
import io.kotest.common.MonotonicTimeSourceCompat
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
/**
* Retry [f] until it's a success or [maxRetry]/[timeout] is reached
*
* This will treat any Exception as a failure, along with [AssertionError].
*
* Retry delay might increase exponentially if you choose a [multiplier] value. For example, if you want to configure
* 5 [maxRetry], with an initial [delay] of 1s between requests, the delay between requests will increase when you
* choose 2 as your [multiplier]:
* 1 - Failed (wait 1s before retrying)
* 2 - Failed (wait 2s before retrying)
* 3 - Failed (wait 4s before retrying)
* ..
*
* If either timeout or max retries is reached, the execution will be aborted and an exception will be thrown.
*
* */
suspend fun <T> retry(
maxRetry: Int,
timeout: Duration,
delay: Duration = 1.seconds,
multiplier: Int = 1,
f: suspend () -> T
): T = retry(maxRetry, timeout, delay, multiplier, Exception::class, f)
/**
* Retry [f] until it's a success or [maxRetry]/[timeout] is reached
*
* This will treat only [exceptionClass] as a failure, along with [AssertionError].
*
* Retry delay might increase exponentially if you choose a [multiplier] value. For example, if you want to configure
* 5 [maxRetry], with an initial [delay] of 1s between requests, the delay between requests will increase when you
* choose 2 as your [multiplier]:
* 1 - Failed (wait 1s before retrying)
* 2 - Failed (wait 2s before retrying)
* 3 - Failed (wait 4s before retrying)
* ..
*
* If either timeout or max retries is reached, the execution will be aborted and an exception will be thrown.
*
* */
suspend fun <T, E : Throwable> retry(
maxRetry: Int,
timeout: Duration,
delay: Duration = 1.seconds,
multiplier: Int = 1,
exceptionClass: KClass<E>,
f: suspend () -> T
): T {
val mark = MonotonicTimeSourceCompat.markNow()
val end = mark.plus(timeout)
var retrySoFar = 0
var nextAwaitDuration = delay.inWholeMilliseconds
var lastError: Throwable? = null
while (end.hasNotPassedNow() && retrySoFar < maxRetry) {
try {
return f()
} catch (e: Throwable) {
when {
// Not the kind of exceptions we were prepared to tolerate
e::class.simpleName != "AssertionError" &&
e::class != exceptionClass &&
e::class.bestName() != "org.opentest4j.AssertionFailedError" &&
!e::class.bestName().endsWith("AssertionFailedError") -> throw e
}
lastError = e
// else ignore and continue
}
retrySoFar++
delay(nextAwaitDuration)
nextAwaitDuration *= multiplier
}
val underlyingCause = if (lastError == null) "" else "; underlying cause was ${lastError.message}"
throw failure(
"Test failed after ${delay.toLong(DurationUnit.SECONDS)} seconds; attempted $retrySoFar times$underlyingCause",
lastError
)
}