From a812c405b3f168a17b0a7e84619d8a8c88b34d51 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Thu, 19 Jan 2023 22:42:28 +0100 Subject: [PATCH 1/3] Use Duration in Schedule --- .../kotlin/arrow/fx/resilience/Schedule.kt | 141 ++++++++++-------- .../kotlin/arrow/fx/resilience/flow.kt | 2 +- .../arrow/fx/resilience/ScheduleTest.kt | 32 ++-- 3 files changed, 93 insertions(+), 82 deletions(-) diff --git a/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/Schedule.kt b/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/Schedule.kt index a62332ecbeb..d9de50b9e59 100644 --- a/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/Schedule.kt +++ b/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/Schedule.kt @@ -12,8 +12,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import kotlin.coroutines.coroutineContext import kotlin.jvm.JvmName -import kotlin.math.max -import kotlin.math.min import kotlin.math.pow import kotlin.math.roundToInt import kotlin.random.Random @@ -23,6 +21,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.retry +import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.DurationUnit.NANOSECONDS @@ -268,24 +267,24 @@ public sealed class Schedule { /** * Combines with another schedule by combining the result and the delay of the [Decision] with the [zipContinue], [zipDuration] and a [zip] functions */ - @ExperimentalTime - public fun combine( + public abstract fun combine( other: Schedule, zipContinue: (cont: Boolean, otherCont: Boolean) -> Boolean, zipDuration: (duration: Duration, otherDuration: Duration) -> Duration, zip: (Output, B) -> C - ): Schedule = - combineNanos(other, zipContinue, { a, b -> zipDuration(a.nanoseconds, b.nanoseconds).toDouble(NANOSECONDS) }, zip) + ): Schedule /** * Combines with another schedule by combining the result and the delay of the [Decision] with the functions [zipContinue], [zipDuration] and a [zip] function */ - public abstract fun combineNanos( + @ExperimentalTime + public fun combineNanos( other: Schedule, zipContinue: (cont: Boolean, otherCont: Boolean) -> Boolean, zipDuration: (duration: Double, otherDuration: Double) -> Double, zip: (Output, B) -> C - ): Schedule + ): Schedule = + combine(other, zipContinue, { a, b -> zipDuration(a.toDouble(NANOSECONDS), b.toDouble(NANOSECONDS)).nanoseconds }, zip) /** * Always retries a schedule regardless of the decision made prior to invoking this method. @@ -301,11 +300,11 @@ public sealed class Schedule { * Changes the delay of a resulting [Decision] based on the [Output] and the produced delay. * */ - @ExperimentalTime - public fun modify(f: suspend (Output, Duration) -> Duration): Schedule = - modifyNanos { output, d -> f(output, d.nanoseconds).toDouble(NANOSECONDS) } + public abstract fun modify(f: suspend (Output, Duration) -> Duration): Schedule - public abstract fun modifyNanos(f: suspend (Output, Double) -> Double): Schedule + @ExperimentalTime + public fun modifyNanos(f: suspend (Output, Double) -> Double): Schedule = + modify { output, d -> f(output, d.toDouble(NANOSECONDS)).nanoseconds } /** * Runs an effectful handler on every input. Does not alter the decision. @@ -388,13 +387,13 @@ public sealed class Schedule { * Combines two schedules. Continues only when both continue and chooses the maximum delay. */ public infix fun and(other: Schedule): Schedule> = - combineNanos(other, { a, b -> a && b }, { a, b -> max(a, b) }, ::Pair) + combine(other, { a, b -> a && b }, { a, b -> a max b }, ::Pair) /** * Combines two schedules. Continues if one continues and chooses the minimum delay. */ public infix fun or(other: Schedule): Schedule> = - combineNanos(other, { a, b -> a || b }, { a, b -> min(a, b) }, ::Pair) + combine(other, { a, b -> a || b }, { a, b -> a min b }, ::Pair) /** * Combines two schedules with [and] but throws away the left schedule's result. @@ -408,13 +407,14 @@ public sealed class Schedule { public infix fun zipLeft(other: Schedule): Schedule = (this and other).map(Pair::first) - @ExperimentalTime public fun delay(f: suspend (duration: Duration) -> Duration): Schedule = modify { _, duration -> f(duration) } + @ExperimentalTime public fun delayedNanos(f: suspend (duration: Double) -> Double): Schedule = modifyNanos { _, duration -> f(duration) } + @ExperimentalTime public fun jittered(genRand: suspend () -> Double): Schedule = modifyNanos { _, duration -> val n = genRand.invoke() @@ -422,7 +422,6 @@ public sealed class Schedule { } @JvmName("jitteredDuration") - @ExperimentalTime public fun jittered(genRand: suspend () -> Duration): Schedule = modify { _, duration -> val n = genRand.invoke() @@ -435,6 +434,7 @@ public sealed class Schedule { * By requiring Kotlin's [Random] as a parameter, this function is deterministic and testable. * The result returned by [Random.nextDouble] between 0.0 and 1.0 is multiplied with the current duration. */ + @ExperimentalTime public fun jittered(random: Random = Random.Default): Schedule = jittered(suspend { random.nextDouble(0.0, 1.0) }) @@ -474,7 +474,7 @@ public sealed class Schedule { val step = update(a, state) if (!step.cont) return Either.Right(step.finish.value()) else { - delay((step.delayInNanos / 1_000_000).toLong()) + delay(step.duration) // Set state before looping again last = { step.finish.value() } @@ -504,7 +504,7 @@ public sealed class Schedule { emit(Either.Right(step.finish.value())) loop = false } else { - delay((step.delayInNanos / 1_000_000).toLong()) + delay(step.duration) val output = step.finish.value() // Set state before looping again and emit Output emit(Either.Right(output)) @@ -533,14 +533,14 @@ public sealed class Schedule { } } - override fun combineNanos( + override fun combine( other: Schedule, zipContinue: (cont: Boolean, otherCont: Boolean) -> Boolean, - zipDuration: (duration: Double, otherDuration: Double) -> Double, + zipDuration: (duration: Duration, otherDuration: Duration) -> Duration, zip: (Output, B) -> C ): Schedule = (other as ScheduleImpl).let { o -> ScheduleImpl(suspend { Pair(initialState.invoke(), o.initialState.invoke()) }) { i, s: Pair -> - update(i, s.first).combineNanos(o.update(i, s.second), zipContinue, zipDuration, zip) + update(i, s.first).combine(o.update(i, s.second), zipContinue, zipDuration, zip) } } @@ -581,12 +581,12 @@ public sealed class Schedule { ) } - override fun modifyNanos(f: suspend (output: Output, duration: Double) -> Double): Schedule = + override fun modify(f: suspend (output: Output, duration: Duration) -> Duration): Schedule = updated { update -> { a: Input, s: State -> val step = update(a, s) - val d = f(step.finish.value(), step.delayInNanos) - step.copy(delayInNanos = d) + val d = f(step.finish.value(), step.duration) + step.copy(duration = d) } } @@ -617,7 +617,7 @@ public sealed class Schedule { ScheduleImpl(suspend { Pair(initialState.invoke(), other.initialState.invoke()) }) { i, s -> val dec1 = update(i, s.first) val dec2 = other.update(dec1.finish.value(), s.second) - dec1.combineNanos(dec2, { a, b -> a && b }, { a, b -> a + b }, { _, b -> b }) + dec1.combine(dec2, { a, b -> a && b }, { a, b -> a + b }, { _, b -> b }) } } @@ -627,7 +627,7 @@ public sealed class Schedule { ScheduleImpl(suspend { Pair(initialState.invoke(), other.initialState.invoke()) }) { i, s -> val dec1 = update(i.first, s.first) val dec2 = other.update(i.second, s.second) - dec1.combineNanos(dec2, { a, b -> a && b }, { a, b -> max(a, b) }, f) + dec1.combine(dec2, { a, b -> a && b }, { a, b -> a max b }, f) } } @@ -683,20 +683,27 @@ public sealed class Schedule { */ public data class Decision( val cont: Boolean, - val delayInNanos: Double, + val duration: Duration, val state: A, val finish: Eval ) { + public constructor( + cont: Boolean, + delayInNanos: Double, + state: A, + finish: Eval + ) : this(cont, delayInNanos.nanoseconds, state, finish) + @ExperimentalTime - val duration: Duration - get() = delayInNanos.nanoseconds + val delayInNanos: Double + get() = duration.toDouble(NANOSECONDS) public operator fun not(): Decision = copy(cont = !cont) public fun bimap(f: (A) -> C, g: (B) -> D): Decision = - Decision(cont, delayInNanos, f(state), finish.map(g)) + Decision(cont, duration, f(state), finish.map(g)) public fun mapLeft(f: (A) -> C): Decision = bimap(f, ::identity) @@ -704,19 +711,15 @@ public sealed class Schedule { public fun map(g: (B) -> D): Decision = bimap(::identity, g) + @ExperimentalTime public fun combineNanos( other: Decision, f: (Boolean, Boolean) -> Boolean, g: (Double, Double) -> Double, zip: (B, D) -> E - ): Decision, E> = Decision( - f(cont, other.cont), - g(delayInNanos, other.delayInNanos), - Pair(state, other.state), - finish.flatMap { first -> other.finish.map { second -> zip(first, second) } } - ) + ): Decision, E> = + combine(other, f, { x, y -> g(x.toDouble(NANOSECONDS), y.toDouble(NANOSECONDS)).nanoseconds }, zip) - @ExperimentalTime public fun combine( other: Decision, f: (Boolean, Boolean) -> Boolean, @@ -724,7 +727,7 @@ public sealed class Schedule { zip: (B, D) -> E ): Decision, E> = Decision( f(cont, other.cont), - g(delayInNanos.nanoseconds, other.delayInNanos.nanoseconds).toDouble(NANOSECONDS), + g(duration, other.duration), Pair(state, other.state), finish.flatMap { first -> other.finish.map { second -> zip(first, second) } } ) @@ -733,31 +736,31 @@ public sealed class Schedule { if (other !is Decision<*, *>) false else cont == other.cont && state == other.state && - delayInNanos == other.delayInNanos && + duration == other.duration && finish.value() == other.finish.value() override fun hashCode(): Int { var result = cont.hashCode() - result = 31 * result + delayInNanos.hashCode() + result = 31 * result + duration.hashCode() result = 31 * result + (state?.hashCode() ?: 0) result = 31 * result + finish.hashCode() return result } public companion object { + @ExperimentalTime public fun cont(d: Double, a: A, b: Eval): Decision = - Decision(true, d, a, b) + cont(d.nanoseconds, a, b) + @ExperimentalTime public fun done(d: Double, a: A, b: Eval): Decision = - Decision(false, d, a, b) + done(d.nanoseconds, a, b) - @ExperimentalTime public fun cont(d: Duration, a: A, b: Eval): Decision = - cont(d.toDouble(NANOSECONDS), a, b) + Decision(true, d, a, b) - @ExperimentalTime public fun done(d: Duration, a: A, b: Eval): Decision = - done(d.toDouble(NANOSECONDS), a, b) + Decision(false, d, a, b) } } @@ -777,7 +780,7 @@ public sealed class Schedule { */ public fun identity(): Schedule = Schedule({ }) { a, s -> - Decision.cont(0.0, s, Eval.now(a)) + Decision.cont(ZERO, s, Eval.now(a)) } /** @@ -794,7 +797,7 @@ public sealed class Schedule { public fun unfoldLazy(c: suspend () -> A, f: suspend (A) -> A): Schedule = Schedule(c) { _: I, acc -> val a = f(acc) - Decision.cont(0.0, a, Eval.now(a)) + Decision.cont(ZERO, a, Eval.now(a)) } /** @@ -814,8 +817,8 @@ public sealed class Schedule { */ public fun recurs(n: Int): Schedule = Schedule(suspend { 0 }) { _: A, acc -> - if (acc < n) Decision.cont(0.0, acc + 1, Eval.now(acc + 1)) - else Decision.done(0.0, acc, Eval.now(acc)) + if (acc < n) Decision.cont(ZERO, acc + 1, Eval.now(acc + 1)) + else Decision.done(ZERO, acc, Eval.now(acc)) } /** @@ -831,7 +834,7 @@ public sealed class Schedule { */ public fun never(): Schedule = Schedule(suspend { awaitCancellation() }) { _, _ -> - Decision(false, 0.0, Unit, Eval.later { throw IllegalArgumentException("Impossible") }) + Decision(false, ZERO, Unit, Eval.later { throw IllegalArgumentException("Impossible") }) } /** @@ -846,6 +849,7 @@ public sealed class Schedule { */ @Suppress("UNCHECKED_CAST") @JvmName("delayedNanos") + @ExperimentalTime public fun delayed(delaySchedule: Schedule): Schedule = (delaySchedule.modifyNanos { a, b -> a + b } as ScheduleImpl) .reconsider { _, dec -> dec.copy(finish = Eval.now(dec.delayInNanos)) } @@ -858,12 +862,11 @@ public sealed class Schedule { * A common use case is to define a unfolding schedule and use the result to change the delay. * For an example see the implementation of [spaced], [linear], [fibonacci] or [exponential] */ - @ExperimentalTime @Suppress("UNCHECKED_CAST") @JvmName("delayedDuration") public fun delayed(delaySchedule: Schedule): Schedule = (delaySchedule.modify { a, b -> a + b } as ScheduleImpl) - .reconsider { _, dec -> dec.copy(finish = Eval.now(dec.delayInNanos.nanoseconds)) } + .reconsider { _, dec -> dec.copy(finish = Eval.now(dec.duration)) } /** * Creates a Schedule which collects all its inputs in a list. @@ -896,25 +899,25 @@ public sealed class Schedule { identity().logOutput(f) @Suppress("UNCHECKED_CAST") + @ExperimentalTime public fun delayInNanos(): Schedule = (forever() as ScheduleImpl).reconsider { _: A, decision -> Decision( cont = decision.cont, - delayInNanos = decision.delayInNanos, + duration = decision.duration, state = decision.state, finish = Eval.now(decision.delayInNanos) ) } - @ExperimentalTime @Suppress("UNCHECKED_CAST") public fun duration(): Schedule = (forever() as ScheduleImpl).reconsider { _: A, decision -> Decision( cont = decision.cont, - delayInNanos = decision.delayInNanos, + duration = decision.duration, state = decision.state, - finish = Eval.now(decision.delayInNanos.nanoseconds) + finish = Eval.now(decision.duration) ) } @@ -922,11 +925,12 @@ public sealed class Schedule { * Creates a Schedule that returns its decisions. */ @Suppress("UNCHECKED_CAST") + @ExperimentalTime public fun decision(): Schedule = (forever() as ScheduleImpl).reconsider { _: A, decision -> Decision( cont = decision.cont, - delayInNanos = decision.delayInNanos, + duration = decision.duration, state = decision.state, finish = Eval.now(decision.cont) ) @@ -937,6 +941,7 @@ public sealed class Schedule { * * @param interval fixed delay in nanoseconds */ + @ExperimentalTime public fun spaced(interval: Double): Schedule = forever().delayedNanos { d -> d + interval } @@ -945,15 +950,15 @@ public sealed class Schedule { * * @param interval fixed delay in [Duration] */ - @ExperimentalTime public fun spaced(interval: Duration): Schedule = - forever().delayedNanos { d -> d + interval.toDouble(NANOSECONDS) } + forever().delay { d -> d + interval } /** * Creates a Schedule that continues with increasing delay by adding the last two delays. * * @param one initial delay in nanoseconds */ + @ExperimentalTime public fun fibonacci(one: Double): Schedule = delayed( unfold>(Pair(0.0, one)) { (del, acc) -> @@ -964,10 +969,9 @@ public sealed class Schedule { /** * Creates a Schedule that continues with increasing delay by adding the last two delays. */ - @ExperimentalTime public fun fibonacci(one: Duration): Schedule = delayed( - unfold>(Pair(0.nanoseconds, one)) { (del, acc) -> + unfold>(Pair(ZERO, one)) { (del, acc) -> Pair(acc, del + acc) }.map { it.first } ) @@ -977,13 +981,13 @@ public sealed class Schedule { * * @param base the base delay in nanoseconds */ + @ExperimentalTime public fun linear(base: Double): Schedule = delayed(forever().map { base * it }) /** * Creates a Schedule which increases its delay linearly, by n * base where n is the number of executions. */ - @ExperimentalTime public fun linear(base: Duration): Schedule = delayed(forever().map { base * it }) @@ -993,6 +997,7 @@ public sealed class Schedule { * * @param base the base delay in nanoseconds */ + @ExperimentalTime public fun exponential(base: Double, factor: Double = 2.0): Schedule = delayed(forever().map { base * factor.pow(it).roundToInt() }) @@ -1000,7 +1005,6 @@ public sealed class Schedule { * Creates a Schedule that increases its delay exponentially with a given factor and base. * Delays can be calculated as [base] * factor ^ n where n is the number of executions. */ - @ExperimentalTime public fun exponential(base: Duration, factor: Double = 2.0): Schedule = delayed(forever().map { base * factor.pow(it).roundToInt() }) } @@ -1028,6 +1032,7 @@ public suspend fun Schedule.retryOrElse( * Also offers a function to handle errors if they are encountered during retrial. */ @Suppress("UNCHECKED_CAST") + public suspend fun Schedule.retryOrElseEither( fa: suspend () -> A, orElse: suspend (Throwable, B) -> C @@ -1045,8 +1050,14 @@ public suspend fun Schedule.retryOrElseEither( dec = update(e, state) state = dec.state - if (dec.cont) delay((dec.delayInNanos / 1_000_000).toLong()) + if (dec.cont) delay(dec.duration) else return Either.Left(orElse(e.nonFatalOrThrow(), dec.finish.value())) } } } + +private infix fun Duration.max(other: Duration): Duration = + if (this >= other) this else other + +private infix fun Duration.min(other: Duration): Duration = + if (this <= other) this else other diff --git a/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/flow.kt b/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/flow.kt index 01102b817c1..708cfdb63b3 100644 --- a/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/flow.kt +++ b/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/flow.kt @@ -44,7 +44,7 @@ public fun Flow.retry(schedule: Schedule): Flow = flo state = dec.state if (dec.cont) { - delay((dec.delayInNanos / 1_000_000).toLong()) + delay(dec.duration) true } else { false diff --git a/arrow-libs/fx/arrow-fx-resilience/src/commonTest/kotlin/arrow/fx/resilience/ScheduleTest.kt b/arrow-libs/fx/arrow-fx-resilience/src/commonTest/kotlin/arrow/fx/resilience/ScheduleTest.kt index a2bb682edd3..df20803e026 100644 --- a/arrow-libs/fx/arrow-fx-resilience/src/commonTest/kotlin/arrow/fx/resilience/ScheduleTest.kt +++ b/arrow-libs/fx/arrow-fx-resilience/src/commonTest/kotlin/arrow/fx/resilience/ScheduleTest.kt @@ -19,6 +19,8 @@ import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.zip +import kotlin.time.Duration.Companion.ZERO +import kotlin.time.DurationUnit internal data class SideEffect(var counter: Int = 0) { fun increment() { @@ -38,14 +40,14 @@ class ScheduleTest : StringSpec({ "Schedule.identity()" { val dec = Schedule.identity().calculateSchedule1(1) - val expected = Schedule.Decision(true, 0.0, Unit, Eval.now(1)) + val expected = Schedule.Decision(true, ZERO, Unit, Eval.now(1)) dec eqv expected } "Schedule.unfold()" { val dec = Schedule.unfold(0) { it + 1 }.calculateSchedule1(0) - val expected = Schedule.Decision(true, 0.0, 1, Eval.now(1)) + val expected = Schedule.Decision(true, ZERO, 1, Eval.now(1)) dec eqv expected } @@ -73,10 +75,10 @@ class ScheduleTest : StringSpec({ val n = 500 val res = Schedule.recurs(n).calculateSchedule(0, n + 1) - res.dropLast(1).map { it.delayInNanos.nanoseconds } shouldBe res.dropLast(1).map { 0.nanoseconds } + res.dropLast(1).map { it.duration } shouldBe res.dropLast(1).map { ZERO } res.dropLast(1).map { it.cont } shouldBe res.dropLast(1).map { true } - res.last() eqv Schedule.Decision(false, 0.0, n + 1, Eval.now(n + 1)) + res.last() eqv Schedule.Decision(false, ZERO, n + 1, Eval.now(n + 1)) } "Schedule.once() repeats 1 additional time" { @@ -142,7 +144,7 @@ class ScheduleTest : StringSpec({ val res = Schedule.spaced(duration).calculateSchedule(0, 500) res.map { it.cont } shouldBe res.map { true } - res.map { it.delayInNanos.nanoseconds } shouldBe res.map { duration } + res.map { it.duration } shouldBe res.map { duration } } fun secondsToNanos(sec: Int): Double = @@ -153,13 +155,11 @@ class ScheduleTest : StringSpec({ val n = 10 val res = Schedule.fibonacci(i).calculateSchedule(0, n) - val sum = res.fold(0.0) { acc, v -> - acc + v.delayInNanos - } + val sum = res.fold(ZERO) { acc, v -> acc + v.duration } val fib = fibs(i).drop(1).take(n) res.all { it.cont } shouldBe true - sum shouldBe fib.sum() + sum.toDouble(DurationUnit.NANOSECONDS) shouldBe fib.sum() } "Schedule.linear()" { @@ -167,11 +167,11 @@ class ScheduleTest : StringSpec({ val n = 10 val res = Schedule.linear(i).calculateSchedule(0, n) - val sum = res.fold(0.0) { acc, v -> acc + v.delayInNanos } + val sum = res.fold(ZERO) { acc, v -> acc + v.duration } val exp = linear(i).drop(1).take(n) res.all { it.cont } shouldBe true - sum shouldBe exp.sum() + sum.toDouble(DurationUnit.NANOSECONDS) shouldBe exp.sum() } "Schedule.exponential()" { @@ -179,11 +179,11 @@ class ScheduleTest : StringSpec({ val n = 10 val res = Schedule.exponential(i).calculateSchedule(0, n) - val sum = res.fold(0.0) { acc, v -> acc + v.delayInNanos } + val sum = res.fold(ZERO) { acc, v -> acc + v.duration } val expSum = exp(i).drop(1).take(n).sum() res.all { it.cont } shouldBe true - sum shouldBe expSum + sum.toDouble(DurationUnit.NANOSECONDS) shouldBe expSum } "repeat is stack-safe" { @@ -196,7 +196,7 @@ class ScheduleTest : StringSpec({ "repeat" { val stop = RuntimeException("WOOO") - val dec = Schedule.Decision(true, 10.0, 0, Eval.now("state")) + val dec = Schedule.Decision(true, 10.nanoseconds, 0, Eval.now("state")) val n = 100 val schedule = Schedule({ 0 }) { _: Unit, _ -> dec } @@ -215,7 +215,7 @@ class ScheduleTest : StringSpec({ "repeatAsFlow" { val stop = RuntimeException("WOOO") - val dec = Schedule.Decision(true, 10.0, 0, Eval.now("state")) + val dec = Schedule.Decision(true, 10.nanoseconds, 0, Eval.now("state")) val n = 100 val schedule = Schedule({ 0 }) { _: Unit, _ -> dec } @@ -356,7 +356,7 @@ private suspend fun checkRepeatAsFlow(schedule: Schedule, expected: @ExperimentalTime private infix fun Schedule.Decision.eqv(other: Schedule.Decision) { require(cont == other.cont) { "Decision#cont: ${this.cont} shouldBe ${other.cont}" } - require(delayInNanos.nanoseconds == other.delayInNanos.nanoseconds) { "Decision#delay.nanoseconds: ${this.delayInNanos.nanoseconds} shouldBe ${other.delayInNanos.nanoseconds}" } + require(duration == other.duration) { "Decision#duration: ${this.duration} shouldBe ${other.duration}" } if (cont) { val lh = finish.value() val rh = other.finish.value() From 438c4ec2507896b1d56965e8511a0afec64f04cd Mon Sep 17 00:00:00 2001 From: serras Date: Thu, 19 Jan 2023 21:47:18 +0000 Subject: [PATCH 2/3] Update API files --- .../api/arrow-fx-resilience.api | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/arrow-libs/fx/arrow-fx-resilience/api/arrow-fx-resilience.api b/arrow-libs/fx/arrow-fx-resilience/api/arrow-fx-resilience.api index 5762d088ca3..83125c5e496 100644 --- a/arrow-libs/fx/arrow-fx-resilience/api/arrow-fx-resilience.api +++ b/arrow-libs/fx/arrow-fx-resilience/api/arrow-fx-resilience.api @@ -68,8 +68,8 @@ public abstract class arrow/fx/resilience/Schedule { public abstract fun check (Lkotlin/jvm/functions/Function3;)Larrow/fx/resilience/Schedule; public abstract fun choose (Larrow/fx/resilience/Schedule;)Larrow/fx/resilience/Schedule; public final fun collect ()Larrow/fx/resilience/Schedule; - public final fun combine (Larrow/fx/resilience/Schedule;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Larrow/fx/resilience/Schedule; - public abstract fun combineNanos (Larrow/fx/resilience/Schedule;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Larrow/fx/resilience/Schedule; + public abstract fun combine (Larrow/fx/resilience/Schedule;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Larrow/fx/resilience/Schedule; + public final fun combineNanos (Larrow/fx/resilience/Schedule;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Larrow/fx/resilience/Schedule; public final fun compose (Larrow/fx/resilience/Schedule;)Larrow/fx/resilience/Schedule; public final fun const (Ljava/lang/Object;)Larrow/fx/resilience/Schedule; public abstract fun contramap (Lkotlin/jvm/functions/Function2;)Larrow/fx/resilience/Schedule; @@ -86,8 +86,8 @@ public abstract class arrow/fx/resilience/Schedule { public abstract fun logInput (Lkotlin/jvm/functions/Function2;)Larrow/fx/resilience/Schedule; public abstract fun logOutput (Lkotlin/jvm/functions/Function2;)Larrow/fx/resilience/Schedule; public abstract fun map (Lkotlin/jvm/functions/Function1;)Larrow/fx/resilience/Schedule; - public final fun modify (Lkotlin/jvm/functions/Function3;)Larrow/fx/resilience/Schedule; - public abstract fun modifyNanos (Lkotlin/jvm/functions/Function3;)Larrow/fx/resilience/Schedule; + public abstract fun modify (Lkotlin/jvm/functions/Function3;)Larrow/fx/resilience/Schedule; + public final fun modifyNanos (Lkotlin/jvm/functions/Function3;)Larrow/fx/resilience/Schedule; public abstract fun not ()Larrow/fx/resilience/Schedule; public final fun or (Larrow/fx/resilience/Schedule;)Larrow/fx/resilience/Schedule; public abstract fun pipe (Larrow/fx/resilience/Schedule;)Larrow/fx/resilience/Schedule; @@ -143,15 +143,16 @@ public final class arrow/fx/resilience/Schedule$Companion { public final class arrow/fx/resilience/Schedule$Decision { public static final field Companion Larrow/fx/resilience/Schedule$Decision$Companion; public fun (ZDLjava/lang/Object;Larrow/core/Eval;)V + public synthetic fun (ZJLjava/lang/Object;Larrow/core/Eval;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun bimap (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Larrow/fx/resilience/Schedule$Decision; public final fun combine (Larrow/fx/resilience/Schedule$Decision;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Larrow/fx/resilience/Schedule$Decision; public final fun combineNanos (Larrow/fx/resilience/Schedule$Decision;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Larrow/fx/resilience/Schedule$Decision; public final fun component1 ()Z - public final fun component2 ()D + public final fun component2-UwyO8pc ()J public final fun component3 ()Ljava/lang/Object; public final fun component4 ()Larrow/core/Eval; - public final fun copy (ZDLjava/lang/Object;Larrow/core/Eval;)Larrow/fx/resilience/Schedule$Decision; - public static synthetic fun copy$default (Larrow/fx/resilience/Schedule$Decision;ZDLjava/lang/Object;Larrow/core/Eval;ILjava/lang/Object;)Larrow/fx/resilience/Schedule$Decision; + public final fun copy-dWUq8MI (ZJLjava/lang/Object;Larrow/core/Eval;)Larrow/fx/resilience/Schedule$Decision; + public static synthetic fun copy-dWUq8MI$default (Larrow/fx/resilience/Schedule$Decision;ZJLjava/lang/Object;Larrow/core/Eval;ILjava/lang/Object;)Larrow/fx/resilience/Schedule$Decision; public fun equals (Ljava/lang/Object;)Z public final fun getCont ()Z public final fun getDelayInNanos ()D From a45ee67e1f5b45d75e538fbd5400fca3d6225c85 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Date: Fri, 20 Jan 2023 14:27:37 +0100 Subject: [PATCH 3/3] Add Deprecated to nanosecond-based APIs --- .../api/arrow-fx-resilience.api | 1 + .../kotlin/arrow/fx/resilience/Schedule.kt | 80 +++++++++++++------ .../arrow/fx/resilience/ScheduleTest.kt | 15 ++-- 3 files changed, 61 insertions(+), 35 deletions(-) diff --git a/arrow-libs/fx/arrow-fx-resilience/api/arrow-fx-resilience.api b/arrow-libs/fx/arrow-fx-resilience/api/arrow-fx-resilience.api index 83125c5e496..7ba4bc76a5d 100644 --- a/arrow-libs/fx/arrow-fx-resilience/api/arrow-fx-resilience.api +++ b/arrow-libs/fx/arrow-fx-resilience/api/arrow-fx-resilience.api @@ -174,6 +174,7 @@ public final class arrow/fx/resilience/Schedule$Decision$Companion { } public final class arrow/fx/resilience/ScheduleKt { + public static final field NanosDeprecation Ljava/lang/String; public static final fun retry (Larrow/fx/resilience/Schedule;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun retryOrElse (Larrow/fx/resilience/Schedule;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun retryOrElseEither (Larrow/fx/resilience/Schedule;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/Schedule.kt b/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/Schedule.kt index d9de50b9e59..59a33fb19d7 100644 --- a/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/Schedule.kt +++ b/arrow-libs/fx/arrow-fx-resilience/src/commonMain/kotlin/arrow/fx/resilience/Schedule.kt @@ -278,6 +278,9 @@ public sealed class Schedule { * Combines with another schedule by combining the result and the delay of the [Decision] with the functions [zipContinue], [zipDuration] and a [zip] function */ @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("combine(other, zipContinue, { a, b -> zipDuration(a.toDouble(DurationUnit.NANOSECONDS), b.toDouble(DurationUnit.NANOSECONDS)).nanoseconds }, zip)", + "kotlin.time.DurationUnit", "kotlin.time.Duration.Companion.nanoseconds")) public fun combineNanos( other: Schedule, zipContinue: (cont: Boolean, otherCont: Boolean) -> Boolean, @@ -303,6 +306,9 @@ public sealed class Schedule { public abstract fun modify(f: suspend (Output, Duration) -> Duration): Schedule @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("modify { output, d -> f(output, d.toDouble(DurationUnit.NANOSECONDS)).nanoseconds }", + "kotlin.time.DurationUnit", "kotlin.time.Duration.Companion.nanoseconds")) public fun modifyNanos(f: suspend (Output, Double) -> Double): Schedule = modify { output, d -> f(output, d.toDouble(NANOSECONDS)).nanoseconds } @@ -411,15 +417,18 @@ public sealed class Schedule { modify { _, duration -> f(duration) } @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("delay { f(it.toDouble(DurationUnit.NANOSECONDS)).nanoseconds }", + "kotlin.time.DurationUnit", "kotlin.time.Duration.Companion.nanoseconds")) public fun delayedNanos(f: suspend (duration: Double) -> Double): Schedule = - modifyNanos { _, duration -> f(duration) } + delay { f(it.toDouble(NANOSECONDS)).nanoseconds } @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("jittered(suspend { genRand().nanoseconds })", + "kotlin.time.Duration.Companion.nanoseconds")) public fun jittered(genRand: suspend () -> Double): Schedule = - modifyNanos { _, duration -> - val n = genRand.invoke() - (duration * n) - } + jittered(suspend { genRand().nanoseconds }) @JvmName("jitteredDuration") public fun jittered(genRand: suspend () -> Duration): Schedule = @@ -436,7 +445,7 @@ public sealed class Schedule { */ @ExperimentalTime public fun jittered(random: Random = Random.Default): Schedule = - jittered(suspend { random.nextDouble(0.0, 1.0) }) + jittered(suspend { random.nextDouble(0.0, 1.0).nanoseconds }) public fun fold(initial: C, f: suspend (acc: C, output: Output) -> C): Schedule = foldLazy(suspend { initial }) { acc, o -> f(acc, o) } @@ -688,6 +697,9 @@ public sealed class Schedule { val finish: Eval ) { + @Deprecated(NanosDeprecation, + ReplaceWith("Decision(cont, delayInNanos.nanoseconds, state, finish)", + "kotlin.time.Duration.Companion.nanoseconds")) public constructor( cont: Boolean, delayInNanos: Double, @@ -696,6 +708,8 @@ public sealed class Schedule { ) : this(cont, delayInNanos.nanoseconds, state, finish) @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("duration.toDouble(DurationUnit.NANOSECONDS)", "kotlin.time.DurationUnit")) val delayInNanos: Double get() = duration.toDouble(NANOSECONDS) @@ -712,6 +726,9 @@ public sealed class Schedule { bimap(::identity, g) @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("combine(other, f, { x, y -> g(x.toDouble(DurationUnit.NANOSECONDS), y.toDouble(DurationUnit.NANOSECONDS)).nanoseconds }, zip)", + "kotlin.time.DurationUnit", "kotlin.time.Duration.Companion.nanoseconds")) public fun combineNanos( other: Decision, f: (Boolean, Boolean) -> Boolean, @@ -749,10 +766,14 @@ public sealed class Schedule { public companion object { @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("cont(d.nanoseconds, a, b)", "kotlin.time.Duration.Companion.nanoseconds")) public fun cont(d: Double, a: A, b: Eval): Decision = cont(d.nanoseconds, a, b) @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("done(d.nanoseconds, a, b)", "kotlin.time.Duration.Companion.nanoseconds")) public fun done(d: Double, a: A, b: Eval): Decision = done(d.nanoseconds, a, b) @@ -847,12 +868,13 @@ public sealed class Schedule { * A common use case is to define a unfolding schedule and use the result to change the delay. * For an example see the implementation of [spaced], [linear], [fibonacci] or [exponential] */ - @Suppress("UNCHECKED_CAST") @JvmName("delayedNanos") @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("delayed(delaySchedule.map { it.nanoseconds }).map { it.toDouble(DurationUnit.NANOSECONDS) }", + "kotlin.time.DurationUnit", "kotlin.time.Duration.Companion.nanoseconds")) public fun delayed(delaySchedule: Schedule): Schedule = - (delaySchedule.modifyNanos { a, b -> a + b } as ScheduleImpl) - .reconsider { _, dec -> dec.copy(finish = Eval.now(dec.delayInNanos)) } + delayed(delaySchedule.map { it.nanoseconds }).map { it.toDouble(NANOSECONDS) } /** * Creates a Schedule that uses another Schedule to generate the delay of this schedule. @@ -898,17 +920,12 @@ public sealed class Schedule { public fun logOutput(f: suspend (A) -> Unit): Schedule = identity().logOutput(f) - @Suppress("UNCHECKED_CAST") @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("duration().map { it.toDouble(DurationUnit.NANOSECONDS) }", + "kotlin.time.DurationUnit")) public fun delayInNanos(): Schedule = - (forever() as ScheduleImpl).reconsider { _: A, decision -> - Decision( - cont = decision.cont, - duration = decision.duration, - state = decision.state, - finish = Eval.now(decision.delayInNanos) - ) - } + duration().map { it.toDouble(NANOSECONDS) } @Suppress("UNCHECKED_CAST") public fun duration(): Schedule = @@ -942,8 +959,11 @@ public sealed class Schedule { * @param interval fixed delay in nanoseconds */ @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("spaced(interval.nanoseconds)", + "kotlin.time.Duration.Companion.nanoseconds")) public fun spaced(interval: Double): Schedule = - forever().delayedNanos { d -> d + interval } + spaced(interval.nanoseconds) /** * Creates a Schedule that continues with a fixed delay. @@ -959,12 +979,11 @@ public sealed class Schedule { * @param one initial delay in nanoseconds */ @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("fibonacci(one.nanoseconds).map { it.toDouble(DurationUnit.NANOSECONDS) }", + "kotlin.time.DurationUnit", "kotlin.time.Duration.Companion.nanoseconds")) public fun fibonacci(one: Double): Schedule = - delayed( - unfold>(Pair(0.0, one)) { (del, acc) -> - Pair(acc, del + acc) - }.map { it.first } - ) + fibonacci(one.nanoseconds).map { it.toDouble(NANOSECONDS) } /** * Creates a Schedule that continues with increasing delay by adding the last two delays. @@ -982,8 +1001,11 @@ public sealed class Schedule { * @param base the base delay in nanoseconds */ @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("linear(base.nanoseconds).map { it.toDouble(DurationUnit.NANOSECONDS) }", + "kotlin.time.DurationUnit", "kotlin.time.Duration.Companion.nanoseconds")) public fun linear(base: Double): Schedule = - delayed(forever().map { base * it }) + linear(base.nanoseconds).map { it.toDouble(NANOSECONDS) } /** * Creates a Schedule which increases its delay linearly, by n * base where n is the number of executions. @@ -998,8 +1020,11 @@ public sealed class Schedule { * @param base the base delay in nanoseconds */ @ExperimentalTime + @Deprecated(NanosDeprecation, + ReplaceWith("exponential(base.nanoseconds).map { it.toDouble(DurationUnit.NANOSECONDS) }", + "kotlin.time.DurationUnit", "kotlin.time.Duration.Companion.nanoseconds")) public fun exponential(base: Double, factor: Double = 2.0): Schedule = - delayed(forever().map { base * factor.pow(it).roundToInt() }) + exponential(base.nanoseconds).map { it.toDouble(NANOSECONDS) } /** * Creates a Schedule that increases its delay exponentially with a given factor and base. @@ -1061,3 +1086,6 @@ private infix fun Duration.max(other: Duration): Duration = private infix fun Duration.min(other: Duration): Duration = if (this <= other) this else other + +public const val NanosDeprecation: String = + "Please prefer Duration-based APIs over those based on nanoseconds." diff --git a/arrow-libs/fx/arrow-fx-resilience/src/commonTest/kotlin/arrow/fx/resilience/ScheduleTest.kt b/arrow-libs/fx/arrow-fx-resilience/src/commonTest/kotlin/arrow/fx/resilience/ScheduleTest.kt index df20803e026..59d14e813d8 100644 --- a/arrow-libs/fx/arrow-fx-resilience/src/commonTest/kotlin/arrow/fx/resilience/ScheduleTest.kt +++ b/arrow-libs/fx/arrow-fx-resilience/src/commonTest/kotlin/arrow/fx/resilience/ScheduleTest.kt @@ -151,36 +151,33 @@ class ScheduleTest : StringSpec({ sec * 1_000_000_000.0 "Schedule.fibonacci()" { - val i = secondsToNanos(10) val n = 10 - val res = Schedule.fibonacci(i).calculateSchedule(0, n) + val res = Schedule.fibonacci(10.seconds).calculateSchedule(0, n) val sum = res.fold(ZERO) { acc, v -> acc + v.duration } - val fib = fibs(i).drop(1).take(n) + val fib = fibs(secondsToNanos(10)).drop(1).take(n) res.all { it.cont } shouldBe true sum.toDouble(DurationUnit.NANOSECONDS) shouldBe fib.sum() } "Schedule.linear()" { - val i = secondsToNanos(10) val n = 10 - val res = Schedule.linear(i).calculateSchedule(0, n) + val res = Schedule.linear(10.seconds).calculateSchedule(0, n) val sum = res.fold(ZERO) { acc, v -> acc + v.duration } - val exp = linear(i).drop(1).take(n) + val exp = linear(secondsToNanos(10)).drop(1).take(n) res.all { it.cont } shouldBe true sum.toDouble(DurationUnit.NANOSECONDS) shouldBe exp.sum() } "Schedule.exponential()" { - val i = secondsToNanos(10) val n = 10 - val res = Schedule.exponential(i).calculateSchedule(0, n) + val res = Schedule.exponential(10.seconds).calculateSchedule(0, n) val sum = res.fold(ZERO) { acc, v -> acc + v.duration } - val expSum = exp(i).drop(1).take(n).sum() + val expSum = exp(secondsToNanos(10)).drop(1).take(n).sum() res.all { it.cont } shouldBe true sum.toDouble(DurationUnit.NANOSECONDS) shouldBe expSum