diff --git a/README.md b/README.md index cd1d4ba..126af10 100644 --- a/README.md +++ b/README.md @@ -2,30 +2,75 @@ [![Build](https://github.com/reugn/kotlin-backoff/actions/workflows/build.yml/badge.svg)](https://github.com/reugn/kotlin-backoff/actions/workflows/build.yml) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.reugn/kotlin-backoff/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.reugn/kotlin-backoff/) -A simple Kotlin Exponential Backoff library designed for `kotlinx.coroutines`. +Any I/O resource can be temporarily unavailable and cause requests to fail. +Use this library to catch errors and retry, slowing down according to your chosen strategy. +Add validation rules for the result and error types if needed. -## Getting started -Gradle: +## Installation +`kotlin-backoff` is available on Maven Central. +Add the library as a dependency to your project: ```kotlin dependencies { - implementation("io.github.reugn:kotlin-backoff:") + implementation("io.github.reugn:kotlin-backoff:0.4.0") } ``` -## Examples +## Backoff strategies +Below is a list of backoff strategies implemented. To create your own strategy, implement the `Strategy` interface. + +### Exponential strategy +A strategy in which the next delay interval is calculated using `baseDelayMs * expBase.pow(attempt)` where: +* `baseDelayMs` is the base delay in milliseconds. +* `attempt` is the number of unsuccessful attempts that have been made. +* `expBase` is the exponent base configured for the strategy. + +The specified jitter¹ and scale factor² are applied to the calculated interval. +The delay time cannot exceed the specified maximum delay in milliseconds. + +### Polynomial strategy +A strategy in which the next delay interval is calculated using `baseDelayMs * attempt.pow(exponent)` where: +* `baseDelayMs` is the base delay in milliseconds. +* `attempt` is the number of unsuccessful attempts that have been made. +* `exponent` is the exponent configured for the strategy. + +The specified jitter¹ and scale factor² are applied to the calculated interval. +The delay time cannot exceed the specified maximum delay in milliseconds. + +### Fixed strategy +A strategy that returns the delay time as a fixed value determined by the attempt number. + +### Constant strategy +A simple backoff strategy that constantly returns the same value. +The specified jitter¹ is applied to the interval. No jitter by default. + +--- +¹ Jitter adds a retry randomization factor to reduce resource congestion. +Specify 1.0 for full jitter, 0.0 for no jitter. + +² The delay determined by the backoff strategy is multiplied by the scale factor. By default, the coefficient is 1. + +## Usage example +First, create an instance of `StrategyBackoff`. Then perform the operation, which may fail, using the `retry` or `withRetries` methods. ```kotlin -private val action = suspend { URL("http://worldclockapi.com/api/json/utc/now").readText() } +private suspend fun urlAction(): String = withContext(Dispatchers.IO) { + URL("http://worldclockapi.com/api/json/utc/now").readText() +} @Test -fun urlTest() { - val backoff = StrategyBackoff(Duration.ofMillis(500), { s -> s.isNotEmpty() }, 3, - Strategy.expFullJitter(2), ::nonFatal) - val result = runBlocking { backoff.retry(action) } +fun `remote URL`() { + val backoff = StrategyBackoff( + maxRetries = 3, + strategy = ExponentialStrategy(), + errorValidator = ::nonFatal, + resultValidator = { s -> s.isNotEmpty() }, + ) + val result = runBlocking { backoff.withRetries(::urlAction) } + assert(result.isOk()) - assertEquals(result.retries, 1) + assertEquals(result.retries, 0) } ``` -More examples can be found in the test section. +For more examples, see the [tests](./src/test/kotlin/io/github/reugn/kotlin/backoff). ## License Licensed under the [Apache 2.0 License](./LICENSE). diff --git a/build.gradle.kts b/build.gradle.kts index 935c3e5..f124923 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,14 +3,13 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { java `java-library` - kotlin("jvm") version "1.5.10" - kotlin("plugin.serialization") version "1.5.10" + kotlin("jvm") version "1.6.0" + kotlin("plugin.serialization") version "1.6.0" `maven-publish` signing } group = "io.github.reugn" -version = "0.3.0" repositories { mavenCentral() @@ -22,9 +21,9 @@ java { } dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:1.0-M1-1.4.0-rc") - testImplementation("org.junit.jupiter:junit-jupiter:5.6.0") + testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") } tasks.withType { @@ -44,7 +43,7 @@ publishing { from(components["java"]) pom { name.set(project.name) - description.set("A simple Exponential Backoff library for Kotlin.") + description.set("An exponential backoff library for Kotlin.") url.set("https://github.com/reugn/kotlin-backoff") licenses { license { diff --git a/gradle.properties b/gradle.properties index 29e08e8..4041205 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,2 @@ -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official +version=0.4.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1948b90..e708b1c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ae85a0..84d1f85 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Tue Feb 11 15:11:05 IST 2020 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index cccdd3d..4f906e0 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -109,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -138,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index e95643d..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/Backoff.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/Backoff.kt index 241dfb0..a5d9bac 100644 --- a/src/main/kotlin/io/github/reugn/kotlin/backoff/Backoff.kt +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/Backoff.kt @@ -1,8 +1,17 @@ package io.github.reugn.kotlin.backoff -import io.github.reugn.kotlin.backoff.utils.Result +import io.github.reugn.kotlin.backoff.util.Result interface Backoff { - suspend fun retry(f: suspend () -> T): Result + /** + * Retries the specified operation with delays and returns the result. + */ + suspend fun retry(operation: suspend () -> T): Result + + /** + * Executes the specified operation with retries and returns the result. + * The difference from [retry] is that the first time the operation is performed without delay. + */ + suspend fun withRetries(operation: suspend () -> T): Result } diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/StrategyBackoff.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/StrategyBackoff.kt index b750493..215fdcb 100644 --- a/src/main/kotlin/io/github/reugn/kotlin/backoff/StrategyBackoff.kt +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/StrategyBackoff.kt @@ -1,61 +1,93 @@ package io.github.reugn.kotlin.backoff +import io.github.reugn.kotlin.backoff.strategy.ExponentialStrategy import io.github.reugn.kotlin.backoff.strategy.Strategy -import io.github.reugn.kotlin.backoff.utils.* +import io.github.reugn.kotlin.backoff.util.* import kotlinx.coroutines.delay -import java.time.Duration /** - * A Strategy based [Backoff] implementation. + * A [Strategy] based [Backoff] implementation. + * Backoff strategies calculate the delay that should be applied between retries. * - * @param T the type of the retry result. - * @property delayTime the initial delay interval. - * @property success the retry method [Success] determiner. - * @property max the maximum number of retries. + * @param T the return type of operation to be executed with retries. + * @property maxRetries the maximum number of retries. * @property strategy the next delay time calculation [Strategy]. - * @property validate validate and exit the retry loop on an invalid exception. + * See implemented strategies in the io.github.reugn.kotlin.backoff.strategy package. + * @property errorValidator exits the retry loop on an invalid exception. + * @property resultValidator validates the operation result and returns if successful. Continues to + * another retry cycle otherwise. */ class StrategyBackoff( - private val delayTime: Duration, - val success: Success, - private val max: Int = 3, - private val strategy: Strategy = Strategy.expFullJitter(2), - private val validate: (Throwable) -> Boolean = ::nonFatal + private val maxRetries: Int = 3, + private val strategy: Strategy = ExponentialStrategy(), + private val errorValidator: ErrorValidator = ::nonFatal, + private val resultValidator: ResultValidator = ::acceptAny, ) : Backoff, Strategy by strategy { - override suspend fun retry(f: suspend () -> T): Result { - return retryN(f, 1, delayTime.toMillis()) + init { + require(maxRetries > 0) } - private suspend fun retryN(f: suspend () -> T, n: Int, prev: Long): Result { + override suspend fun retry( + operation: suspend () -> T + ): Result { + return retryWithAttempts(operation, 1) + } + + override suspend fun withRetries( + operation: suspend () -> T + ): Result { + return try { + val result = operation() + if (resultValidator(result)) + Ok(result, 0) + else + retryWithAttempts(operation, 1) + } catch (e: Throwable) { + if (errorValidator(e)) + retryWithAttempts(operation, 1) + else + Err(e, 0) + } + } + + private suspend fun retryWithAttempts( + operation: suspend () -> T, + attempt: Int + ): Result { while (true) { return try { - delay(prev) - val res = f() - if (success(res)) - Ok(res, n) + delay(nextDelay(attempt)) + + val result = operation() + if (resultValidator(result)) + Ok(result, attempt) else - checkCondition(f, n + 1, next(prev)) + validateAttempts(operation, attempt + 1) } catch (e: Throwable) { - if (validate(e)) - checkCondition(f, n + 1, next(prev), e) + if (validateThrowable(e)) + validateAttempts(operation, attempt + 1, e) else - Err(RetryException(e), n) + Err(e, attempt) } } } - private suspend fun checkCondition( - f: suspend () -> T, n: Int, next: Long, + private fun validateThrowable(e: Throwable): Boolean { + return e !is RetryException && errorValidator(e) + } + + private suspend fun validateAttempts( + operation: suspend () -> T, + attempt: Int, e: Throwable? = null - ): Result { - return if (n <= max) - retryN(f, n, next) - else { + ): Result { + return if (attempt <= maxRetries) { + retryWithAttempts(operation, attempt) + } else { e?.let { - Err(RetryException(it), max) - } ?: Err(RetryException(), max) + Err(it, maxRetries) + } ?: Err(RetryException("Result validation failed."), maxRetries) } } - } diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ConstantStrategy.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ConstantStrategy.kt index 7824bb2..cf3f618 100644 --- a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ConstantStrategy.kt +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ConstantStrategy.kt @@ -1,6 +1,33 @@ package io.github.reugn.kotlin.backoff.strategy -class ConstantStrategy : Strategy { +/** + * A simple backoff strategy that constantly returns the same value. + * The specified jitter is applied to the interval. + */ +data class ConstantStrategy( - override fun next(previousDelayPeriod: Long): Long = previousDelayPeriod + /** + * The constant delay interval in milliseconds. + */ + private val delayMs: Long = 100L, + + /** + * The relative jitter factor applied to the interval. + * Specify 1.0 for full jitter, 0.0 for no jitter. + * No jitter by default. + */ + private val jitterFactor: Double = 0.0 + +) : Strategy, Jitter { + + init { + require(delayMs > 0) + require(jitterFactor in 0.0..1.0) + } + + override fun nextDelay( + attempt: Int + ): Long { + return withJitter(delayMs, jitterFactor) + } } diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ExponentialFullJitterStrategy.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ExponentialFullJitterStrategy.kt deleted file mode 100644 index 1bafdaf..0000000 --- a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ExponentialFullJitterStrategy.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.reugn.kotlin.backoff.strategy - -import kotlin.random.Random - -class ExponentialFullJitterStrategy(private val base: Int) : Strategy { - - override fun next(previousDelayPeriod: Long): Long { - val next = previousDelayPeriod * base - val jitter = Random.nextInt(0, (next / 2).toInt()) - return next + jitter - } -} diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ExponentialPartialJitterStrategy.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ExponentialPartialJitterStrategy.kt deleted file mode 100644 index 21372f2..0000000 --- a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ExponentialPartialJitterStrategy.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.reugn.kotlin.backoff.strategy - -import kotlin.random.Random - -class ExponentialPartialJitterStrategy(private val base: Int, private val ratio: Double) : Strategy { - - override fun next(previousDelayPeriod: Long): Long { - val next = previousDelayPeriod * base - val jitter = Random.nextInt(0, (previousDelayPeriod / 2).toInt()) - return (next * ratio).toLong() + jitter - } -} diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ExponentialStrategy.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ExponentialStrategy.kt index 0edd582..5e4bff2 100644 --- a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ExponentialStrategy.kt +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/ExponentialStrategy.kt @@ -1,6 +1,63 @@ package io.github.reugn.kotlin.backoff.strategy -class ExponentialStrategy(private val base: Int) : Strategy { +import java.lang.Long.min +import kotlin.math.pow - override fun next(previousDelayPeriod: Long): Long = previousDelayPeriod * base +/** + * A strategy in which the next delay interval is calculated using + * `baseDelayMs * expBase.pow(attempt)` where: + * - baseDelayMs is the base delay in milliseconds. + * - attempt is the number of unsuccessful attempts that have been made. + * - expBase is the exponent base configured for the strategy. + * + * The specified jitter and scale factor are applied to the calculated interval. + * The delay time cannot exceed the specified maximum delay in milliseconds. + */ +data class ExponentialStrategy( + + /** + * The base delay in milliseconds. + */ + private val baseDelayMs: Long = 100L, + + /** + * The maximum delay in milliseconds. + */ + private val maxDelayMs: Long = 10000L, + + /** + * The exponent base. + */ + private val expBase: Int = 2, + + /** + * The relative jitter factor applied to the interval. + * Specify 1.0 for full jitter, 0.0 for no jitter. + */ + private val jitterFactor: Double = 0.1, + + /** + * The scale factor by which to multiply the calculated interval. + */ + private val scaleFactor: Double = 1.0 + +) : Strategy, Jitter { + + init { + require(baseDelayMs > 0) + require(maxDelayMs > 0) + require(expBase > 0) + require(jitterFactor in 0.0..1.0) + require(scaleFactor > 0) + } + + override fun nextDelay( + attempt: Int, + ): Long { + val expInterval = (baseDelayMs * expBase.toDouble().pow(attempt.toDouble())).toLong() + return min( + maxDelayMs, + (withJitter(expInterval, jitterFactor) * scaleFactor).toLong() + ) + } } diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/FixedStrategy.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/FixedStrategy.kt new file mode 100644 index 0000000..b82b2fa --- /dev/null +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/FixedStrategy.kt @@ -0,0 +1,28 @@ +package io.github.reugn.kotlin.backoff.strategy + +import io.github.reugn.kotlin.backoff.util.RetryException + +/** + * A strategy that returns the delay time as a fixed value determined by the attempt number. + */ +data class FixedStrategy( + + /** + * The list of the fixed intervals in milliseconds. + */ + private val intervalsMs: List + +) : Strategy { + + init { + require(intervalsMs.isNotEmpty()) + } + + override fun nextDelay(attempt: Int): Long { + try { + return intervalsMs.elementAt(attempt - 1) + } catch (e: IndexOutOfBoundsException) { + throw RetryException(e) + } + } +} diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/Jitter.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/Jitter.kt new file mode 100644 index 0000000..a87c3aa --- /dev/null +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/Jitter.kt @@ -0,0 +1,26 @@ +package io.github.reugn.kotlin.backoff.strategy + +import kotlin.random.Random + +/** + * Represents the jitter trait for backoff strategies. + */ +interface Jitter { + + /** + * Applies jitter to the delay interval using the specified relative factor. + */ + fun withJitter(delayInterval: Long, jitterFactor: Double): Long { + require(jitterFactor in 0.0..1.0) + + val from = (delayInterval * (1 - jitterFactor)).toLong() + if (from == delayInterval) { + return delayInterval + } + + return Random.nextLong( + from, + delayInterval + ) + } +} diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/PolynomialStrategy.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/PolynomialStrategy.kt new file mode 100644 index 0000000..f3ce892 --- /dev/null +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/PolynomialStrategy.kt @@ -0,0 +1,63 @@ +package io.github.reugn.kotlin.backoff.strategy + +import java.lang.Long.min +import kotlin.math.pow + +/** + * A strategy in which the next delay interval is calculated using + * `baseDelayMs * attempt.pow(exponent)` where: + * - baseDelayMs is the base delay in milliseconds. + * - attempt is the number of unsuccessful attempts that have been made. + * - exponent is the exponent configured for the strategy. + * + * The specified jitter and scale factor are applied to the calculated interval. + * The delay time cannot exceed the specified maximum delay in milliseconds. + */ +data class PolynomialStrategy( + + /** + * The base delay in milliseconds. + */ + private val baseDelayMs: Long = 100L, + + /** + * The maximum delay in milliseconds. + */ + private val maxDelayMs: Long = 10000L, + + /** + * The exponent. + */ + private val exponent: Int = 2, + + /** + * The relative jitter factor applied to the interval. + * Specify 1.0 for full jitter, 0.0 for no jitter. + */ + private val jitterFactor: Double = 0.1, + + /** + * The scale factor by which to multiply the calculated interval. + */ + private val scaleFactor: Double = 1.0 + +) : Strategy, Jitter { + + init { + require(baseDelayMs > 0) + require(maxDelayMs > 0) + require(exponent > 0) + require(jitterFactor in 0.0..1.0) + require(scaleFactor > 0) + } + + override fun nextDelay( + attempt: Int + ): Long { + val polInterval = (baseDelayMs * attempt.toDouble().pow(exponent.toDouble())).toLong() + return min( + maxDelayMs, + (withJitter(polInterval, jitterFactor) * scaleFactor).toLong() + ) + } +} diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/Strategy.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/Strategy.kt index b0d08a3..7b48316 100644 --- a/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/Strategy.kt +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/strategy/Strategy.kt @@ -1,33 +1,12 @@ package io.github.reugn.kotlin.backoff.strategy +/** + * This is the base interface type for all backoff strategies. + */ interface Strategy { /** - * Calculate a delay period for the next retry. + * Calculates and returns a delay interval for the next retry. */ - fun next(previousDelayPeriod: Long): Long - - companion object { - - /** - * A [ConstantStrategy] factory method. - */ - fun constant(): ConstantStrategy = ConstantStrategy() - - /** - * An [ExponentialFullJitterStrategy] factory method. - */ - fun expFullJitter(base: Int): ExponentialFullJitterStrategy = ExponentialFullJitterStrategy(base) - - /** - * An [ExponentialPartialJitterStrategy] factory method. - */ - fun expPartialJitter(base: Int, ratio: Double): ExponentialPartialJitterStrategy = - ExponentialPartialJitterStrategy(base, ratio) - - /** - * An [ExponentialStrategy] factory method. - */ - fun exp(base: Int): ExponentialStrategy = ExponentialStrategy(base) - } + fun nextDelay(attempt: Int): Long } diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/utils/RetryException.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/util/ErrorValidator.kt similarity index 67% rename from src/main/kotlin/io/github/reugn/kotlin/backoff/utils/RetryException.kt rename to src/main/kotlin/io/github/reugn/kotlin/backoff/util/ErrorValidator.kt index 4e5a575..f9cef0c 100644 --- a/src/main/kotlin/io/github/reugn/kotlin/backoff/utils/RetryException.kt +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/util/ErrorValidator.kt @@ -1,11 +1,6 @@ -package io.github.reugn.kotlin.backoff.utils +package io.github.reugn.kotlin.backoff.util -class RetryException : Exception { - - constructor() : super() - - constructor(e: Throwable) : super(e) -} +typealias ErrorValidator = (Throwable) -> Boolean /** * Returns true if the provided Throwable is to be considered non-fatal, diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/utils/Result.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/util/Result.kt similarity index 52% rename from src/main/kotlin/io/github/reugn/kotlin/backoff/utils/Result.kt rename to src/main/kotlin/io/github/reugn/kotlin/backoff/util/Result.kt index b1ae2c0..e94c871 100644 --- a/src/main/kotlin/io/github/reugn/kotlin/backoff/utils/Result.kt +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/util/Result.kt @@ -1,4 +1,4 @@ -package io.github.reugn.kotlin.backoff.utils +package io.github.reugn.kotlin.backoff.util import kotlinx.serialization.Serializable @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable * Result is a type that represents either success [Ok] or failure [Err]. */ @Serializable -sealed class Result { +sealed class Result { abstract val retries: Int @@ -16,20 +16,28 @@ sealed class Result { } /** - * The [Result] that contains the success value. + * The [Result] that contains a success value. */ @Serializable -data class Ok(val value: T, override val retries: Int) : Result() { +data class Ok( + val value: T, + override val retries: Int +) : Result() { + override fun isOk(): Boolean = true override fun isErr(): Boolean = false } /** - * The [Result] that contains the error value. + * The [Result] that contains an error value. */ @Serializable -data class Err(val value: E, override val retries: Int) : Result() { +data class Err( + val value: E, + override val retries: Int +) : Result() { + override fun isOk(): Boolean = false override fun isErr(): Boolean = true diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/util/ResultValidator.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/util/ResultValidator.kt new file mode 100644 index 0000000..9b49e41 --- /dev/null +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/util/ResultValidator.kt @@ -0,0 +1,11 @@ +package io.github.reugn.kotlin.backoff.util + +typealias ResultValidator = (T) -> Boolean + +/** + * Returns true for any result. + */ +@Suppress("UNUSED_PARAMETER") +fun acceptAny(result: T): Boolean { + return true +} diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/util/RetryException.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/util/RetryException.kt new file mode 100644 index 0000000..17f6c7e --- /dev/null +++ b/src/main/kotlin/io/github/reugn/kotlin/backoff/util/RetryException.kt @@ -0,0 +1,11 @@ +package io.github.reugn.kotlin.backoff.util + +/** + * Thrown to indicate that a retry has failed. + */ +class RetryException : Exception { + + constructor(message: String) : super(message) + + constructor(e: Throwable) : super(e) +} diff --git a/src/main/kotlin/io/github/reugn/kotlin/backoff/utils/Success.kt b/src/main/kotlin/io/github/reugn/kotlin/backoff/utils/Success.kt deleted file mode 100644 index 8b6ca82..0000000 --- a/src/main/kotlin/io/github/reugn/kotlin/backoff/utils/Success.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.reugn.kotlin.backoff.utils - -typealias Success = (T) -> Boolean - -/** - * Succeed for any condition. - */ -val forall: (Any) -> Boolean = { _ -> true } diff --git a/src/test/kotlin/io/github/reugn/kotlin/backoff/ConstantStrategyTest.kt b/src/test/kotlin/io/github/reugn/kotlin/backoff/ConstantStrategyTest.kt new file mode 100644 index 0000000..b190920 --- /dev/null +++ b/src/test/kotlin/io/github/reugn/kotlin/backoff/ConstantStrategyTest.kt @@ -0,0 +1,82 @@ +package io.github.reugn.kotlin.backoff + +import io.github.reugn.kotlin.backoff.strategy.ConstantStrategy +import io.github.reugn.kotlin.backoff.util.Err +import io.github.reugn.kotlin.backoff.util.Ok +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.fail +import kotlin.system.measureTimeMillis + +class ConstantStrategyTest { + + @Test + fun `Constant strategy`() { + val backoff = StrategyBackoff( + strategy = ConstantStrategy(), + resultValidator = { i -> i == 1 }, + ) + val t = measureTimeMillis { + when (val v = runBlocking { backoff.retry { 1 } }) { + is Ok -> { + assertEquals(1, v.value) + assertEquals(1, v.retries) + } + else -> { + fail("Retry failed") + } + } + } + assert(t < 200) + } + + @Test + fun `Constant strategy next interval`() { + val strategy = ConstantStrategy() + var interval = 0L + for (attempt in 1..4) { + interval = strategy.nextDelay(attempt) + } + assertEquals(100, interval) + } + + @Test + fun `Constant strategy failure`() { + val backoff = StrategyBackoff( + strategy = ConstantStrategy(), + ) + val t = measureTimeMillis { + when (val v = runBlocking { backoff.retry { throw Exception() } }) { + is Err -> { + assert(v.value is Exception) + assertEquals(3, v.retries) + } + else -> { + fail("Retry succeed") + } + } + } + assert(t < 450) + } + + @Test + fun `Constant strategy with jitter`() { + val backoff = StrategyBackoff( + strategy = ConstantStrategy(jitterFactor = 0.5), + resultValidator = { i -> i == 1 }, + ) + val t = measureTimeMillis { + when (val v = runBlocking { backoff.retry { 1 } }) { + is Ok -> { + assertEquals(1, v.value) + assertEquals(1, v.retries) + } + else -> { + fail("Retry failed") + } + } + } + assert(t < 150) + } +} diff --git a/src/test/kotlin/io/github/reugn/kotlin/backoff/ExponentialStrategyTest.kt b/src/test/kotlin/io/github/reugn/kotlin/backoff/ExponentialStrategyTest.kt new file mode 100644 index 0000000..4267474 --- /dev/null +++ b/src/test/kotlin/io/github/reugn/kotlin/backoff/ExponentialStrategyTest.kt @@ -0,0 +1,97 @@ +package io.github.reugn.kotlin.backoff + +import io.github.reugn.kotlin.backoff.strategy.ExponentialStrategy +import io.github.reugn.kotlin.backoff.util.Err +import io.github.reugn.kotlin.backoff.util.Ok +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.fail +import kotlin.system.measureTimeMillis + +class ExponentialStrategyTest { + + @Test + fun `Exponential strategy no jitter`() { + val backoff = StrategyBackoff( + strategy = ExponentialStrategy(jitterFactor = 0.0), + resultValidator = { i -> i == 1 }, + ) + val t = measureTimeMillis { + when (val v = runBlocking { backoff.retry { 1 } }) { + is Ok -> { + assertEquals(1, v.value) + assertEquals(1, v.retries) + } + else -> { + fail("Retry failed") + } + } + } + assert(t < 300) + } + + @Test + fun `Exponential strategy next interval`() { + val strategy = ExponentialStrategy(jitterFactor = 0.0) + var interval = 0L + for (attempt in 1..5) { + interval = strategy.nextDelay(attempt) + } + assertEquals(3200, interval) + } + + @Test + fun `Exponential strategy retryable error`() { + val backoff = StrategyBackoff( + maxRetries = 2, + strategy = ExponentialStrategy(), + ) + when (val v = runBlocking { backoff.retry { throw Exception() } }) { + is Err -> { + assert(v.value is Exception) + assertEquals(2, v.retries) + } + else -> { + fail("Retry succeed") + } + } + } + + @Test + fun `Exponential strategy unretryable error`() { + val backoff = StrategyBackoff( + maxRetries = 4, + strategy = ExponentialStrategy(), + ) + when (val v = runBlocking { backoff.retry { throw InterruptedException() } }) { + is Err -> { + assert(v.value is InterruptedException) + assertEquals(1, v.retries) + } + else -> { + fail("Retry succeed") + } + } + } + + @Test + fun `Exponential strategy with jitter`() { + val backoff = StrategyBackoff( + strategy = ExponentialStrategy(jitterFactor = 0.5), + resultValidator = { i -> i == 1 }, + ) + val t = measureTimeMillis { + when (val v = runBlocking { backoff.retry { 1 } }) { + is Ok -> { + assertEquals(1, v.value) + assertEquals(1, v.retries) + } + else -> { + fail("Retry failed") + } + } + } + assert(t < 250) + } +} diff --git a/src/test/kotlin/io/github/reugn/kotlin/backoff/FixedStrategyTest.kt b/src/test/kotlin/io/github/reugn/kotlin/backoff/FixedStrategyTest.kt new file mode 100644 index 0000000..8ad0631 --- /dev/null +++ b/src/test/kotlin/io/github/reugn/kotlin/backoff/FixedStrategyTest.kt @@ -0,0 +1,62 @@ +package io.github.reugn.kotlin.backoff + +import io.github.reugn.kotlin.backoff.strategy.FixedStrategy +import io.github.reugn.kotlin.backoff.util.Err +import io.github.reugn.kotlin.backoff.util.Ok +import io.github.reugn.kotlin.backoff.util.RetryException +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.fail +import kotlin.system.measureTimeMillis + +class FixedStrategyTest { + + @Test + fun `Fixed strategy`() { + val backoff = StrategyBackoff( + strategy = FixedStrategy(listOf(50)), + resultValidator = { i -> i == 1 }, + ) + val t = measureTimeMillis { + when (val v = runBlocking { backoff.retry { 1 } }) { + is Ok -> { + assertEquals(1, v.value) + assertEquals(1, v.retries) + } + else -> { + fail("Retry failed") + } + } + } + assert(t < 130) + } + + @Test + fun `Fixed strategy next interval`() { + val intervalList = listOf(100, 50, 450, 1000) + val strategy = FixedStrategy(intervalList) + for (attempt in 1..4) { + val interval = strategy.nextDelay(attempt) + assertEquals(intervalList.elementAt(attempt - 1), interval) + } + } + + @Test + fun `Fixed strategy internal error`() { + val backoff = StrategyBackoff( + maxRetries = 5, + strategy = FixedStrategy(listOf(50, 100)), + resultValidator = { i -> i == 1 }, + ) + when (val v = runBlocking { backoff.retry { throw Exception() } }) { + is Err -> { + assert(v.value is RetryException) + assertEquals(3, v.retries) + } + else -> { + fail("Retry succeed") + } + } + } +} diff --git a/src/test/kotlin/io/github/reugn/kotlin/backoff/JitterTest.kt b/src/test/kotlin/io/github/reugn/kotlin/backoff/JitterTest.kt new file mode 100644 index 0000000..f69af02 --- /dev/null +++ b/src/test/kotlin/io/github/reugn/kotlin/backoff/JitterTest.kt @@ -0,0 +1,31 @@ +package io.github.reugn.kotlin.backoff + +import io.github.reugn.kotlin.backoff.strategy.Jitter +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class JitterTest { + + private val jitterObject = object : Jitter {} + + @Test + fun `Low jitter bound`() { + assertEquals(1000L, jitterObject.withJitter(1000L, 0.0)) + } + + @Test + fun `High jitter bound`() { + for (i in 1..10) { + val interval = jitterObject.withJitter(1000L, 1.0) + assert(interval in 0L..1000L) + } + } + + @Test + fun `Jitter bound`() { + for (i in 1..10) { + val interval = jitterObject.withJitter(1000L, 0.1) + assert(interval in 900L..1000L) + } + } +} diff --git a/src/test/kotlin/io/github/reugn/kotlin/backoff/PolynomialStrategyTest.kt b/src/test/kotlin/io/github/reugn/kotlin/backoff/PolynomialStrategyTest.kt new file mode 100644 index 0000000..f852a4d --- /dev/null +++ b/src/test/kotlin/io/github/reugn/kotlin/backoff/PolynomialStrategyTest.kt @@ -0,0 +1,97 @@ +package io.github.reugn.kotlin.backoff + +import io.github.reugn.kotlin.backoff.strategy.PolynomialStrategy +import io.github.reugn.kotlin.backoff.util.Err +import io.github.reugn.kotlin.backoff.util.Ok +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.fail +import kotlin.system.measureTimeMillis + +class PolynomialStrategyTest { + + @Test + fun `Polynomial strategy no jitter`() { + val backoff = StrategyBackoff( + strategy = PolynomialStrategy(jitterFactor = 0.0), + resultValidator = { i -> i == 1 }, + ) + val t = measureTimeMillis { + when (val v = runBlocking { backoff.retry { 1 } }) { + is Ok -> { + assertEquals(1, v.value) + assertEquals(1, v.retries) + } + else -> { + fail("Retry failed") + } + } + } + assert(t < 200) + } + + @Test + fun `Polynomial strategy next interval`() { + val strategy = PolynomialStrategy(jitterFactor = 0.0) + var interval = 0L + for (attempt in 1..5) { + interval = strategy.nextDelay(attempt) + } + assertEquals(2500, interval) + } + + @Test + fun `Polynomial strategy retryable error`() { + val backoff = StrategyBackoff( + maxRetries = 2, + strategy = PolynomialStrategy(), + ) + when (val v = runBlocking { backoff.retry { throw Exception() } }) { + is Err -> { + assert(v.value is Exception) + assertEquals(2, v.retries) + } + else -> { + fail("Retry succeed") + } + } + } + + @Test + fun `Polynomial strategy unretryable error`() { + val backoff = StrategyBackoff( + maxRetries = 4, + strategy = PolynomialStrategy(), + ) + when (val v = runBlocking { backoff.retry { throw InterruptedException() } }) { + is Err -> { + assert(v.value is InterruptedException) + assertEquals(1, v.retries) + } + else -> { + fail("Retry succeed") + } + } + } + + @Test + fun `Polynomial strategy with jitter`() { + val backoff = StrategyBackoff( + strategy = PolynomialStrategy(jitterFactor = 0.5), + resultValidator = { i -> i == 1 }, + ) + val t = measureTimeMillis { + when (val v = runBlocking { backoff.retry { 1 } }) { + is Ok -> { + assertEquals(1, v.value) + assertEquals(1, v.retries) + } + else -> { + fail("Retry failed") + } + } + } + assert(t < 150) + } +} diff --git a/src/test/kotlin/io/github/reugn/kotlin/backoff/RemoteURLTest.kt b/src/test/kotlin/io/github/reugn/kotlin/backoff/RemoteURLTest.kt new file mode 100644 index 0000000..6318305 --- /dev/null +++ b/src/test/kotlin/io/github/reugn/kotlin/backoff/RemoteURLTest.kt @@ -0,0 +1,32 @@ +package io.github.reugn.kotlin.backoff + +import io.github.reugn.kotlin.backoff.strategy.ExponentialStrategy +import io.github.reugn.kotlin.backoff.util.nonFatal +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.net.URL + +@Suppress("BlockingMethodInNonBlockingContext") +class RemoteURLTest { + + private suspend fun urlAction(): String = withContext(Dispatchers.IO) { + URL("http://worldclockapi.com/api/json/utc/now").readText() + } + + @Test + fun `remote URL`() { + val backoff = StrategyBackoff( + maxRetries = 3, + strategy = ExponentialStrategy(), + errorValidator = ::nonFatal, + resultValidator = { s -> s.isNotEmpty() }, + ) + val result = runBlocking { backoff.withRetries(::urlAction) } + + assert(result.isOk()) + assertEquals(result.retries, 0) + } +} diff --git a/src/test/kotlin/io/github/reugn/kotlin/backoff/StrategyBackoffTest.kt b/src/test/kotlin/io/github/reugn/kotlin/backoff/StrategyBackoffTest.kt deleted file mode 100644 index d4a2172..0000000 --- a/src/test/kotlin/io/github/reugn/kotlin/backoff/StrategyBackoffTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package io.github.reugn.kotlin.backoff - -import io.github.reugn.kotlin.backoff.strategy.Strategy -import io.github.reugn.kotlin.backoff.utils.Ok -import io.github.reugn.kotlin.backoff.utils.forall -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.fail -import java.time.Duration -import kotlin.system.measureTimeMillis - -class StrategyBackoffTest { - - @Test - fun exponentialDefaultStrategy() { - val backoff = StrategyBackoff(Duration.ofMillis(100), { i -> i == 1 }) - val t = measureTimeMillis { - when (val v = runBlocking { backoff.retry { 1 } }) { - is Ok -> { - assertEquals(v.value, 1) - assertEquals(v.retries, 1) - } - else -> fail("Error returned") - } - } - println("exponentialDefaultStrategy time: $t") - assert(t < 250) - } - - @Test - fun constantStrategy() { - val backoff = StrategyBackoff(Duration.ofMillis(100), forall, 2, Strategy.constant()) - val result = runBlocking { backoff.retry { 1 } } - assert(result.isOk()) - assertEquals(result.retries, 1) - } - - @Test - fun exponentialDefaultStrategyErr() { - val backoff = StrategyBackoff(Duration.ofMillis(100), { i -> i == 1 }) - val result = runBlocking { backoff.retry { 2 } } - assert(result.isErr()) - assertEquals(result.retries, 3) - } - - @Test - fun invalidThrowableErr() { - val backoff = StrategyBackoff(Duration.ofMillis(100), { i -> i == 1 }) - val result = runBlocking { backoff.retry { throw InterruptedException() } } - assert(result.isErr()) - assertEquals(result.retries, 1) - } - - @Test - fun validThrowableErr() { - val backoff = StrategyBackoff(Duration.ofMillis(100), { i -> i == 1 }) - val result = runBlocking { backoff.retry { throw Exception() } } - assert(result.isErr()) - assertEquals(result.retries, 3) - } -} diff --git a/src/test/kotlin/io/github/reugn/kotlin/backoff/URLTest.kt b/src/test/kotlin/io/github/reugn/kotlin/backoff/URLTest.kt deleted file mode 100644 index 71eedb4..0000000 --- a/src/test/kotlin/io/github/reugn/kotlin/backoff/URLTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.github.reugn.kotlin.backoff - -import io.github.reugn.kotlin.backoff.strategy.Strategy -import io.github.reugn.kotlin.backoff.utils.nonFatal -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import java.net.URL -import java.time.Duration - -class URLTest { - - private val action = suspend { URL("http://worldclockapi.com/api/json/utc/now").readText() } - - @Test - fun urlTest() { - val backoff = StrategyBackoff( - Duration.ofMillis(500), { s -> s.isNotEmpty() }, 3, - Strategy.expFullJitter(2), ::nonFatal - ) - val result = runBlocking { backoff.retry(action) } - //println((result as Ok).value) - assert(result.isOk()) - Assertions.assertEquals(result.retries, 1) - } -}