From dec5037bf7d39d6f1b85bd438aa9714fb49cbfd6 Mon Sep 17 00:00:00 2001 From: aleksanderkotbury Date: Fri, 9 Feb 2024 17:14:43 +0100 Subject: [PATCH] added retryRaise and retryEither functions --- .../arrow-resilience/api/arrow-resilience.api | 6 ++ .../kotlin/arrow/resilience/ScheduleEither.kt | 64 +++++++++++++++++ .../arrow/resilience/ScheduleEitherTest.kt | 69 +++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/ScheduleEither.kt create mode 100644 arrow-libs/resilience/arrow-resilience/src/commonTest/kotlin/arrow/resilience/ScheduleEitherTest.kt diff --git a/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.api b/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.api index 14575ad3e72..b772ccd9e32 100644 --- a/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.api +++ b/arrow-libs/resilience/arrow-resilience/api/arrow-resilience.api @@ -1,3 +1,9 @@ +public final class ScheduleEitherKt { + public static final fun retry-YL6hcnA (Larrow/core/raise/Raise;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun retryEither-4AuOtiA (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun retryRaise-4AuOtiA (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class arrow/resilience/CircuitBreaker { public static final field Companion Larrow/resilience/CircuitBreaker$Companion; public synthetic fun (Ljava/util/concurrent/atomic/AtomicReference;JDJLkotlin/time/TimeSource;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/ScheduleEither.kt b/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/ScheduleEither.kt new file mode 100644 index 00000000000..f64b5c37c07 --- /dev/null +++ b/arrow-libs/resilience/arrow-resilience/src/commonMain/kotlin/arrow/resilience/ScheduleEither.kt @@ -0,0 +1,64 @@ +@file:OptIn(ExperimentalTypeInference::class) + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.either +import arrow.core.raise.fold +import arrow.resilience.Schedule +import arrow.resilience.ScheduleStep +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlin.experimental.ExperimentalTypeInference +import kotlin.time.Duration + +/** + * Retries [action] using any [Error] that occurred as the input to the [Schedule]. + * It will return the last [Error] if the [Schedule] is exhausted, and ignores the output of the [Schedule]. + */ +public suspend inline fun Schedule.retryRaise( + @BuilderInference action: Raise.() -> Result, +): Either = either { + retry(this@retryRaise, action) +} + +/** + * Retries [action] using any [Error] that occurred as the input to the [Schedule]. + * It will return the last [Error] if the [Schedule] is exhausted, and ignores the output of the [Schedule]. + */ +public suspend inline fun Schedule.retryEither( + @BuilderInference action: () -> Either, +): Either = retryRaise { + action().bind() +} + +/** + * Retries [action] using any [Error] that occurred as the input to the [Schedule]. + * It will return the last [Error] if the [Schedule] is exhausted, and ignores the output of the [Schedule]. + */ +public suspend inline fun Raise.retry( + schedule: Schedule, + @BuilderInference action: Raise.() -> Result, +): Result { + var step: ScheduleStep = schedule.step + + while (true) { + currentCoroutineContext().ensureActive() + fold( + action, + recover = { error -> + when (val decision = step(error)) { + is Schedule.Decision.Continue -> { + if (decision.delay != Duration.ZERO) delay(decision.delay) + step = decision.step + } + + is Schedule.Decision.Done -> raise(error) + } + }, + transform = { result -> + return result + }, + ) + } +} diff --git a/arrow-libs/resilience/arrow-resilience/src/commonTest/kotlin/arrow/resilience/ScheduleEitherTest.kt b/arrow-libs/resilience/arrow-resilience/src/commonTest/kotlin/arrow/resilience/ScheduleEitherTest.kt new file mode 100644 index 00000000000..3ddbb3a286f --- /dev/null +++ b/arrow-libs/resilience/arrow-resilience/src/commonTest/kotlin/arrow/resilience/ScheduleEitherTest.kt @@ -0,0 +1,69 @@ +package arrow.resilience + +import arrow.atomic.AtomicLong +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.runTest +import retryEither +import retryRaise +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class ScheduleEitherTest { + + @Test + fun retryRaiseIsStackSafe(): TestResult = runTest(timeout = 1.seconds) { + val count = AtomicLong(0) + val iterations = stackSafeIteration().toLong() + + suspend fun increment() { + count.incrementAndGet() + } + + val result = Schedule.recurs(iterations).retryRaise { + increment() + raise(CustomError) + } + + assertTrue { result is Either.Left } + assertEquals(iterations + 1, count.get()) + } + + @Test + fun retryRaiseSucceedsIfErrorIsNotRaised(): TestResult = runTest { + val result = Schedule.recurs(0).retryRaise { 1 } + + assertTrue { result is Either.Right && result.value == 1 } + } + + @Test + fun retryEitherIsStackSafe(): TestResult = runTest { + val count = AtomicLong(0) + val iterations = stackSafeIteration().toLong() + + suspend fun increment() { + count.incrementAndGet() + } + + val result = Schedule.recurs(iterations).retryEither { + increment() + CustomError.left() + } + + assertTrue { result is Either.Left } + assertEquals(iterations + 1, count.get()) + } + + @Test + fun retryEitherSucceedsIfErrorIsNotRaised(): TestResult = runTest { + val result = Schedule.recurs(0).retryEither { 1.right() } + + assertTrue { result is Either.Right && result.value == 1 } + } +} + +private object CustomError