Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added retryRaise and retryEither functions #3373

Merged
merged 1 commit into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,11 @@ public final class arrow/resilience/Schedule$Decision$Done : arrow/resilience/Sc

public final class arrow/resilience/ScheduleKt {
public static final fun retry-4AuOtiA (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
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 retryOrElse-aZo8_V4 (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun retryOrElseEither-aZo8_V4 (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;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/common/Platform : java/lang/Enum {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalTypeInference::class)

package arrow.resilience

import arrow.core.Either
Expand All @@ -7,6 +9,9 @@ import arrow.core.identity
import arrow.core.left
import arrow.core.merge
import arrow.core.nonFatalOrThrow
import arrow.core.raise.Raise
import arrow.core.raise.either
import arrow.core.raise.fold
import arrow.core.right
import arrow.core.some
import arrow.resilience.Schedule.Companion.identity
Expand All @@ -17,6 +22,7 @@ import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.retry
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmInline
import kotlin.math.pow
import kotlin.random.Random
Expand Down Expand Up @@ -444,3 +450,54 @@ public suspend fun <Input, Output, A> Schedule<Throwable, Output>.retryOrElseEit
}
}
}

/**
* 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 <Error, Result, Output> Schedule<Error, Output>.retryRaise(
@BuilderInference action: Raise<Error>.() -> Result,
): Either<Error, Result> = 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 <Error, Result, Output> Schedule<Error, Output>.retryEither(
@BuilderInference action: () -> Either<Error, Result>,
): Either<Error, Result> = 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 <Error, Result, Output> Raise<Error>.retry(
schedule: Schedule<Error, Output>,
@BuilderInference action: Raise<Error>.() -> Result,
): Result {
var step: ScheduleStep<Error, Output> = schedule.step

while (true) {
currentCoroutineContext().ensureActive()
fold(
action,
recover = { error ->
when (val decision = step(error)) {
is Continue -> {
if (decision.delay != ZERO) delay(decision.delay)
step = decision.step
}

is Done -> raise(error)
}
},
transform = { result ->
return result
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package arrow.resilience
import arrow.atomic.AtomicLong
import arrow.atomic.updateAndGet
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import arrow.resilience.Schedule.Decision.Continue
import arrow.resilience.Schedule.Decision.Done
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestResult
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withTimeout
Expand All @@ -27,7 +28,6 @@ internal data class SideEffect(var counter: Int = 0) {
}
}

@OptIn(ExperimentalCoroutinesApi::class)
@ExperimentalTime
@Suppress("UNREACHABLE_CODE", "UNUSED_VARIABLE")
class ScheduleTest {
Expand Down Expand Up @@ -280,6 +280,56 @@ class ScheduleTest {

result.fold({ assertEquals(ex, it) }, { fail("The impossible happened") })
}

@Test
fun retryRaiseIsStackSafe(): TestResult = runTest {
val count = AtomicLong(0)
val iterations = stackSafeIteration().toLong()

suspend fun increment() {
count.incrementAndGet()
}

val result = Schedule.recurs<CustomError>(iterations).retryRaise {
increment()
raise(CustomError)
}

assertTrue { result is Either.Left }
assertEquals(iterations + 1, count.get())
}

@Test
fun retryRaiseSucceedsIfErrorIsNotRaised(): TestResult = runTest {
val result = Schedule.recurs<CustomError>(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<CustomError>(iterations).retryEither {
increment()
CustomError.left()
}

assertTrue { result is Either.Left }
assertEquals(iterations + 1, count.get())
}

@Test
fun retryEitherSucceedsIfErrorIsNotRaised(): TestResult = runTest {
val result = Schedule.recurs<CustomError>(0).retryEither { 1.right() }

assertTrue { result is Either.Right && result.value == 1 }
}
}

fun <A, B> Schedule.Decision<A, B>.delay(): Duration? = when (this) {
Expand Down Expand Up @@ -355,3 +405,5 @@ private suspend fun <B> checkRepeat(schedule: Schedule<Long, List<B>>, expected:

assertContentEquals(expected, result)
}

private object CustomError