From 0d6ca0c78df316056473da79d556b016796dba35 Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Tue, 8 Oct 2024 11:28:10 -0700 Subject: [PATCH 1/9] data: Schedule --- README.md | 25 +- .../src/main/scala/kyo/Combinators.scala | 27 +- .../test/scala/kyo/EffectCombinatorTest.scala | 22 +- .../shared/src/main/scala/kyo/Retry.scala | 79 +- .../shared/src/test/scala/kyo/RetryTest.scala | 10 +- .../shared/src/main/scala/kyo/Duration.scala | 4 + .../shared/src/main/scala/kyo/Instant.scala | 18 + .../shared/src/main/scala/kyo/Schedule.scala | 307 ++++++++ .../src/test/scala/kyo/DurationSpec.scala | 17 + .../src/test/scala/kyo/InstantTest.scala | 52 ++ .../src/test/scala/kyo/ScheduleTest.scala | 674 ++++++++++++++++++ 11 files changed, 1104 insertions(+), 131 deletions(-) create mode 100644 kyo-data/shared/src/main/scala/kyo/Schedule.scala create mode 100644 kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala diff --git a/README.md b/README.md index 0ecc9b288..210472873 100644 --- a/README.md +++ b/README.md @@ -2038,30 +2038,17 @@ import scala.concurrent.duration.* val unreliableComputation: Int < Abort[Exception] = Abort.catching[Exception](throw new Exception("Temporary failure")) -// Customize retry policy -val customPolicy = Retry.Policy.default - .limit(5) - .exponential(100.millis, maxBackoff = 5.seconds) +// Customize retry schedule +val shedule = + Schedule.exponentialBackoff(initial = 100.millis, factor = 2, maxBackoff = 5.seconds) + .take(5) val a: Int < (Abort[Exception] & Async) = - Retry[Exception](customPolicy)(unreliableComputation) + Retry[Exception](shedule)(unreliableComputation) -// Use a custom policy builder -val b: Int < (Abort[Exception] & Async) = - Retry[Exception] { policy => - policy - .limit(10) - .backoff(attempt => (attempt * 100).millis) - }(unreliableComputation) ``` -The `Retry` effect automatically adds the `Async` effect to handle the backoff delays between retry attempts. The `Policy` class allows for fine-tuning of the retry behavior: - -- `limit`: Sets the maximum number of retry attempts. -- `exponential`: Configures exponential backoff with a starting delay and optional maximum delay. -- `backoff`: Allows for custom backoff strategies based on the attempt number. - -`Retry` will continue attempting the computation until it succeeds, the retry limit is reached, or an unhandled exception is thrown. If all retries fail, the last failure is propagated. +The `Retry` effect automatically adds the `Async` effect to handle the provided `Schedule`. `Retry` will continue attempting the computation until it succeeds, the retry schedule is done, or an unhandled exception is thrown. If all retries fail, the last failure is propagated. ### Queue: Concurrent Queuing diff --git a/kyo-combinators/shared/src/main/scala/kyo/Combinators.scala b/kyo-combinators/shared/src/main/scala/kyo/Combinators.scala index 164883bd1..76c39e6b5 100644 --- a/kyo-combinators/shared/src/main/scala/kyo/Combinators.scala +++ b/kyo-combinators/shared/src/main/scala/kyo/Combinators.scala @@ -72,10 +72,11 @@ extension [A, S](effect: A < S) * @return * A computation that produces the result of the last execution */ - def repeat(policy: Retry.Policy)(using Flat[A], Frame): A < (S & Async) = - Loop.indexed { i => - if i >= policy.limit then effect.map(Loop.done) - else effect.delayed(policy.backoff(i)).as(Loop.continue) + def repeat(schedule: Schedule)(using Flat[A], Frame): A < (S & Async) = + Loop(schedule) { schedule => + val (delay, nextSchedule) = schedule.next + if !delay.isFinite then effect.map(Loop.done) + else effect.delayed(delay).as(Loop.continue(nextSchedule)) } /** Performs this computation repeatedly with a limit. @@ -186,8 +187,8 @@ extension [A, S](effect: A < S) * @return * A computation that produces the result of this computation with Async and Abort[Throwable] effects */ - def retry(policy: Retry.Policy)(using Flat[A], Frame): A < (S & Async & Abort[Throwable]) = - Retry[Throwable](policy)(effect) + def retry(schedule: Schedule)(using Flat[A], Frame): A < (S & Async & Abort[Throwable]) = + Retry[Throwable](schedule)(effect) /** Performs this computation repeatedly with a limit in case of failures. * @@ -197,19 +198,7 @@ extension [A, S](effect: A < S) * A computation that produces the result of this computation with Async and Abort[Throwable] effects */ def retry(n: Int)(using Flat[A], Frame): A < (S & Async & Abort[Throwable]) = - Retry[Throwable](Retry.Policy(_ => Duration.Zero, n))(effect) - - /** Performs this computation repeatedly with a backoff policy and a limit in case of failures. - * - * @param backoff - * The backoff policy to use - * @param limit - * The limit to use - * @return - * A computation that produces the result of this computation with Async and Abort[Throwable] effects - */ - def retry(backoff: Int => Duration, n: Int)(using Flat[A], Frame): A < (S & Async & Abort[Throwable]) = - Retry[Throwable](Retry.Policy(backoff, n))(effect) + Retry[Throwable](Schedule.repeat(n))(effect) /** Performs this computation indefinitely. * diff --git a/kyo-combinators/shared/src/test/scala/kyo/EffectCombinatorTest.scala b/kyo-combinators/shared/src/test/scala/kyo/EffectCombinatorTest.scala index 4db91b2e6..e840d63e8 100644 --- a/kyo-combinators/shared/src/test/scala/kyo/EffectCombinatorTest.scala +++ b/kyo-combinators/shared/src/test/scala/kyo/EffectCombinatorTest.scala @@ -208,9 +208,9 @@ class EffectCombinatorTest extends Test: "repeat with policy" - { "repeat with custom policy" in run { - var count = 0 - val policy = Retry.Policy(_ => Duration.Zero, 3) - val effect = IO { count += 1; count }.repeat(policy) + var count = 0 + val schedule = Schedule.repeat(3) + val effect = IO { count += 1; count }.repeat(schedule) Async.run(effect).map(_.toFuture).map { handled => handled.map { v => assert(v == 4) @@ -321,7 +321,7 @@ class EffectCombinatorTest extends Test: "retry with policy" - { "successful after retries with custom policy" in run { var count = 0 - val policy = Retry.Policy(_ => 10.millis, 3) + val policy = Schedule.fixed(10.millis).take(3) val effect = IO { count += 1 if count < 3 then throw new Exception("Retry") @@ -333,20 +333,6 @@ class EffectCombinatorTest extends Test: } } - "retry with backoff and limit" - { - "successful after retries with exponential backoff" in run { - var count = 0 - val backoff = (i: Int) => Math.pow(2, i).toLong.millis - val effect = IO { - count += 1 - if count < 3 then throw new Exception("Retry") - else count - }.retry(backoff, 3) - Async.run(effect).map(_.toFuture).map { handled => - handled.map(v => assert(v == 3)) - } - } - } } "explicitThrowable" - { diff --git a/kyo-core/shared/src/main/scala/kyo/Retry.scala b/kyo-core/shared/src/main/scala/kyo/Retry.scala index 478a3e160..b7f9c7cc7 100644 --- a/kyo-core/shared/src/main/scala/kyo/Retry.scala +++ b/kyo-core/shared/src/main/scala/kyo/Retry.scala @@ -6,74 +6,12 @@ import scala.util.* /** Provides utilities for retrying operations with customizable policies. */ object Retry: - /** Represents a retry policy with backoff strategy and attempt limit. */ - final case class Policy(backoff: Int => Duration, limit: Int): - - /** Creates an exponential backoff strategy. - * - * @param startBackoff - * The initial backoff duration. - * @param factor - * The multiplier for each subsequent backoff. - * @param maxBackoff - * The maximum backoff duration. - * @return - * A new Policy with exponential backoff. - */ - def exponential( - startBackoff: Duration, - factor: Int = 2, - maxBackoff: Duration = Duration.Infinity - ): Policy = - backoff { i => - (startBackoff * factor * (i + 1)).min(maxBackoff) - } - - /** Sets a custom backoff function. - * - * @param f - * A function that takes the attempt number and returns a Duration. - * @return - * A new Policy with the custom backoff function. - */ - def backoff(f: Int => Duration): Policy = - copy(backoff = f) - - /** Sets the maximum number of retry attempts. - * - * @param v - * The maximum number of attempts. - * @return - * A new Policy with the specified attempt limit. - */ - def limit(v: Int): Policy = - copy(limit = v) - end Policy - - object Policy: - /** The default retry policy with no backoff and 3 attempts. */ - val default = Policy(_ => Duration.Zero, 3) + /** The default retry schedule. */ + val defaultSchedule = Schedule.exponentialBackoff(initial = 100.millis, factor = 2, maxBackoff = 5.seconds).take(3) /** Provides retry operations for a specific error type. */ final class RetryOps[E >: Nothing](dummy: Unit) extends AnyVal: - /** Retries an operation using the specified policy. - * - * @param policy - * The retry policy to use. - * @param v - * The operation to retry. - * @return - * The result of the operation, or an abort if all retries fail. - */ - def apply[A: Flat, S](policy: Policy)(v: => A < S)( - using - SafeClassTag[E], - Tag[E], - Frame - ): A < (Async & Abort[E] & S) = - apply(_ => policy)(v) - /** Retries an operation using a custom policy builder. * * @param builder @@ -83,21 +21,22 @@ object Retry: * @return * The result of the operation, or an abort if all retries fail. */ - def apply[A: Flat, S](builder: Policy => Policy)(v: => A < (Abort[E] & S))( + def apply[A: Flat, S](schedule: Schedule)(v: => A < (Abort[E] & S))( using SafeClassTag[E], Tag[E], Frame ): A < (Async & Abort[E] & S) = - val b = builder(Policy.default) - Loop.indexed { attempt => + Loop(schedule) { schedule => Abort.run[E](v).map(_.fold { r => - if attempt < b.limit then - Async.sleep(b.backoff(attempt)).andThen { - Loop.continue + val (delay, nextSchedule) = schedule.next + if delay.isFinite then + Async.sleep(delay).andThen { + Loop.continue(nextSchedule) } else Abort.get(r) + end if }(Loop.done(_))) } end apply diff --git a/kyo-core/shared/src/test/scala/kyo/RetryTest.scala b/kyo-core/shared/src/test/scala/kyo/RetryTest.scala index efef7e4fb..5824af6fb 100644 --- a/kyo-core/shared/src/test/scala/kyo/RetryTest.scala +++ b/kyo-core/shared/src/test/scala/kyo/RetryTest.scala @@ -7,7 +7,7 @@ class RetryTest extends Test: "no retries" - { "ok" in run { var calls = 0 - Retry[Any](_.limit(0)) { + Retry[Any](Schedule.never) { calls += 1 42 }.map { v => @@ -17,7 +17,7 @@ class RetryTest extends Test: "nok" in run { var calls = 0 Abort.run[Exception] { - Retry[Exception](_.limit(0)) { + Retry[Exception](Schedule.never) { calls += 1 throw ex } @@ -30,7 +30,7 @@ class RetryTest extends Test: "retries" - { "ok" in run { var calls = 0 - Retry[Any](_.limit(3)) { + Retry[Any](Schedule.repeat(3)) { calls += 1 42 }.map { v => @@ -40,7 +40,7 @@ class RetryTest extends Test: "nok" in run { var calls = 0 Abort.run[Exception] { - Retry[Exception](_.limit(3)) { + Retry[Exception](Schedule.repeat(3)) { calls += 1 throw ex } @@ -54,7 +54,7 @@ class RetryTest extends Test: var calls = 0 val start = java.lang.System.currentTimeMillis() Abort.run[Exception] { - Retry[Exception](_.limit(4).exponential(1.milli)) { + Retry[Exception](Schedule.exponentialBackoff(1.milli, 2.0, Duration.Infinity).take(4)) { calls += 1 throw ex } diff --git a/kyo-data/shared/src/main/scala/kyo/Duration.scala b/kyo-data/shared/src/main/scala/kyo/Duration.scala index d6c653aab..c16a33ba0 100644 --- a/kyo-data/shared/src/main/scala/kyo/Duration.scala +++ b/kyo-data/shared/src/main/scala/kyo/Duration.scala @@ -131,6 +131,10 @@ object Duration: val sum: Long = self.toLong + that.toLong if sum >= 0 then sum else Duration.Infinity + inline infix def -(that: Duration): Duration = + val diff: Long = self.toLong - that.toLong + if diff > 0 then diff else Duration.Zero + inline infix def *(factor: Double): Duration = if factor <= 0 || self.toLong <= 0L then Duration.Zero else if factor <= Long.MaxValue / self.toLong.toDouble then Math.round(self.toLong.toDouble * factor) diff --git a/kyo-data/shared/src/main/scala/kyo/Instant.scala b/kyo-data/shared/src/main/scala/kyo/Instant.scala index 9b13171bf..e7434aae4 100644 --- a/kyo-data/shared/src/main/scala/kyo/Instant.scala +++ b/kyo-data/shared/src/main/scala/kyo/Instant.scala @@ -138,6 +138,24 @@ object Instant: def truncatedTo(unit: Duration.Units & Duration.Truncatable): Instant = instant.truncatedTo(unit.chronoUnit) + /** Returns the minimum of this Instant and another. + * + * @param other + * The other Instant to compare with. + * @return + * The earlier of the two Instants. + */ + def min(other: Instant): Instant = if instant.isBefore(other) then instant else other + + /** Returns the maximum of this Instant and another. + * + * @param other + * The other Instant to compare with. + * @return + * The later of the two Instants. + */ + def max(other: Instant): Instant = if instant.isAfter(other) then instant else other + /** Converts this Instant to a human-readable ISO-8601 formatted string. * * @return diff --git a/kyo-data/shared/src/main/scala/kyo/Schedule.scala b/kyo-data/shared/src/main/scala/kyo/Schedule.scala new file mode 100644 index 000000000..7257e1b35 --- /dev/null +++ b/kyo-data/shared/src/main/scala/kyo/Schedule.scala @@ -0,0 +1,307 @@ +package kyo + +import Schedule.internal.* +import kyo.Duration +import kyo.Instant + +/** An immutable, composable scheduling policy. + * + * Schedule provides various combinators for creating complex scheduling policies. It can be used to define retry policies, periodic tasks, + * or any other time-based scheduling logic. + */ +sealed abstract class Schedule derives CanEqual: + + /** Returns the next delay and the updated schedule. + * + * @return + * a tuple containing the next delay duration and the updated schedule + */ + def next: (Duration, Schedule) + + /** Combines this schedule with another, taking the maximum delay of both. + * + * @param that + * the schedule to combine with + * @return + * a new schedule that produces the maximum delay of both schedules + */ + def max(that: Schedule): Schedule = + this match + case Never => this + case Done | Immediate => that + case _ => + that match + case Never => that + case Done | Immediate => this + case _ => Max(this, that) + + /** Combines this schedule with another, taking the minimum delay of both. + * + * @param that + * the schedule to combine with + * @return + * a new schedule that produces the minimum delay of both schedules + */ + def min(that: Schedule): Schedule = + this match + case Never => that + case Done | Immediate => this + case _ => + that match + case Never => this + case Done | Immediate => that + case _ => Min(this, that) + + /** Limits the number of repetitions of this schedule. + * + * @param n + * the maximum number of repetitions + * @return + * a new schedule that stops after n repetitions + */ + def take(n: Int): Schedule = + if n <= 0 then Schedule.done + else + this match + case Never | Done => this + case _ => Take(this, n) + + /** Chains this schedule with another, running the second after the first completes. + * + * @param that + * the schedule to run after this one + * @return + * a new schedule that runs this schedule followed by the other + */ + def andThen(that: Schedule): Schedule = + this match + case Never => Never + case Done => that + case _ => AndThen(this, that) + + /** Repeats this schedule a specified number of times. + * + * @param n + * the number of times to repeat + * @return + * a new schedule that repeats this schedule n times + */ + def repeat(n: Int): Schedule = + if n <= 0 then Schedule.done + else if n == 1 then this + else + this match + case Never | Done => this + case _ => Repeat(this, n) + + /** Limits the total duration of this schedule. + * + * @param maxDuration + * the maximum total duration + * @return + * a new schedule that stops after the specified duration + */ + def maxDuration(maxDuration: Duration): Schedule = + this match + case Never | Done => this + case _ => Limit(this, maxDuration) + + /** Repeats this schedule indefinitely. + * + * @return + * a new schedule that repeats this schedule forever + */ + def forever: Schedule = + this match + case Never | Done => this + case _ => Forever(this) + + /** Adds a fixed delay before each iteration of this schedule. + * + * @param duration + * the delay to add + * @return + * a new schedule with the added delay + */ + def delay(duration: Duration): Schedule = + this match + case Never | Done => this + case _ => Delay(this, duration) + +end Schedule + +object Schedule: + + /** A schedule that completes once immediately. */ + val immediate: Schedule = Immediate + + /** A schedule that never completes. */ + val never: Schedule = Never + + /** A schedule that is already done. */ + val done: Schedule = Done + + /** Creates a schedule that delays for a fixed duration. + * + * @param duration + * the delay duration + * @return + * a new schedule with the specified delay + */ + def delay(duration: Duration): Schedule = + immediate.delay(duration) + + /** Creates a schedule that immediately repeats a specified number of times. + * + * @param n + * the number of repetitions + * @return + * a new schedule that repeats n times + */ + def repeat(n: Int): Schedule = + immediate.repeat(n) + + /** Creates a schedule with a fixed interval between iterations. + * + * @param interval + * the fixed interval + * @return + * a new schedule with the specified fixed interval + */ + def fixed(interval: Duration): Schedule = Fixed(interval) + + /** Creates a schedule with linearly increasing intervals. + * + * @param base + * the initial interval + * @return + * a new schedule with linearly increasing intervals + */ + def linear(base: Duration): Schedule = Linear(base) + + /** Creates a schedule with intervals following the Fibonacci sequence. + * + * @param a + * the first interval + * @param b + * the second interval + * @return + * a new schedule with Fibonacci sequence intervals + */ + def fibonacci(a: Duration, b: Duration): Schedule = Fibonacci(a, b) + + /** Creates a schedule with exponentially increasing intervals. + * + * @param initial + * the initial interval + * @param factor + * the factor by which to increase the interval + * @return + * a new schedule with exponentially increasing intervals + */ + def exponential(initial: Duration, factor: Double): Schedule = Exponential(initial, factor) + + /** Creates a schedule with exponential backoff and a maximum delay. + * + * @param initial + * the initial interval + * @param factor + * the factor by which to increase the interval + * @param maxBackoff + * the maximum delay allowed + * @return + * a new schedule with exponential backoff and a maximum delay + */ + def exponentialBackoff(initial: Duration, factor: Double, maxBackoff: Duration): Schedule = + ExponentialBackoff(initial, factor, maxBackoff) + + private[kyo] object internal: + + case object Immediate extends Schedule: + val next: (Duration, Schedule) = (Duration.Zero, Done) + + case object Never extends Schedule: + val next: (Duration, Schedule) = (Duration.Infinity, Never) + + case object Done extends Schedule: + val next: (Duration, Schedule) = (Duration.Infinity, Done) + + case class Fixed(interval: Duration) extends Schedule: + val next: (Duration, Schedule) = (interval, this) + + case class Exponential(initial: Duration, factor: Double) extends Schedule: + def next: (Duration, Schedule) = (initial, Exponential(initial * factor, factor)) + + case class Fibonacci(a: Duration, b: Duration) extends Schedule: + def next: (Duration, Schedule) = (a, Fibonacci(b, a + b)) + + case class ExponentialBackoff(initial: Duration, factor: Double, maxBackoff: Duration) extends Schedule: + def next: (Duration, Schedule) = + val nextDelay = initial.min(maxBackoff) + (nextDelay, exponentialBackoff(nextDelay * factor, factor, maxBackoff)) + end ExponentialBackoff + + case class Linear(base: Duration) extends Schedule: + def next: (Duration, Schedule) = (base, linear(base + base)) + + case class Max(a: Schedule, b: Schedule) extends Schedule: + def next: (Duration, Schedule) = + val (d1, s1) = a.next + val (d2, s2) = b.next + (d1.max(d2), s1.max(s2)) + end next + end Max + + case class Min(a: Schedule, b: Schedule) extends Schedule: + def next: (Duration, Schedule) = + val (d1, s1) = a.next + val (d2, s2) = b.next + (d1.min(d2), s1.min(s2)) + end next + end Min + + case class Take(schedule: Schedule, remaining: Int) extends Schedule: + def next: (Duration, Schedule) = + val (d, s) = schedule.next + (d, s.take(remaining - 1)) + end Take + + case class AndThen(a: Schedule, b: Schedule) extends Schedule: + def next: (Duration, Schedule) = + val (d, s) = a.next + (d, s.andThen(b)) + end AndThen + + case class Limit(schedule: Schedule, duration: Duration) extends Schedule: + def next: (Duration, Schedule) = + val (d, s) = schedule.next + if d > duration then Done.next + else (d, s.maxDuration(duration - d)) + end next + end Limit + + case class Repeat(schedule: Schedule, remaining: Int) extends Schedule: + def next: (Duration, Schedule) = + if remaining == 1 then schedule.next + else + val (d, s) = schedule.next + (d, s.andThen(schedule.repeat(remaining - 1))) + end next + end Repeat + + case class Forever(schedule: Schedule) extends Schedule: + def next: (Duration, Schedule) = + val (d, s) = schedule.next + (d, s.andThen(schedule.forever)) + end next + end Forever + + case class Delay(schedule: Schedule, duration: Duration) extends Schedule: + def next: (Duration, Schedule) = + val (d, s) = schedule.next + (duration + d, s.delay(duration)) + end next + end Delay + + end internal +end Schedule diff --git a/kyo-data/shared/src/test/scala/kyo/DurationSpec.scala b/kyo-data/shared/src/test/scala/kyo/DurationSpec.scala index d07a36b48..d5fc8445b 100644 --- a/kyo-data/shared/src/test/scala/kyo/DurationSpec.scala +++ b/kyo-data/shared/src/test/scala/kyo/DurationSpec.scala @@ -239,6 +239,23 @@ object DurationSpec extends ZIOSpecDefault: assertTrue((1000.nanos).show == "1.micros") ) } + ), + suite("Duration subtraction")( + test("subtracting smaller from larger") { + assertTrue(5.seconds - 2.seconds == 3.seconds) + }, + test("subtracting larger from smaller") { + assertTrue(2.seconds - 5.seconds == Duration.Zero) + }, + test("subtracting equal durations") { + assertTrue(3.minutes - 3.minutes == Duration.Zero) + }, + test("subtracting from zero") { + assertTrue(Duration.Zero - 1.second == Duration.Zero) + }, + test("subtracting zero") { + assertTrue(10.hours - Duration.Zero == 10.hours) + } ) ) @@ TestAspect.exceptNative end DurationSpec diff --git a/kyo-data/shared/src/test/scala/kyo/InstantTest.scala b/kyo-data/shared/src/test/scala/kyo/InstantTest.scala index 0f79cc779..b7daf6e6a 100644 --- a/kyo-data/shared/src/test/scala/kyo/InstantTest.scala +++ b/kyo-data/shared/src/test/scala/kyo/InstantTest.scala @@ -267,4 +267,56 @@ class InstantTest extends Test: } } + "min" - { + "earlier instant" in { + val instant1 = Instant.Epoch + val instant2 = instant1 + 1000.seconds + assert(instant1.min(instant2) == instant1) + } + + "later instant" in { + val instant1 = Instant.Epoch + val instant2 = instant1 - 1000.seconds + assert(instant1.min(instant2) == instant2) + } + + "equal instants" in { + val instant1 = Instant.Epoch + val instant2 = Instant.Epoch + assert(instant1.min(instant2) == instant1) + } + + "with Min and Max" in { + val instant = Instant.Epoch + assert(instant.min(Instant.Min) == Instant.Min) + assert(instant.min(Instant.Max) == instant) + } + } + + "max" - { + "earlier instant" in { + val instant1 = Instant.Epoch + val instant2 = instant1 + 1000.seconds + assert(instant1.max(instant2) == instant2) + } + + "later instant" in { + val instant1 = Instant.Epoch + val instant2 = instant1 - 1000.seconds + assert(instant1.max(instant2) == instant1) + } + + "equal instants" in { + val instant1 = Instant.Epoch + val instant2 = Instant.Epoch + assert(instant1.max(instant2) == instant1) + } + + "with Min and Max" in { + val instant = Instant.Epoch + assert(instant.max(Instant.Min) == instant) + assert(instant.max(Instant.Max) == Instant.Max) + } + } + end InstantTest diff --git a/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala b/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala new file mode 100644 index 000000000..3d3e649ec --- /dev/null +++ b/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala @@ -0,0 +1,674 @@ +package kyo + +class ScheduleTest extends Test: + + "fixed" - { + "returns correct next duration and same schedule" in { + val interval = 5.seconds + val schedule = Schedule.fixed(interval) + val (next, nextSchedule) = schedule.next + assert(next == interval) + assert(nextSchedule == schedule) + } + + "works with zero interval" in { + val (next, _) = Schedule.fixed(Duration.Zero).next + assert(next == Duration.Zero) + } + } + + "exponential" - { + "increases interval exponentially" in { + val schedule = Schedule.exponential(1.second, 2.0) + val (next1, schedule2) = schedule.next + val (next2, _) = schedule2.next + assert(next1 == 1.second) + assert(next2 == 2.seconds) + } + + "works with factor less than 1" in { + val schedule = Schedule.exponential(1.second, 0.5) + val (next1, schedule2) = schedule.next + val (next2, _) = schedule2.next + assert(next1 == 1.second) + assert(next2 == 500.millis) + } + + "handles very large intervals" in { + val schedule = Schedule.exponential(365.days, 2.0) + val (next1, schedule2) = schedule.next + val (next2, _) = schedule2.next + assert(next1 == 365.days) + assert(next2 == 730.days) + } + + "works with factor of 1" in { + val schedule = Schedule.exponential(1.second, 1.0) + val (next1, schedule2) = schedule.next + val (next2, _) = schedule2.next + assert(next1 == 1.second) + assert(next2 == 1.second) + } + } + + "fibonacci" - { + "follows fibonacci sequence" in { + val schedule = Schedule.fibonacci(1.second, 1.second) + val (next1, schedule2) = schedule.next + val (next2, schedule3) = schedule2.next + val (next3, _) = schedule3.next + assert(next1 == 1.second) + assert(next2 == 1.second) + assert(next3 == 2.seconds) + } + + "works with different initial values" in { + val schedule = Schedule.fibonacci(1.second, 2.seconds) + val (next1, schedule2) = schedule.next + val (next2, schedule3) = schedule2.next + val (next3, _) = schedule3.next + assert(next1 == 1.second) + assert(next2 == 2.seconds) + assert(next3 == 3.seconds) + } + + "works with zero initial values" in { + val schedule = Schedule.fibonacci(Duration.Zero, Duration.Zero) + val (next1, schedule2) = schedule.next + val (next2, schedule3) = schedule2.next + val (next3, _) = schedule3.next + assert(next1 == Duration.Zero) + assert(next2 == Duration.Zero) + assert(next3 == Duration.Zero) + } + } + + "immediate" - { + "returns zero duration" in { + val (next, nextSchedule) = Schedule.immediate.next + assert(next == Duration.Zero) + assert(nextSchedule == Schedule.done) + } + + "always returns never as next schedule" in { + val (_, nextSchedule1) = Schedule.immediate.next + val (_, nextSchedule2) = nextSchedule1.next + assert(nextSchedule1 == Schedule.done) + assert(nextSchedule2 == Schedule.done) + } + } + + "never" - { + "always returns infinite duration" in { + val (next, nextSchedule) = Schedule.never.next + assert(next == Duration.Infinity) + assert(nextSchedule == Schedule.never) + } + } + + "exponentialBackoff" - { + "respects maxDelay" in { + val initial = 1.second + val factor = 2.0 + val maxDelay = 4.seconds + val schedule = Schedule.exponentialBackoff(initial, factor, maxDelay) + val (next1, schedule2) = schedule.next + val (next2, schedule3) = schedule2.next + val (next3, _) = schedule3.next + assert(next1 == 1.second) + assert(next2 == 2.seconds) + assert(next3 == 4.seconds) + } + + "caps at maxDelay" in { + val initial = 1.second + val factor = 2.0 + val maxDelay = 4.seconds + val schedule = Schedule.exponentialBackoff(initial, factor, maxDelay) + var current = schedule + for _ <- 1 to 5 do + val (nextDuration, nextSchedule) = current.next + assert(nextDuration <= maxDelay) + current = nextSchedule + end for + succeed + } + + "works with factor less than 1" in { + val initial = 4.seconds + val factor = 0.5 + val maxDelay = 4.seconds + val schedule = Schedule.exponentialBackoff(initial, factor, maxDelay) + val (next1, schedule2) = schedule.next + val (next2, _) = schedule2.next + assert(next1 == 4.seconds) + assert(next2 == 2.seconds) + } + } + + "repeat" - { + "repeats specified number of times" in { + val schedule = Schedule.repeat(3) + val (next1, schedule2) = schedule.next + val (next2, schedule3) = schedule2.next + val (next3, schedule4) = schedule3.next + val (next4, _) = schedule4.next + assert(next1 == Duration.Zero) + assert(next2 == Duration.Zero) + assert(next3 == Duration.Zero) + assert(next4 == Duration.Infinity) + } + + "works with zero repetitions" in { + val schedule = Schedule.repeat(0) + val (next, _) = schedule.next + assert(next == Duration.Infinity) + } + + "works with finite inner schedule" in { + val innerSchedule = Schedule.fixed(1.second).take(2) + val s = innerSchedule.repeat(3) + val results = List.unfold(s) { schedule => + val (next, newSchedule) = schedule.next + if next == Duration.Infinity then None + else Some((next, newSchedule)) + } + assert(results == List(1.second, 1.second, 1.second, 1.second, 1.second, 1.second)) + } + + "repeats correct number of times with complex inner schedule" in { + val s = Schedule.immediate.andThen(Schedule.fixed(1.second).take(1)).repeat(2) + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, s4) = s3.next + val (next4, s5) = s4.next + val (next5, _) = s5.next + assert(next1 == Duration.Zero) + assert(next2 == 1.second) + assert(next3 == Duration.Zero) + assert(next4 == 1.second) + assert(next5 == Duration.Infinity) + } + + "Schedule.repeat" - { + "repeats immediate schedule specified number of times" in { + val s = Schedule.repeat(3) + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, s4) = s3.next + val (next4, _) = s4.next + + assert(next1 == Duration.Zero) + assert(next2 == Duration.Zero) + assert(next3 == Duration.Zero) + assert(next4 == Duration.Infinity) + } + + "works with zero repetitions" in { + val s = Schedule.repeat(0) + val (next, _) = s.next + + assert(next == Duration.Infinity) + } + + "can be chained with other schedules" in { + val s = Schedule.repeat(2).andThen(Schedule.fixed(1.second)) + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, s4) = s3.next + val (next4, _) = s4.next + + assert(next1 == Duration.Zero) + assert(next2 == Duration.Zero) + assert(next3 == 1.second) + assert(next4 == 1.second) + } + + } + } + + "linear" - { + "increases interval linearly" in { + val base = 1.second + val schedule = Schedule.linear(base) + val (next1, schedule2) = schedule.next + val (next2, schedule3) = schedule2.next + val (next3, _) = schedule3.next + assert(next1 == 1.second) + assert(next2 == 2.seconds) + assert(next3 == 4.seconds) + } + + "works with zero base" in { + val schedule = Schedule.linear(Duration.Zero) + val (next1, schedule2) = schedule.next + val (next2, _) = schedule2.next + assert(next1 == Duration.Zero) + assert(next2 == Duration.Zero) + } + } + + "max" - { + "returns later of two schedules" in { + val s1 = Schedule.fixed(1.second) + val s2 = Schedule.fixed(2.seconds) + val combined = s1.max(s2) + val (next, _) = combined.next + assert(next == 2.seconds) + } + + "handles one schedule being never" in { + val s1 = Schedule.fixed(1.second) + val s2 = Schedule.never + assert(s1.max(s2) == Schedule.never) + } + + "handles both schedules being never" in { + val combined = Schedule.never.max(Schedule.never) + val (next, _) = combined.next + assert(next == Duration.Infinity) + } + } + + "min" - { + "returns earlier of two schedules" in { + val s1 = Schedule.fixed(1.second) + val s2 = Schedule.fixed(2.seconds) + val combined = s1.min(s2) + val (next, _) = combined.next + assert(next == 1.second) + } + + "handles one schedule being immediate" in { + val s1 = Schedule.fixed(1.second) + val s2 = Schedule.immediate + val combined = s1.min(s2) + val (next, _) = combined.next + assert(next == Duration.Zero) + } + + "handles both schedules being immediate" in { + val combined = Schedule.immediate.min(Schedule.immediate) + val (next, _) = combined.next + assert(next == Duration.Zero) + } + } + + "take" - { + "limits number of executions" in { + val s = Schedule.fixed(1.second).take(2) + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, _) = s3.next + assert(next1 == 1.second) + assert(next2 == 1.second) + assert(next3 == Duration.Infinity) + } + + "returns never for non-positive count" in { + val s = Schedule.fixed(1.second).take(0) + assert(s == Schedule.done) + } + } + + "andThen" - { + "switches to second schedule after first completes" in { + val s1 = Schedule.repeat(2) + val s2 = Schedule.fixed(1.second) + val combined = s1.andThen(s2) + val (next1, c2) = combined.next + val (next2, c3) = c2.next + val (next3, _) = c3.next + assert(next1 == Duration.Zero) + assert(next2 == Duration.Zero) + assert(next3 == 1.second) + } + + "works with never as first schedule" in { + val s1 = Schedule.never + val s2 = Schedule.fixed(1.second) + val combined = s1.andThen(s2) + val (next, _) = combined.next + assert(next == Duration.Infinity) + } + + "works with never as second schedule" in { + val s1 = Schedule.immediate + val s2 = Schedule.never + val combined = s1.andThen(s2) + val (next1, c2) = combined.next + val (next2, _) = c2.next + assert(next1 == Duration.Zero) + assert(next2 == Duration.Infinity) + } + + "chains multiple schedules" in { + val s = Schedule.immediate.andThen(Schedule.fixed(1.second).take(1)).andThen(Schedule.fixed(2.seconds).take(1)) + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, s4) = s3.next + val (next4, _) = s4.next + assert(next1 == Duration.Zero) + assert(next2 == 1.second) + assert(next3 == 2.seconds) + assert(next4 == Duration.Infinity) + } + } + + "maxDuration" - { + "stops after specified duration" in { + val s = Schedule.fixed(1.second).maxDuration(2.seconds + 500.millis) + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, _) = s3.next + assert(next1 == 1.second) + assert(next2 == 1.second) + assert(next3 == Duration.Infinity) + } + + "works with zero duration" in { + val s = Schedule.fixed(1.second).maxDuration(Duration.Zero) + val (next, _) = s.next + assert(next == Duration.Infinity) + } + + "works with complex schedule" in { + val s = Schedule.exponential(1.second, 2.0).repeat(5).maxDuration(7.seconds) + val results = List.unfold(s) { schedule => + val (next, newSchedule) = schedule.next + if next == Duration.Infinity then None + else Some((next, newSchedule)) + } + assert(results == List(1.second, 2.seconds, 4.seconds)) + } + + "limits duration correctly with delayed start" in { + val s = Schedule.fixed(2.seconds).take(1).andThen(Schedule.linear(1.second)).maxDuration(5.seconds) + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, s4) = s3.next + val (next4, _) = s4.next + assert(next1 == 2.seconds) + assert(next2 == 1.second) + assert(next3 == 2.seconds) + assert(next4 == Duration.Infinity) + } + } + + "forever" - { + "repeats indefinitely" in { + val s = Schedule.repeat(1).forever + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, _) = s3.next + assert(next1 == Duration.Zero) + assert(next2 == Duration.Zero) + assert(next3 == Duration.Zero) + } + + "works with never schedule" in { + val (next, _) = Schedule.never.forever.next + assert(next == Duration.Infinity) + } + + "works with immediate schedule" in { + val s = Schedule.immediate.forever + val (next1, s2) = s.next + val (next2, _) = s2.next + assert(next1 == Duration.Zero) + assert(next2 == Duration.Zero) + } + + "works with fixed schedule" in { + val s = Schedule.fixed(1.second).forever + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, _) = s3.next + assert(next1 == 1.second) + assert(next2 == 1.second) + assert(next3 == 1.second) + } + + "works with exponential schedule" in { + val s = Schedule.exponential(1.second, 2.0).forever + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, _) = s3.next + assert(next1 == 1.second) + assert(next2 == 2.seconds) + assert(next3 == 4.seconds) + } + + "works with complex schedule" in { + val s = (Schedule.immediate.andThen(Schedule.fixed(1.second).take(1))).forever + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, s4) = s3.next + val (next4, _) = s4.next + assert(next1 == Duration.Zero) + assert(next2 == 1.second) + assert(next3 == Duration.Zero) + assert(next4 == 1.second) + } + } + + "delay" - { + "adds fixed delay to each interval" in { + val original = Schedule.fixed(1.second) + val delayed = original.delay(500.millis) + + val (next1, s2) = delayed.next + val (next2, _) = s2.next + + assert(next1 == 1500.millis) + assert(next2 == 1500.millis) + } + + "works with zero delay" in { + val original = Schedule.fixed(1.second) + val delayed = original.delay(Duration.Zero) + + val (next, _) = delayed.next + + assert(next == 1.second) + } + + "works with immediate schedule" in { + val delayed = Schedule.immediate.delay(1.second) + + val (next1, s2) = delayed.next + val (next2, _) = s2.next + + assert(next1 == 1.second) + assert(next2 == Duration.Infinity) + } + + "works with never schedule" in { + val delayed = Schedule.never.delay(1.second) + + val (next, _) = delayed.next + + assert(next == Duration.Infinity) + } + + "works with complex schedule" in { + val original = Schedule.exponential(1.second, 2.0).take(3) + val delayed = original.delay(500.millis) + + val (next1, s2) = delayed.next + val (next2, s3) = s2.next + val (next3, s4) = s3.next + val (next4, _) = s4.next + + assert(next1 == 1500.millis) + assert(next2 == 2500.millis) + assert(next3 == 4500.millis) + assert(next4 == Duration.Infinity) + } + + "Schedule.delay" - { + "creates a delayed immediate schedule" in { + val s = Schedule.delay(1.second) + val (next1, s2) = s.next + val (next2, _) = s2.next + + assert(next1 == 1.second) + assert(next2 == Duration.Infinity) + } + + "works with zero delay" in { + val s = Schedule.delay(Duration.Zero) + val (next, _) = s.next + + assert(next == Duration.Zero) + } + + "can be chained with other schedules" in { + val s = Schedule.delay(500.millis).andThen(Schedule.fixed(1.second)) + val (next1, s2) = s.next + val (next2, _) = s2.next + + assert(next1 == 500.millis) + assert(next2 == 1.second) + } + + "works in combination with other schedules" in { + val s1 = Schedule.fixed(1.second).take(2) + val s2 = Schedule.exponential(2.seconds, 2.0).take(2) + val combined = s1.andThen(Schedule.delay(3.seconds)).andThen(s2) + + val (next1, c2) = combined.next + val (next2, c3) = c2.next + val (next3, c4) = c3.next + val (next4, c5) = c4.next + val (next5, _) = c5.next + + assert(next1 == 1.second) + assert(next2 == 1.second) + assert(next3 == 3.seconds) + assert(next4 == 2.seconds) + assert(next5 == 4.seconds) + } + } + } + + "complex schedules" - { + "combines max and min schedules" in { + val s1 = Schedule.fixed(1.second) + val s2 = Schedule.fixed(2.seconds) + val s3 = Schedule.fixed(3.seconds) + val combined = s1.max(s2).min(s3) + + val (next1, c2) = combined.next + val (next2, _) = c2.next + + assert(next1 == 2.seconds) + assert(next2 == 2.seconds) + } + + "limits a forever schedule" in { + val s = Schedule.exponential(1.second, 2.0).forever.maxDuration(5.seconds) + + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, _) = s3.next + + assert(next1 == 1.second) + assert(next2 == 2.seconds) + assert(next3 == Duration.Infinity) + } + + "combines repeat with exponential backoff" in { + val s = Schedule.repeat(3).andThen(Schedule.exponentialBackoff(1.second, 2.0, 8.seconds)) + + val (next1, s2) = s.next + val (next2, s3) = s2.next + val (next3, s4) = s3.next + val (next4, s5) = s4.next + val (next5, _) = s5.next + + assert(next1 == Duration.Zero) + assert(next2 == Duration.Zero) + assert(next3 == Duration.Zero) + assert(next4 == 1.second) + assert(next5 == 2.seconds) + } + } + + "schedule reduction and equality" - { + "max reduction" in { + val s1 = Schedule.fixed(1.second) + val s2 = Schedule.fixed(2.seconds) + val s3 = Schedule.never + val s4 = Schedule.immediate + + assert(s1.max(s3) == s3) + assert(s3.max(s1) == s3) + assert(s1.max(s4) == s1) + assert(s4.max(s1) == s1) + assert(s3.max(s4) == s3) + assert(s4.max(s3) == s3) + } + + "min reduction" in { + val s1 = Schedule.fixed(1.second) + val s2 = Schedule.fixed(2.seconds) + val s3 = Schedule.never + val s4 = Schedule.immediate + + assert(s1.min(s3) == s1) + assert(s3.min(s1) == s1) + assert(s1.min(s4) == s4) + assert(s4.min(s1) == s4) + assert(s3.min(s4) == s4) + assert(s4.min(s3) == s4) + } + + "take reduction" in { + val s1 = Schedule.fixed(1.second) + + assert(s1.take(0) == Schedule.done) + assert(Schedule.never.take(3) == Schedule.never) + } + + "andThen reduction" in { + val s1 = Schedule.fixed(1.second) + val s2 = Schedule.fixed(2.seconds) + + assert(Schedule.never.andThen(s1) == Schedule.never) + } + + "maxDuration reduction" in { + val s1 = Schedule.fixed(1.second) + val duration = 5.seconds + + assert(Schedule.never.maxDuration(duration) == Schedule.never) + } + + "forever reduction" in { + val s1 = Schedule.fixed(1.second) + + assert(Schedule.never.forever == Schedule.never) + assert(Schedule.done.forever == Schedule.done) + } + + "correctly compares complex schedules" in { + val s1 = Schedule.exponential(1.second, 2.0).take(3) + val s2 = Schedule.exponential(1.second, 2.0).take(3) + val s3 = Schedule.exponential(1.second, 2.0).take(4) + + assert(s1 == s2) + assert(s1 != s3) + } + + "handles equality with forever schedules" in { + val s1 = Schedule.fixed(1.second).forever + val s2 = Schedule.fixed(1.second).forever + val s3 = Schedule.fixed(2.seconds).forever + + assert(s1 == s2) + assert(s1 != s3) + } + } + +end ScheduleTest From 2aae8802eba315a626da9a28a7ba160bdd2dae21 Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Fri, 11 Oct 2024 16:04:44 -0700 Subject: [PATCH 2/9] introduce schedule.withNext to avoid tuple allocations + Schedule.forever --- .../shared/src/main/scala/kyo/Schedule.scala | 81 +++++++++++-------- .../src/test/scala/kyo/ScheduleTest.scala | 29 +++++++ 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/kyo-data/shared/src/main/scala/kyo/Schedule.scala b/kyo-data/shared/src/main/scala/kyo/Schedule.scala index 7257e1b35..945b8dd94 100644 --- a/kyo-data/shared/src/main/scala/kyo/Schedule.scala +++ b/kyo-data/shared/src/main/scala/kyo/Schedule.scala @@ -16,7 +16,9 @@ sealed abstract class Schedule derives CanEqual: * @return * a tuple containing the next delay duration and the updated schedule */ - def next: (Duration, Schedule) + def next: (Duration, Schedule) = withNext((_, _)) + + def withNext[A](f: (Duration, Schedule) => A): A /** Combines this schedule with another, taking the maximum delay of both. * @@ -141,7 +143,10 @@ object Schedule: /** A schedule that is already done. */ val done: Schedule = Done - /** Creates a schedule that delays for a fixed duration. + /** A schedule that forever repeats immediately. */ + val forever: Schedule = immediate.forever + + /** Creates a schedule that executes once after a fixed duration. * * @param duration * the delay duration @@ -218,89 +223,95 @@ object Schedule: private[kyo] object internal: case object Immediate extends Schedule: - val next: (Duration, Schedule) = (Duration.Zero, Done) + def withNext[A](f: (Duration, Schedule) => A) = + f(Duration.Zero, Done) case object Never extends Schedule: - val next: (Duration, Schedule) = (Duration.Infinity, Never) + def withNext[A](f: (Duration, Schedule) => A) = + f(Duration.Infinity, Never) case object Done extends Schedule: - val next: (Duration, Schedule) = (Duration.Infinity, Done) + override val next = (Duration.Infinity, Done) + def withNext[A](f: (Duration, Schedule) => A) = + f(Duration.Infinity, Done) + end Done case class Fixed(interval: Duration) extends Schedule: - val next: (Duration, Schedule) = (interval, this) + def withNext[A](f: (Duration, Schedule) => A) = + f(interval, this) case class Exponential(initial: Duration, factor: Double) extends Schedule: - def next: (Duration, Schedule) = (initial, Exponential(initial * factor, factor)) + def withNext[A](f: (Duration, Schedule) => A) = + f(initial, Exponential(initial * factor, factor)) case class Fibonacci(a: Duration, b: Duration) extends Schedule: - def next: (Duration, Schedule) = (a, Fibonacci(b, a + b)) + def withNext[A](f: (Duration, Schedule) => A) = + f(a, Fibonacci(b, a + b)) case class ExponentialBackoff(initial: Duration, factor: Double, maxBackoff: Duration) extends Schedule: - def next: (Duration, Schedule) = + def withNext[A](f: (Duration, Schedule) => A) = val nextDelay = initial.min(maxBackoff) - (nextDelay, exponentialBackoff(nextDelay * factor, factor, maxBackoff)) + f(nextDelay, exponentialBackoff(nextDelay * factor, factor, maxBackoff)) end ExponentialBackoff case class Linear(base: Duration) extends Schedule: - def next: (Duration, Schedule) = (base, linear(base + base)) + def withNext[A](f: (Duration, Schedule) => A) = + f(base, linear(base + base)) case class Max(a: Schedule, b: Schedule) extends Schedule: - def next: (Duration, Schedule) = + def withNext[A](f: (Duration, Schedule) => A) = val (d1, s1) = a.next val (d2, s2) = b.next - (d1.max(d2), s1.max(s2)) - end next + f(d1.max(d2), s1.max(s2)) + end withNext end Max case class Min(a: Schedule, b: Schedule) extends Schedule: - def next: (Duration, Schedule) = + def withNext[A](f: (Duration, Schedule) => A) = val (d1, s1) = a.next val (d2, s2) = b.next - (d1.min(d2), s1.min(s2)) - end next + f(d1.min(d2), s1.min(s2)) + end withNext end Min case class Take(schedule: Schedule, remaining: Int) extends Schedule: - def next: (Duration, Schedule) = + def withNext[A](f: (Duration, Schedule) => A) = val (d, s) = schedule.next - (d, s.take(remaining - 1)) + f(d, s.take(remaining - 1)) end Take case class AndThen(a: Schedule, b: Schedule) extends Schedule: - def next: (Duration, Schedule) = + def withNext[A](f: (Duration, Schedule) => A) = val (d, s) = a.next - (d, s.andThen(b)) + f(d, s.andThen(b)) end AndThen case class Limit(schedule: Schedule, duration: Duration) extends Schedule: - def next: (Duration, Schedule) = + def withNext[A](f: (Duration, Schedule) => A) = val (d, s) = schedule.next - if d > duration then Done.next - else (d, s.maxDuration(duration - d)) - end next + if d > duration then Done.withNext(f) + else f(d, s.maxDuration(duration - d)) + end withNext end Limit case class Repeat(schedule: Schedule, remaining: Int) extends Schedule: - def next: (Duration, Schedule) = - if remaining == 1 then schedule.next + def withNext[A](f: (Duration, Schedule) => A) = + if remaining == 1 then schedule.withNext(f) else val (d, s) = schedule.next - (d, s.andThen(schedule.repeat(remaining - 1))) - end next + f(d, s.andThen(schedule.repeat(remaining - 1))) end Repeat case class Forever(schedule: Schedule) extends Schedule: - def next: (Duration, Schedule) = + def withNext[A](f: (Duration, Schedule) => A) = val (d, s) = schedule.next - (d, s.andThen(schedule.forever)) - end next + f(d, s.andThen(schedule.forever)) end Forever case class Delay(schedule: Schedule, duration: Duration) extends Schedule: - def next: (Duration, Schedule) = + def withNext[A](f: (Duration, Schedule) => A) = val (d, s) = schedule.next - (duration + d, s.delay(duration)) - end next + f(duration + d, s.delay(duration)) end Delay end internal diff --git a/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala b/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala index 3d3e649ec..b49be1f38 100644 --- a/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala +++ b/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala @@ -671,4 +671,33 @@ class ScheduleTest extends Test: } } + "withNext" - { + "allows custom processing of next duration and schedule" in { + val schedule = Schedule.fixed(1.second) + val result = schedule.withNext { (duration, nextSchedule) => + (duration * 2, nextSchedule.take(1)) + } + + assert(result == (2.seconds, Schedule.fixed(1.second).take(1))) + } + + "works with immediate schedule" in { + val schedule = Schedule.immediate + val result = schedule.withNext { (duration, nextSchedule) => + (duration, nextSchedule == Schedule.done) + } + + assert(result == (Duration.Zero, true)) + } + + "works with never schedule" in { + val schedule = Schedule.never + val result = schedule.withNext { (duration, nextSchedule) => + (duration == Duration.Infinity, nextSchedule == Schedule.never) + } + + assert(result == (true, true)) + } + } + end ScheduleTest From 76c01874181c8b2119ee176afd03a6c6e59f0306 Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Sun, 13 Oct 2024 22:27:05 -0700 Subject: [PATCH 3/9] make schedule.next return Maybe + remove withNext --- .../shared/src/main/scala/kyo/Schedule.scala | 104 +++--- .../src/test/scala/kyo/ScheduleTest.scala | 339 ++++++++---------- 2 files changed, 187 insertions(+), 256 deletions(-) diff --git a/kyo-data/shared/src/main/scala/kyo/Schedule.scala b/kyo-data/shared/src/main/scala/kyo/Schedule.scala index 945b8dd94..37ff48850 100644 --- a/kyo-data/shared/src/main/scala/kyo/Schedule.scala +++ b/kyo-data/shared/src/main/scala/kyo/Schedule.scala @@ -16,9 +16,7 @@ sealed abstract class Schedule derives CanEqual: * @return * a tuple containing the next delay duration and the updated schedule */ - def next: (Duration, Schedule) = withNext((_, _)) - - def withNext[A](f: (Duration, Schedule) => A): A + def next: Maybe[(Duration, Schedule)] /** Combines this schedule with another, taking the maximum delay of both. * @@ -106,7 +104,7 @@ sealed abstract class Schedule derives CanEqual: def maxDuration(maxDuration: Duration): Schedule = this match case Never | Done => this - case _ => Limit(this, maxDuration) + case _ => MaxDuration(this, maxDuration) /** Repeats this schedule indefinitely. * @@ -223,96 +221,78 @@ object Schedule: private[kyo] object internal: case object Immediate extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - f(Duration.Zero, Done) + val next = Maybe((Duration.Zero, Done)) case object Never extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - f(Duration.Infinity, Never) + def next = Maybe.empty case object Done extends Schedule: - override val next = (Duration.Infinity, Done) - def withNext[A](f: (Duration, Schedule) => A) = - f(Duration.Infinity, Done) - end Done + def next = Maybe.empty case class Fixed(interval: Duration) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - f(interval, this) + val next = Maybe((interval, this)) case class Exponential(initial: Duration, factor: Double) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - f(initial, Exponential(initial * factor, factor)) + def next = Maybe((initial, Exponential(initial * factor, factor))) case class Fibonacci(a: Duration, b: Duration) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - f(a, Fibonacci(b, a + b)) + def next = Maybe((a, Fibonacci(b, a + b))) case class ExponentialBackoff(initial: Duration, factor: Double, maxBackoff: Duration) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = + def next = val nextDelay = initial.min(maxBackoff) - f(nextDelay, exponentialBackoff(nextDelay * factor, factor, maxBackoff)) + Maybe((nextDelay, exponentialBackoff(nextDelay * factor, factor, maxBackoff))) end ExponentialBackoff case class Linear(base: Duration) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - f(base, linear(base + base)) + def next = Maybe((base, linear(base + base))) case class Max(a: Schedule, b: Schedule) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - val (d1, s1) = a.next - val (d2, s2) = b.next - f(d1.max(d2), s1.max(s2)) - end withNext + def next = + for + (d1, s1) <- a.next + (d2, s2) <- b.next + yield (d1.max(d2), s1.max(s2)) end Max case class Min(a: Schedule, b: Schedule) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - val (d1, s1) = a.next - val (d2, s2) = b.next - f(d1.min(d2), s1.min(s2)) - end withNext + def next = + a.next match + case Maybe.Empty => b.next + case n @ Maybe.Defined((d1, s1)) => + b.next match + case Maybe.Empty => n + case Maybe.Defined((d2, s2)) => + Maybe((d1.min(d2), s1.min(s2))) end Min case class Take(schedule: Schedule, remaining: Int) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - val (d, s) = schedule.next - f(d, s.take(remaining - 1)) - end Take + def next = + schedule.next.map((d, s) => (d, s.take(remaining - 1))) case class AndThen(a: Schedule, b: Schedule) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - val (d, s) = a.next - f(d, s.andThen(b)) - end AndThen - - case class Limit(schedule: Schedule, duration: Duration) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - val (d, s) = schedule.next - if d > duration then Done.withNext(f) - else f(d, s.maxDuration(duration - d)) - end withNext - end Limit + def next = + a.next.map((d, s) => (d, s.andThen(b))).orElse(b.next) + + case class MaxDuration(schedule: Schedule, duration: Duration) extends Schedule: + def next = + schedule.next.flatMap { (d, s) => + if d > duration then Maybe.empty + else Maybe((d, s.maxDuration(duration - d))) + } + end MaxDuration case class Repeat(schedule: Schedule, remaining: Int) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - if remaining == 1 then schedule.withNext(f) - else - val (d, s) = schedule.next - f(d, s.andThen(schedule.repeat(remaining - 1))) - end Repeat + def next = + schedule.next.map((d, s) => (d, s.andThen(schedule.repeat(remaining - 1)))) case class Forever(schedule: Schedule) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - val (d, s) = schedule.next - f(d, s.andThen(schedule.forever)) - end Forever + def next = + schedule.next.map((d, s) => (d, s.andThen(this))) case class Delay(schedule: Schedule, duration: Duration) extends Schedule: - def withNext[A](f: (Duration, Schedule) => A) = - val (d, s) = schedule.next - f(duration + d, s.delay(duration)) - end Delay + def next = + schedule.next.map((d, s) => (duration + d, s.delay(duration))) end internal end Schedule diff --git a/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala b/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala index b49be1f38..ad4925f5c 100644 --- a/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala +++ b/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala @@ -6,13 +6,13 @@ class ScheduleTest extends Test: "returns correct next duration and same schedule" in { val interval = 5.seconds val schedule = Schedule.fixed(interval) - val (next, nextSchedule) = schedule.next + val (next, nextSchedule) = schedule.next.get assert(next == interval) assert(nextSchedule == schedule) } "works with zero interval" in { - val (next, _) = Schedule.fixed(Duration.Zero).next + val (next, _) = Schedule.fixed(Duration.Zero).next.get assert(next == Duration.Zero) } } @@ -20,32 +20,32 @@ class ScheduleTest extends Test: "exponential" - { "increases interval exponentially" in { val schedule = Schedule.exponential(1.second, 2.0) - val (next1, schedule2) = schedule.next - val (next2, _) = schedule2.next + val (next1, schedule2) = schedule.next.get + val (next2, _) = schedule2.next.get assert(next1 == 1.second) assert(next2 == 2.seconds) } "works with factor less than 1" in { val schedule = Schedule.exponential(1.second, 0.5) - val (next1, schedule2) = schedule.next - val (next2, _) = schedule2.next + val (next1, schedule2) = schedule.next.get + val (next2, _) = schedule2.next.get assert(next1 == 1.second) assert(next2 == 500.millis) } "handles very large intervals" in { val schedule = Schedule.exponential(365.days, 2.0) - val (next1, schedule2) = schedule.next - val (next2, _) = schedule2.next + val (next1, schedule2) = schedule.next.get + val (next2, _) = schedule2.next.get assert(next1 == 365.days) assert(next2 == 730.days) } "works with factor of 1" in { val schedule = Schedule.exponential(1.second, 1.0) - val (next1, schedule2) = schedule.next - val (next2, _) = schedule2.next + val (next1, schedule2) = schedule.next.get + val (next2, _) = schedule2.next.get assert(next1 == 1.second) assert(next2 == 1.second) } @@ -54,9 +54,9 @@ class ScheduleTest extends Test: "fibonacci" - { "follows fibonacci sequence" in { val schedule = Schedule.fibonacci(1.second, 1.second) - val (next1, schedule2) = schedule.next - val (next2, schedule3) = schedule2.next - val (next3, _) = schedule3.next + val (next1, schedule2) = schedule.next.get + val (next2, schedule3) = schedule2.next.get + val (next3, _) = schedule3.next.get assert(next1 == 1.second) assert(next2 == 1.second) assert(next3 == 2.seconds) @@ -64,9 +64,9 @@ class ScheduleTest extends Test: "works with different initial values" in { val schedule = Schedule.fibonacci(1.second, 2.seconds) - val (next1, schedule2) = schedule.next - val (next2, schedule3) = schedule2.next - val (next3, _) = schedule3.next + val (next1, schedule2) = schedule.next.get + val (next2, schedule3) = schedule2.next.get + val (next3, _) = schedule3.next.get assert(next1 == 1.second) assert(next2 == 2.seconds) assert(next3 == 3.seconds) @@ -74,9 +74,9 @@ class ScheduleTest extends Test: "works with zero initial values" in { val schedule = Schedule.fibonacci(Duration.Zero, Duration.Zero) - val (next1, schedule2) = schedule.next - val (next2, schedule3) = schedule2.next - val (next3, _) = schedule3.next + val (next1, schedule2) = schedule.next.get + val (next2, schedule3) = schedule2.next.get + val (next3, _) = schedule3.next.get assert(next1 == Duration.Zero) assert(next2 == Duration.Zero) assert(next3 == Duration.Zero) @@ -85,24 +85,19 @@ class ScheduleTest extends Test: "immediate" - { "returns zero duration" in { - val (next, nextSchedule) = Schedule.immediate.next + val (next, nextSchedule) = Schedule.immediate.next.get assert(next == Duration.Zero) assert(nextSchedule == Schedule.done) } "always returns never as next schedule" in { - val (_, nextSchedule1) = Schedule.immediate.next - val (_, nextSchedule2) = nextSchedule1.next - assert(nextSchedule1 == Schedule.done) - assert(nextSchedule2 == Schedule.done) + assert(Schedule.immediate.next.flatMap(_._2.next).isEmpty) } } "never" - { "always returns infinite duration" in { - val (next, nextSchedule) = Schedule.never.next - assert(next == Duration.Infinity) - assert(nextSchedule == Schedule.never) + assert(Schedule.never.next.isEmpty) } } @@ -112,9 +107,9 @@ class ScheduleTest extends Test: val factor = 2.0 val maxDelay = 4.seconds val schedule = Schedule.exponentialBackoff(initial, factor, maxDelay) - val (next1, schedule2) = schedule.next - val (next2, schedule3) = schedule2.next - val (next3, _) = schedule3.next + val (next1, schedule2) = schedule.next.get + val (next2, schedule3) = schedule2.next.get + val (next3, _) = schedule3.next.get assert(next1 == 1.second) assert(next2 == 2.seconds) assert(next3 == 4.seconds) @@ -127,7 +122,7 @@ class ScheduleTest extends Test: val schedule = Schedule.exponentialBackoff(initial, factor, maxDelay) var current = schedule for _ <- 1 to 5 do - val (nextDuration, nextSchedule) = current.next + val (nextDuration, nextSchedule) = current.next.get assert(nextDuration <= maxDelay) current = nextSchedule end for @@ -139,8 +134,8 @@ class ScheduleTest extends Test: val factor = 0.5 val maxDelay = 4.seconds val schedule = Schedule.exponentialBackoff(initial, factor, maxDelay) - val (next1, schedule2) = schedule.next - val (next2, _) = schedule2.next + val (next1, schedule2) = schedule.next.get + val (next2, _) = schedule2.next.get assert(next1 == 4.seconds) assert(next2 == 2.seconds) } @@ -149,74 +144,68 @@ class ScheduleTest extends Test: "repeat" - { "repeats specified number of times" in { val schedule = Schedule.repeat(3) - val (next1, schedule2) = schedule.next - val (next2, schedule3) = schedule2.next - val (next3, schedule4) = schedule3.next - val (next4, _) = schedule4.next + val (next1, schedule2) = schedule.next.get + val (next2, schedule3) = schedule2.next.get + val (next3, schedule4) = schedule3.next.get + val next4 = schedule4.next assert(next1 == Duration.Zero) assert(next2 == Duration.Zero) assert(next3 == Duration.Zero) - assert(next4 == Duration.Infinity) + assert(next4.isEmpty) } "works with zero repetitions" in { - val schedule = Schedule.repeat(0) - val (next, _) = schedule.next - assert(next == Duration.Infinity) + val schedule = Schedule.repeat(0) + assert(schedule.next.isEmpty) } "works with finite inner schedule" in { val innerSchedule = Schedule.fixed(1.second).take(2) val s = innerSchedule.repeat(3) val results = List.unfold(s) { schedule => - val (next, newSchedule) = schedule.next - if next == Duration.Infinity then None - else Some((next, newSchedule)) + schedule.next.map((next, newSchedule) => Some((next, newSchedule))).getOrElse(None) } assert(results == List(1.second, 1.second, 1.second, 1.second, 1.second, 1.second)) } "repeats correct number of times with complex inner schedule" in { val s = Schedule.immediate.andThen(Schedule.fixed(1.second).take(1)).repeat(2) - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, s4) = s3.next - val (next4, s5) = s4.next - val (next5, _) = s5.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val (next3, s4) = s3.next.get + val (next4, s5) = s4.next.get + val next5 = s5.next assert(next1 == Duration.Zero) assert(next2 == 1.second) assert(next3 == Duration.Zero) assert(next4 == 1.second) - assert(next5 == Duration.Infinity) + assert(next5.isEmpty) } "Schedule.repeat" - { "repeats immediate schedule specified number of times" in { val s = Schedule.repeat(3) - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, s4) = s3.next - val (next4, _) = s4.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val (next3, s4) = s3.next.get + val next4 = s4.next assert(next1 == Duration.Zero) assert(next2 == Duration.Zero) assert(next3 == Duration.Zero) - assert(next4 == Duration.Infinity) + assert(next4.isEmpty) } "works with zero repetitions" in { - val s = Schedule.repeat(0) - val (next, _) = s.next - - assert(next == Duration.Infinity) + assert(Schedule.repeat(0).next.isEmpty) } "can be chained with other schedules" in { val s = Schedule.repeat(2).andThen(Schedule.fixed(1.second)) - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, s4) = s3.next - val (next4, _) = s4.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val (next3, s4) = s3.next.get + val (next4, _) = s4.next.get assert(next1 == Duration.Zero) assert(next2 == Duration.Zero) @@ -231,9 +220,9 @@ class ScheduleTest extends Test: "increases interval linearly" in { val base = 1.second val schedule = Schedule.linear(base) - val (next1, schedule2) = schedule.next - val (next2, schedule3) = schedule2.next - val (next3, _) = schedule3.next + val (next1, schedule2) = schedule.next.get + val (next2, schedule3) = schedule2.next.get + val (next3, _) = schedule3.next.get assert(next1 == 1.second) assert(next2 == 2.seconds) assert(next3 == 4.seconds) @@ -241,8 +230,8 @@ class ScheduleTest extends Test: "works with zero base" in { val schedule = Schedule.linear(Duration.Zero) - val (next1, schedule2) = schedule.next - val (next2, _) = schedule2.next + val (next1, schedule2) = schedule.next.get + val (next2, _) = schedule2.next.get assert(next1 == Duration.Zero) assert(next2 == Duration.Zero) } @@ -253,20 +242,19 @@ class ScheduleTest extends Test: val s1 = Schedule.fixed(1.second) val s2 = Schedule.fixed(2.seconds) val combined = s1.max(s2) - val (next, _) = combined.next + val (next, _) = combined.next.get assert(next == 2.seconds) } "handles one schedule being never" in { val s1 = Schedule.fixed(1.second) val s2 = Schedule.never - assert(s1.max(s2) == Schedule.never) + assert(s1.max(s2).next.isEmpty) } "handles both schedules being never" in { - val combined = Schedule.never.max(Schedule.never) - val (next, _) = combined.next - assert(next == Duration.Infinity) + val combined = Schedule.never.max(Schedule.never) + assert(combined.next.isEmpty) } } @@ -275,7 +263,7 @@ class ScheduleTest extends Test: val s1 = Schedule.fixed(1.second) val s2 = Schedule.fixed(2.seconds) val combined = s1.min(s2) - val (next, _) = combined.next + val (next, _) = combined.next.get assert(next == 1.second) } @@ -283,13 +271,13 @@ class ScheduleTest extends Test: val s1 = Schedule.fixed(1.second) val s2 = Schedule.immediate val combined = s1.min(s2) - val (next, _) = combined.next + val (next, _) = combined.next.get assert(next == Duration.Zero) } "handles both schedules being immediate" in { val combined = Schedule.immediate.min(Schedule.immediate) - val (next, _) = combined.next + val (next, _) = combined.next.get assert(next == Duration.Zero) } } @@ -297,12 +285,12 @@ class ScheduleTest extends Test: "take" - { "limits number of executions" in { val s = Schedule.fixed(1.second).take(2) - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, _) = s3.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val next3 = s3.next assert(next1 == 1.second) assert(next2 == 1.second) - assert(next3 == Duration.Infinity) + assert(next3.isEmpty) } "returns never for non-positive count" in { @@ -316,114 +304,109 @@ class ScheduleTest extends Test: val s1 = Schedule.repeat(2) val s2 = Schedule.fixed(1.second) val combined = s1.andThen(s2) - val (next1, c2) = combined.next - val (next2, c3) = c2.next - val (next3, _) = c3.next + val (next1, c2) = combined.next.get + val (next2, c3) = c2.next.get + val (next3, _) = c3.next.get assert(next1 == Duration.Zero) assert(next2 == Duration.Zero) assert(next3 == 1.second) } "works with never as first schedule" in { - val s1 = Schedule.never - val s2 = Schedule.fixed(1.second) - val combined = s1.andThen(s2) - val (next, _) = combined.next - assert(next == Duration.Infinity) + val s1 = Schedule.never + val s2 = Schedule.fixed(1.second) + val combined = s1.andThen(s2) + assert(combined.next.isEmpty) } "works with never as second schedule" in { val s1 = Schedule.immediate val s2 = Schedule.never val combined = s1.andThen(s2) - val (next1, c2) = combined.next - val (next2, _) = c2.next + val (next1, c2) = combined.next.get + val next2 = c2.next assert(next1 == Duration.Zero) - assert(next2 == Duration.Infinity) + assert(next2.isEmpty) } "chains multiple schedules" in { val s = Schedule.immediate.andThen(Schedule.fixed(1.second).take(1)).andThen(Schedule.fixed(2.seconds).take(1)) - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, s4) = s3.next - val (next4, _) = s4.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val (next3, s4) = s3.next.get + val next4 = s4.next assert(next1 == Duration.Zero) assert(next2 == 1.second) assert(next3 == 2.seconds) - assert(next4 == Duration.Infinity) + assert(next4.isEmpty) } } "maxDuration" - { "stops after specified duration" in { val s = Schedule.fixed(1.second).maxDuration(2.seconds + 500.millis) - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, _) = s3.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val next3 = s3.next assert(next1 == 1.second) assert(next2 == 1.second) - assert(next3 == Duration.Infinity) + assert(next3.isEmpty) } "works with zero duration" in { - val s = Schedule.fixed(1.second).maxDuration(Duration.Zero) - val (next, _) = s.next - assert(next == Duration.Infinity) + val s = Schedule.fixed(1.second).maxDuration(Duration.Zero) + assert(s.next.isEmpty) } "works with complex schedule" in { val s = Schedule.exponential(1.second, 2.0).repeat(5).maxDuration(7.seconds) val results = List.unfold(s) { schedule => - val (next, newSchedule) = schedule.next - if next == Duration.Infinity then None - else Some((next, newSchedule)) + schedule.next.map((next, newSchedule) => Some((next, newSchedule))).getOrElse(None) } assert(results == List(1.second, 2.seconds, 4.seconds)) } "limits duration correctly with delayed start" in { val s = Schedule.fixed(2.seconds).take(1).andThen(Schedule.linear(1.second)).maxDuration(5.seconds) - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, s4) = s3.next - val (next4, _) = s4.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val (next3, s4) = s3.next.get + val next4 = s4.next assert(next1 == 2.seconds) assert(next2 == 1.second) assert(next3 == 2.seconds) - assert(next4 == Duration.Infinity) + assert(next4.isEmpty) } } "forever" - { "repeats indefinitely" in { val s = Schedule.repeat(1).forever - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, _) = s3.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val (next3, _) = s3.next.get assert(next1 == Duration.Zero) assert(next2 == Duration.Zero) assert(next3 == Duration.Zero) } "works with never schedule" in { - val (next, _) = Schedule.never.forever.next - assert(next == Duration.Infinity) + assert(Schedule.never.forever.next.isEmpty) } "works with immediate schedule" in { val s = Schedule.immediate.forever - val (next1, s2) = s.next - val (next2, _) = s2.next + val (next1, s2) = s.next.get + val (next2, _) = s2.next.get assert(next1 == Duration.Zero) assert(next2 == Duration.Zero) } "works with fixed schedule" in { val s = Schedule.fixed(1.second).forever - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, _) = s3.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val (next3, _) = s3.next.get assert(next1 == 1.second) assert(next2 == 1.second) assert(next3 == 1.second) @@ -431,9 +414,9 @@ class ScheduleTest extends Test: "works with exponential schedule" in { val s = Schedule.exponential(1.second, 2.0).forever - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, _) = s3.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val (next3, _) = s3.next.get assert(next1 == 1.second) assert(next2 == 2.seconds) assert(next3 == 4.seconds) @@ -441,10 +424,10 @@ class ScheduleTest extends Test: "works with complex schedule" in { val s = (Schedule.immediate.andThen(Schedule.fixed(1.second).take(1))).forever - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, s4) = s3.next - val (next4, _) = s4.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val (next3, s4) = s3.next.get + val (next4, _) = s4.next.get assert(next1 == Duration.Zero) assert(next2 == 1.second) assert(next3 == Duration.Zero) @@ -457,8 +440,8 @@ class ScheduleTest extends Test: val original = Schedule.fixed(1.second) val delayed = original.delay(500.millis) - val (next1, s2) = delayed.next - val (next2, _) = s2.next + val (next1, s2) = delayed.next.get + val (next2, _) = s2.next.get assert(next1 == 1500.millis) assert(next2 == 1500.millis) @@ -468,7 +451,7 @@ class ScheduleTest extends Test: val original = Schedule.fixed(1.second) val delayed = original.delay(Duration.Zero) - val (next, _) = delayed.next + val (next, _) = delayed.next.get assert(next == 1.second) } @@ -476,57 +459,54 @@ class ScheduleTest extends Test: "works with immediate schedule" in { val delayed = Schedule.immediate.delay(1.second) - val (next1, s2) = delayed.next - val (next2, _) = s2.next + val (next1, s1) = delayed.next.get + val next2 = s1.next assert(next1 == 1.second) - assert(next2 == Duration.Infinity) + assert(next2.isEmpty) } "works with never schedule" in { val delayed = Schedule.never.delay(1.second) - - val (next, _) = delayed.next - - assert(next == Duration.Infinity) + assert(delayed.next.isEmpty) } "works with complex schedule" in { val original = Schedule.exponential(1.second, 2.0).take(3) val delayed = original.delay(500.millis) - val (next1, s2) = delayed.next - val (next2, s3) = s2.next - val (next3, s4) = s3.next - val (next4, _) = s4.next + val (next1, s2) = delayed.next.get + val (next2, s3) = s2.next.get + val (next3, s4) = s3.next.get + val next4 = s4.next assert(next1 == 1500.millis) assert(next2 == 2500.millis) assert(next3 == 4500.millis) - assert(next4 == Duration.Infinity) + assert(next4.isEmpty) } "Schedule.delay" - { "creates a delayed immediate schedule" in { val s = Schedule.delay(1.second) - val (next1, s2) = s.next - val (next2, _) = s2.next + val (next1, s2) = s.next.get + val next2 = s2.next assert(next1 == 1.second) - assert(next2 == Duration.Infinity) + assert(next2.isEmpty) } "works with zero delay" in { val s = Schedule.delay(Duration.Zero) - val (next, _) = s.next + val (next, _) = s.next.get assert(next == Duration.Zero) } "can be chained with other schedules" in { val s = Schedule.delay(500.millis).andThen(Schedule.fixed(1.second)) - val (next1, s2) = s.next - val (next2, _) = s2.next + val (next1, s2) = s.next.get + val (next2, _) = s2.next.get assert(next1 == 500.millis) assert(next2 == 1.second) @@ -537,11 +517,11 @@ class ScheduleTest extends Test: val s2 = Schedule.exponential(2.seconds, 2.0).take(2) val combined = s1.andThen(Schedule.delay(3.seconds)).andThen(s2) - val (next1, c2) = combined.next - val (next2, c3) = c2.next - val (next3, c4) = c3.next - val (next4, c5) = c4.next - val (next5, _) = c5.next + val (next1, c2) = combined.next.get + val (next2, c3) = c2.next.get + val (next3, c4) = c3.next.get + val (next4, c5) = c4.next.get + val (next5, _) = c5.next.get assert(next1 == 1.second) assert(next2 == 1.second) @@ -559,8 +539,8 @@ class ScheduleTest extends Test: val s3 = Schedule.fixed(3.seconds) val combined = s1.max(s2).min(s3) - val (next1, c2) = combined.next - val (next2, _) = c2.next + val (next1, c2) = combined.next.get + val (next2, _) = c2.next.get assert(next1 == 2.seconds) assert(next2 == 2.seconds) @@ -569,23 +549,23 @@ class ScheduleTest extends Test: "limits a forever schedule" in { val s = Schedule.exponential(1.second, 2.0).forever.maxDuration(5.seconds) - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, _) = s3.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val next3 = s3.next assert(next1 == 1.second) assert(next2 == 2.seconds) - assert(next3 == Duration.Infinity) + assert(next3.isEmpty) } "combines repeat with exponential backoff" in { val s = Schedule.repeat(3).andThen(Schedule.exponentialBackoff(1.second, 2.0, 8.seconds)) - val (next1, s2) = s.next - val (next2, s3) = s2.next - val (next3, s4) = s3.next - val (next4, s5) = s4.next - val (next5, _) = s5.next + val (next1, s2) = s.next.get + val (next2, s3) = s2.next.get + val (next3, s4) = s3.next.get + val (next4, s5) = s4.next.get + val (next5, _) = s5.next.get assert(next1 == Duration.Zero) assert(next2 == Duration.Zero) @@ -671,33 +651,4 @@ class ScheduleTest extends Test: } } - "withNext" - { - "allows custom processing of next duration and schedule" in { - val schedule = Schedule.fixed(1.second) - val result = schedule.withNext { (duration, nextSchedule) => - (duration * 2, nextSchedule.take(1)) - } - - assert(result == (2.seconds, Schedule.fixed(1.second).take(1))) - } - - "works with immediate schedule" in { - val schedule = Schedule.immediate - val result = schedule.withNext { (duration, nextSchedule) => - (duration, nextSchedule == Schedule.done) - } - - assert(result == (Duration.Zero, true)) - } - - "works with never schedule" in { - val schedule = Schedule.never - val result = schedule.withNext { (duration, nextSchedule) => - (duration == Duration.Infinity, nextSchedule == Schedule.never) - } - - assert(result == (true, true)) - } - } - end ScheduleTest From 23b431e2e312c8fac35675d8ed38004c0097867d Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Sun, 13 Oct 2024 22:32:23 -0700 Subject: [PATCH 4/9] fix build --- .../shared/src/main/scala/kyo/Combinators.scala | 8 +++++--- kyo-core/shared/src/main/scala/kyo/Retry.scala | 11 ++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/kyo-combinators/shared/src/main/scala/kyo/Combinators.scala b/kyo-combinators/shared/src/main/scala/kyo/Combinators.scala index 76c39e6b5..bdc345ab7 100644 --- a/kyo-combinators/shared/src/main/scala/kyo/Combinators.scala +++ b/kyo-combinators/shared/src/main/scala/kyo/Combinators.scala @@ -74,9 +74,11 @@ extension [A, S](effect: A < S) */ def repeat(schedule: Schedule)(using Flat[A], Frame): A < (S & Async) = Loop(schedule) { schedule => - val (delay, nextSchedule) = schedule.next - if !delay.isFinite then effect.map(Loop.done) - else effect.delayed(delay).as(Loop.continue(nextSchedule)) + schedule.next.map { (delay, nextSchedule) => + effect.delayed(delay).as(Loop.continue(nextSchedule)) + }.getOrElse { + effect.map(Loop.done) + } } /** Performs this computation repeatedly with a limit. diff --git a/kyo-core/shared/src/main/scala/kyo/Retry.scala b/kyo-core/shared/src/main/scala/kyo/Retry.scala index b7f9c7cc7..fdc318aff 100644 --- a/kyo-core/shared/src/main/scala/kyo/Retry.scala +++ b/kyo-core/shared/src/main/scala/kyo/Retry.scala @@ -29,14 +29,11 @@ object Retry: ): A < (Async & Abort[E] & S) = Loop(schedule) { schedule => Abort.run[E](v).map(_.fold { r => - val (delay, nextSchedule) = schedule.next - if delay.isFinite then - Async.sleep(delay).andThen { - Loop.continue(nextSchedule) - } - else + schedule.next.map { (delay, nextSchedule) => + Async.delay(delay)(Loop.continue(nextSchedule)) + }.getOrElse { Abort.get(r) - end if + } }(Loop.done(_))) } end apply From 45d130680fb314033336b36437b959c5d75f9fbc Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Sun, 13 Oct 2024 22:50:22 -0700 Subject: [PATCH 5/9] improve schedule reduction --- .../shared/src/main/scala/kyo/Schedule.scala | 39 +++-- .../src/test/scala/kyo/ScheduleTest.scala | 136 ++++++++++++++++++ 2 files changed, 164 insertions(+), 11 deletions(-) diff --git a/kyo-data/shared/src/main/scala/kyo/Schedule.scala b/kyo-data/shared/src/main/scala/kyo/Schedule.scala index 37ff48850..60eb29715 100644 --- a/kyo-data/shared/src/main/scala/kyo/Schedule.scala +++ b/kyo-data/shared/src/main/scala/kyo/Schedule.scala @@ -77,7 +77,10 @@ sealed abstract class Schedule derives CanEqual: this match case Never => Never case Done => that - case _ => AndThen(this, that) + case _ => + that match + case Done | Never | Immediate => this + case _ => AndThen(this, that) /** Repeats this schedule a specified number of times. * @@ -102,9 +105,11 @@ sealed abstract class Schedule derives CanEqual: * a new schedule that stops after the specified duration */ def maxDuration(maxDuration: Duration): Schedule = - this match - case Never | Done => this - case _ => MaxDuration(this, maxDuration) + if !maxDuration.isFinite then this + else + this match + case Never | Done | Immediate => this + case _ => MaxDuration(this, maxDuration) /** Repeats this schedule indefinitely. * @@ -114,6 +119,7 @@ sealed abstract class Schedule derives CanEqual: def forever: Schedule = this match case Never | Done => this + case _: Forever => this case _ => Forever(this) /** Adds a fixed delay before each iteration of this schedule. @@ -124,9 +130,11 @@ sealed abstract class Schedule derives CanEqual: * a new schedule with the added delay */ def delay(duration: Duration): Schedule = - this match - case Never | Done => this - case _ => Delay(this, duration) + if duration == Duration.Zero then this + else + this match + case Never | Done => this + case _ => Delay(this, duration) end Schedule @@ -180,7 +188,9 @@ object Schedule: * @return * a new schedule with linearly increasing intervals */ - def linear(base: Duration): Schedule = Linear(base) + def linear(base: Duration): Schedule = + if base == Duration.Zero then immediate.forever + else Linear(base) /** Creates a schedule with intervals following the Fibonacci sequence. * @@ -191,7 +201,9 @@ object Schedule: * @return * a new schedule with Fibonacci sequence intervals */ - def fibonacci(a: Duration, b: Duration): Schedule = Fibonacci(a, b) + def fibonacci(a: Duration, b: Duration): Schedule = + if a == Duration.Zero && b == Duration.Zero then immediate.forever + else Fibonacci(a, b) /** Creates a schedule with exponentially increasing intervals. * @@ -202,7 +214,10 @@ object Schedule: * @return * a new schedule with exponentially increasing intervals */ - def exponential(initial: Duration, factor: Double): Schedule = Exponential(initial, factor) + def exponential(initial: Duration, factor: Double): Schedule = + if initial == Duration.Zero then immediate + else if factor == 1.0 then fixed(initial) + else Exponential(initial, factor) /** Creates a schedule with exponential backoff and a maximum delay. * @@ -216,7 +231,9 @@ object Schedule: * a new schedule with exponential backoff and a maximum delay */ def exponentialBackoff(initial: Duration, factor: Double, maxBackoff: Duration): Schedule = - ExponentialBackoff(initial, factor, maxBackoff) + if initial == Duration.Zero then immediate + else if factor == 1.0 then fixed(initial) + else ExponentialBackoff(initial, factor, maxBackoff) private[kyo] object internal: diff --git a/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala b/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala index ad4925f5c..6f92c2c1d 100644 --- a/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala +++ b/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala @@ -649,6 +649,142 @@ class ScheduleTest extends Test: assert(s1 == s2) assert(s1 != s3) } + + "reduces exponential schedule with factor 1 to fixed schedule" in { + val s1 = Schedule.exponential(1.second, 1.0) + val s2 = Schedule.fixed(1.second) + + assert(s1 == s2) + } + + "reduces exponential backoff schedule with factor 1 to fixed schedule" in { + val s1 = Schedule.exponentialBackoff(1.second, 1.0, 10.seconds) + val s2 = Schedule.fixed(1.second) + + assert(s1 == s2) + } + + "reduces linear schedule with zero base to immediate schedule" in { + val s1 = Schedule.linear(Duration.Zero) + val s2 = Schedule.immediate.forever + + assert(s1 == s2) + } + + "reduces fibonacci schedule with zero initial values to immediate forever" in { + val s1 = Schedule.fibonacci(Duration.Zero, Duration.Zero) + val s2 = Schedule.immediate.forever + + assert(s1 == s2) + } + + "reduces exponential schedule with zero initial to immediate" in { + val s1 = Schedule.exponential(Duration.Zero, 2.0) + val s2 = Schedule.immediate + + assert(s1 == s2) + } + + "reduces delay with zero duration to original schedule" in { + val original = Schedule.fixed(1.second) + val delayed = original.delay(Duration.Zero) + assert(delayed == original) + } + + "reduces maxDuration with infinite duration to original schedule" in { + val original = Schedule.fixed(1.second) + val limited = original.maxDuration(Duration.Infinity) + assert(limited == original) + } + + "reduces repeat with count 1 to original schedule" in { + val original = Schedule.fixed(1.second) + val repeated = original.repeat(1) + assert(repeated == original) + } + + "reduces andThen with immediate to original schedule" in { + val original = Schedule.fixed(1.second) + val chained = original.andThen(Schedule.immediate) + assert(chained == original) + } + + "reduces forever of forever to single forever" in { + val original = Schedule.fixed(1.second) + val doubleForever = original.forever + assert(doubleForever == original.forever) + } + + "reduces delay of never to never" in { + val delayed = Schedule.never.delay(1.second) + assert(delayed == Schedule.never) + } + + "reduces maxDuration of immediate to immediate" in { + val limited = Schedule.immediate.maxDuration(1.second) + assert(limited == Schedule.immediate) + } + + "reduces andThen of done and any schedule to that schedule" in { + val s = Schedule.fixed(1.second) + val chained = Schedule.done.andThen(s) + assert(chained == s) + } + + "reduces repeat with count 0 to done" in { + val original = Schedule.fixed(1.second) + val repeated = original.repeat(0) + assert(repeated == Schedule.done) + } + + "reduces take with count 0 to done" in { + val original = Schedule.fixed(1.second) + val taken = original.take(0) + assert(taken == Schedule.done) + } + + "reduces andThen with never to original schedule" in { + val original = Schedule.fixed(1.second) + val chained = original.andThen(Schedule.never) + assert(chained == original) + } + + "reduces max of done" in { + val s = Schedule.fixed(1.second) + val maxed = Schedule.done.max(s) + assert(maxed == s) + } + + "reduces min of never and any schedule to that schedule" in { + val s = Schedule.fixed(1.second) + val minned = Schedule.never.min(s) + assert(minned == s) + } + + "reduces delay of done to done" in { + val delayed = Schedule.done.delay(1.second) + assert(delayed == Schedule.done) + } + + "reduces delay of immediate to fixed delay" in { + val delayed = Schedule.immediate.delay(1.second) + assert(delayed == Schedule.delay(1.second)) + } + + "reduces maxDuration of never to never" in { + val limited = Schedule.never.maxDuration(1.second) + assert(limited == Schedule.never) + } + + "reduces forever of never to never" in { + val foreverNever = Schedule.never.forever + assert(foreverNever == Schedule.never) + } + + "reduces forever of done to done" in { + val foreverDone = Schedule.done.forever + assert(foreverDone == Schedule.done) + } } end ScheduleTest From b22ba56c7317253ed0146918ac46e9bf0f135fcf Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Sun, 13 Oct 2024 22:57:49 -0700 Subject: [PATCH 6/9] add schedule.show --- .../shared/src/main/scala/kyo/Schedule.scala | 30 +++++++++++++ .../src/test/scala/kyo/ScheduleTest.scala | 45 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/kyo-data/shared/src/main/scala/kyo/Schedule.scala b/kyo-data/shared/src/main/scala/kyo/Schedule.scala index 60eb29715..ad84a946d 100644 --- a/kyo-data/shared/src/main/scala/kyo/Schedule.scala +++ b/kyo-data/shared/src/main/scala/kyo/Schedule.scala @@ -136,6 +136,15 @@ sealed abstract class Schedule derives CanEqual: case Never | Done => this case _ => Delay(this, duration) + /** Returns a string representation of the schedule as it would appear in source code. + * + * @return + * a string representation of the schedule + */ + def show: String + + override def toString() = show + end Schedule object Schedule: @@ -239,30 +248,38 @@ object Schedule: case object Immediate extends Schedule: val next = Maybe((Duration.Zero, Done)) + def show = "Schedule.immediate" case object Never extends Schedule: def next = Maybe.empty + def show = "Schedule.never" case object Done extends Schedule: def next = Maybe.empty + def show = "Schedule.done" case class Fixed(interval: Duration) extends Schedule: val next = Maybe((interval, this)) + def show = s"Schedule.fixed(${interval.show})" case class Exponential(initial: Duration, factor: Double) extends Schedule: def next = Maybe((initial, Exponential(initial * factor, factor))) + def show = s"Schedule.exponential(${initial.show}, $factor)" case class Fibonacci(a: Duration, b: Duration) extends Schedule: def next = Maybe((a, Fibonacci(b, a + b))) + def show = s"Schedule.fibonacci(${a.show}, ${b.show})" case class ExponentialBackoff(initial: Duration, factor: Double, maxBackoff: Duration) extends Schedule: def next = val nextDelay = initial.min(maxBackoff) Maybe((nextDelay, exponentialBackoff(nextDelay * factor, factor, maxBackoff))) + def show = s"Schedule.exponentialBackoff(${initial.show}, $factor, ${maxBackoff.show})" end ExponentialBackoff case class Linear(base: Duration) extends Schedule: def next = Maybe((base, linear(base + base))) + def show = s"Schedule.linear(${base.show})" case class Max(a: Schedule, b: Schedule) extends Schedule: def next = @@ -270,6 +287,7 @@ object Schedule: (d1, s1) <- a.next (d2, s2) <- b.next yield (d1.max(d2), s1.max(s2)) + def show = s"(${a.show}).max(${b.show})" end Max case class Min(a: Schedule, b: Schedule) extends Schedule: @@ -281,15 +299,20 @@ object Schedule: case Maybe.Empty => n case Maybe.Defined((d2, s2)) => Maybe((d1.min(d2), s1.min(s2))) + def show = s"(${a.show}).min(${b.show})" end Min case class Take(schedule: Schedule, remaining: Int) extends Schedule: def next = schedule.next.map((d, s) => (d, s.take(remaining - 1))) + def show = s"(${schedule.show}).take($remaining)" + end Take case class AndThen(a: Schedule, b: Schedule) extends Schedule: def next = a.next.map((d, s) => (d, s.andThen(b))).orElse(b.next) + def show = s"(${a.show}).andThen(${b.show})" + end AndThen case class MaxDuration(schedule: Schedule, duration: Duration) extends Schedule: def next = @@ -297,19 +320,26 @@ object Schedule: if d > duration then Maybe.empty else Maybe((d, s.maxDuration(duration - d))) } + def show = s"(${schedule.show}).maxDuration(${duration.show})" end MaxDuration case class Repeat(schedule: Schedule, remaining: Int) extends Schedule: def next = schedule.next.map((d, s) => (d, s.andThen(schedule.repeat(remaining - 1)))) + def show = s"(${schedule.show}).repeat($remaining)" + end Repeat case class Forever(schedule: Schedule) extends Schedule: def next = schedule.next.map((d, s) => (d, s.andThen(this))) + def show = s"(${schedule.show}).forever" + end Forever case class Delay(schedule: Schedule, duration: Duration) extends Schedule: def next = schedule.next.map((d, s) => (duration + d, s.delay(duration))) + def show = s"(${schedule.show}).delay(${duration.show})" + end Delay end internal end Schedule diff --git a/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala b/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala index 6f92c2c1d..00d54cb92 100644 --- a/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala +++ b/kyo-data/shared/src/test/scala/kyo/ScheduleTest.scala @@ -787,4 +787,49 @@ class ScheduleTest extends Test: } } + "show" - { + "correctly represents simple schedules" in { + assert(Schedule.immediate.show == "Schedule.immediate") + assert(Schedule.never.show == "Schedule.never") + assert(Schedule.done.show == "Schedule.done") + assert(Schedule.fixed(1.second).show == s"Schedule.fixed(${1.second.show})") + assert(Schedule.linear(2.seconds).show == s"Schedule.linear(${2.seconds.show})") + assert(Schedule.exponential(1.second, 2.0).show == s"Schedule.exponential(${1.second.show}, 2.0)") + assert(Schedule.fibonacci(1.second, 2.seconds).show == s"Schedule.fibonacci(${1.second.show}, ${2.seconds.show})") + assert(Schedule.exponentialBackoff(1.second, 2.0, 10.seconds) + .show == s"Schedule.exponentialBackoff(${1.second.show}, 2.0, ${10.seconds.show})") + } + + "correctly represents composite schedules" in { + val s1 = Schedule.fixed(1.second).take(3) + assert(s1.show == s"(Schedule.fixed(${1.second.show})).take(3)") + + val s2 = Schedule.exponential(1.second, 2.0).forever + assert(s2.show == s"(Schedule.exponential(${1.second.show}, 2.0)).forever") + + val s3 = Schedule.fixed(1.second).max(Schedule.fixed(2.seconds)) + assert(s3.show == s"(Schedule.fixed(${1.second.show})).max(Schedule.fixed(${2.seconds.show}))") + + val s4 = Schedule.immediate.andThen(Schedule.fixed(1.second)) + assert(s4.show == s"(Schedule.immediate).andThen(Schedule.fixed(${1.second.show}))") + } + + "correctly represents complex composite schedules" in { + val s = Schedule.exponential(1.second, 2.0) + .take(5) + .andThen(Schedule.fixed(10.seconds)) + .forever + .maxDuration(1.minute) + + assert( + s.show == s"((((Schedule.exponential(${1.second.show}, 2.0)).take(5)).andThen(Schedule.fixed(${10.seconds.show}))).forever).maxDuration(${1.minute.show})" + ) + } + + "correctly represents schedules with delay" in { + val s1 = Schedule.fixed(1.second).delay(500.millis) + assert(s1.show == s"(Schedule.fixed(${1.second.show})).delay(${500.millis.show})") + } + } + end ScheduleTest From 19dcb017e6ec0e385c6a5ab9b12e97c25bea7200 Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Sun, 13 Oct 2024 23:44:16 -0700 Subject: [PATCH 7/9] fix schedule.show formatting in JS --- kyo-data/shared/src/main/scala/kyo/Schedule.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/kyo-data/shared/src/main/scala/kyo/Schedule.scala b/kyo-data/shared/src/main/scala/kyo/Schedule.scala index ad84a946d..2bd74dbf3 100644 --- a/kyo-data/shared/src/main/scala/kyo/Schedule.scala +++ b/kyo-data/shared/src/main/scala/kyo/Schedule.scala @@ -264,7 +264,7 @@ object Schedule: case class Exponential(initial: Duration, factor: Double) extends Schedule: def next = Maybe((initial, Exponential(initial * factor, factor))) - def show = s"Schedule.exponential(${initial.show}, $factor)" + def show = s"Schedule.exponential(${initial.show}, ${formatDouble(factor)})" case class Fibonacci(a: Duration, b: Duration) extends Schedule: def next = Maybe((a, Fibonacci(b, a + b))) @@ -274,7 +274,7 @@ object Schedule: def next = val nextDelay = initial.min(maxBackoff) Maybe((nextDelay, exponentialBackoff(nextDelay * factor, factor, maxBackoff))) - def show = s"Schedule.exponentialBackoff(${initial.show}, $factor, ${maxBackoff.show})" + def show = s"Schedule.exponentialBackoff(${initial.show}, ${formatDouble(factor)}, ${maxBackoff.show})" end ExponentialBackoff case class Linear(base: Duration) extends Schedule: @@ -341,5 +341,8 @@ object Schedule: def show = s"(${schedule.show}).delay(${duration.show})" end Delay + private def formatDouble(d: Double): String = + if d == d.toLong then f"$d%.1f" else d.toString + end internal end Schedule From 5955e5cc809cadcdc46e29a4d0bb37f30e9aeac5 Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Sun, 13 Oct 2024 23:54:49 -0700 Subject: [PATCH 8/9] add missing final modifiers --- .../shared/src/main/scala/kyo/Schedule.scala | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/kyo-data/shared/src/main/scala/kyo/Schedule.scala b/kyo-data/shared/src/main/scala/kyo/Schedule.scala index 2bd74dbf3..5deb2e7df 100644 --- a/kyo-data/shared/src/main/scala/kyo/Schedule.scala +++ b/kyo-data/shared/src/main/scala/kyo/Schedule.scala @@ -25,7 +25,7 @@ sealed abstract class Schedule derives CanEqual: * @return * a new schedule that produces the maximum delay of both schedules */ - def max(that: Schedule): Schedule = + final def max(that: Schedule): Schedule = this match case Never => this case Done | Immediate => that @@ -42,7 +42,7 @@ sealed abstract class Schedule derives CanEqual: * @return * a new schedule that produces the minimum delay of both schedules */ - def min(that: Schedule): Schedule = + final def min(that: Schedule): Schedule = this match case Never => that case Done | Immediate => this @@ -59,7 +59,7 @@ sealed abstract class Schedule derives CanEqual: * @return * a new schedule that stops after n repetitions */ - def take(n: Int): Schedule = + final def take(n: Int): Schedule = if n <= 0 then Schedule.done else this match @@ -73,7 +73,7 @@ sealed abstract class Schedule derives CanEqual: * @return * a new schedule that runs this schedule followed by the other */ - def andThen(that: Schedule): Schedule = + final def andThen(that: Schedule): Schedule = this match case Never => Never case Done => that @@ -89,7 +89,7 @@ sealed abstract class Schedule derives CanEqual: * @return * a new schedule that repeats this schedule n times */ - def repeat(n: Int): Schedule = + final def repeat(n: Int): Schedule = if n <= 0 then Schedule.done else if n == 1 then this else @@ -104,7 +104,7 @@ sealed abstract class Schedule derives CanEqual: * @return * a new schedule that stops after the specified duration */ - def maxDuration(maxDuration: Duration): Schedule = + final def maxDuration(maxDuration: Duration): Schedule = if !maxDuration.isFinite then this else this match @@ -116,7 +116,7 @@ sealed abstract class Schedule derives CanEqual: * @return * a new schedule that repeats this schedule forever */ - def forever: Schedule = + final def forever: Schedule = this match case Never | Done => this case _: Forever => this @@ -129,7 +129,7 @@ sealed abstract class Schedule derives CanEqual: * @return * a new schedule with the added delay */ - def delay(duration: Duration): Schedule = + final def delay(duration: Duration): Schedule = if duration == Duration.Zero then this else this match @@ -258,30 +258,30 @@ object Schedule: def next = Maybe.empty def show = "Schedule.done" - case class Fixed(interval: Duration) extends Schedule: + final case class Fixed(interval: Duration) extends Schedule: val next = Maybe((interval, this)) def show = s"Schedule.fixed(${interval.show})" - case class Exponential(initial: Duration, factor: Double) extends Schedule: + final case class Exponential(initial: Duration, factor: Double) extends Schedule: def next = Maybe((initial, Exponential(initial * factor, factor))) def show = s"Schedule.exponential(${initial.show}, ${formatDouble(factor)})" - case class Fibonacci(a: Duration, b: Duration) extends Schedule: + final case class Fibonacci(a: Duration, b: Duration) extends Schedule: def next = Maybe((a, Fibonacci(b, a + b))) def show = s"Schedule.fibonacci(${a.show}, ${b.show})" - case class ExponentialBackoff(initial: Duration, factor: Double, maxBackoff: Duration) extends Schedule: + final case class ExponentialBackoff(initial: Duration, factor: Double, maxBackoff: Duration) extends Schedule: def next = val nextDelay = initial.min(maxBackoff) Maybe((nextDelay, exponentialBackoff(nextDelay * factor, factor, maxBackoff))) def show = s"Schedule.exponentialBackoff(${initial.show}, ${formatDouble(factor)}, ${maxBackoff.show})" end ExponentialBackoff - case class Linear(base: Duration) extends Schedule: + final case class Linear(base: Duration) extends Schedule: def next = Maybe((base, linear(base + base))) def show = s"Schedule.linear(${base.show})" - case class Max(a: Schedule, b: Schedule) extends Schedule: + final case class Max(a: Schedule, b: Schedule) extends Schedule: def next = for (d1, s1) <- a.next @@ -290,7 +290,7 @@ object Schedule: def show = s"(${a.show}).max(${b.show})" end Max - case class Min(a: Schedule, b: Schedule) extends Schedule: + final case class Min(a: Schedule, b: Schedule) extends Schedule: def next = a.next match case Maybe.Empty => b.next @@ -302,19 +302,19 @@ object Schedule: def show = s"(${a.show}).min(${b.show})" end Min - case class Take(schedule: Schedule, remaining: Int) extends Schedule: + final case class Take(schedule: Schedule, remaining: Int) extends Schedule: def next = schedule.next.map((d, s) => (d, s.take(remaining - 1))) def show = s"(${schedule.show}).take($remaining)" end Take - case class AndThen(a: Schedule, b: Schedule) extends Schedule: + final case class AndThen(a: Schedule, b: Schedule) extends Schedule: def next = a.next.map((d, s) => (d, s.andThen(b))).orElse(b.next) def show = s"(${a.show}).andThen(${b.show})" end AndThen - case class MaxDuration(schedule: Schedule, duration: Duration) extends Schedule: + final case class MaxDuration(schedule: Schedule, duration: Duration) extends Schedule: def next = schedule.next.flatMap { (d, s) => if d > duration then Maybe.empty @@ -323,19 +323,19 @@ object Schedule: def show = s"(${schedule.show}).maxDuration(${duration.show})" end MaxDuration - case class Repeat(schedule: Schedule, remaining: Int) extends Schedule: + final case class Repeat(schedule: Schedule, remaining: Int) extends Schedule: def next = schedule.next.map((d, s) => (d, s.andThen(schedule.repeat(remaining - 1)))) def show = s"(${schedule.show}).repeat($remaining)" end Repeat - case class Forever(schedule: Schedule) extends Schedule: + final case class Forever(schedule: Schedule) extends Schedule: def next = schedule.next.map((d, s) => (d, s.andThen(this))) def show = s"(${schedule.show}).forever" end Forever - case class Delay(schedule: Schedule, duration: Duration) extends Schedule: + final case class Delay(schedule: Schedule, duration: Duration) extends Schedule: def next = schedule.next.map((d, s) => (duration + d, s.delay(duration))) def show = s"(${schedule.show}).delay(${duration.show})" From 9660dca10ade3904a4c5bce9aba83c5f317aba9e Mon Sep 17 00:00:00 2001 From: Flavio Brasil Date: Thu, 17 Oct 2024 08:00:08 -0700 Subject: [PATCH 9/9] review feedback + rebase main --- kyo-data/shared/src/main/scala/kyo/Instant.scala | 4 ++-- kyo-data/shared/src/main/scala/kyo/Schedule.scala | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/kyo-data/shared/src/main/scala/kyo/Instant.scala b/kyo-data/shared/src/main/scala/kyo/Instant.scala index e7434aae4..63c7f7e77 100644 --- a/kyo-data/shared/src/main/scala/kyo/Instant.scala +++ b/kyo-data/shared/src/main/scala/kyo/Instant.scala @@ -145,7 +145,7 @@ object Instant: * @return * The earlier of the two Instants. */ - def min(other: Instant): Instant = if instant.isBefore(other) then instant else other + infix def min(other: Instant): Instant = if instant.isBefore(other) then instant else other /** Returns the maximum of this Instant and another. * @@ -154,7 +154,7 @@ object Instant: * @return * The later of the two Instants. */ - def max(other: Instant): Instant = if instant.isAfter(other) then instant else other + infix def max(other: Instant): Instant = if instant.isAfter(other) then instant else other /** Converts this Instant to a human-readable ISO-8601 formatted string. * diff --git a/kyo-data/shared/src/main/scala/kyo/Schedule.scala b/kyo-data/shared/src/main/scala/kyo/Schedule.scala index 5deb2e7df..cd13742d7 100644 --- a/kyo-data/shared/src/main/scala/kyo/Schedule.scala +++ b/kyo-data/shared/src/main/scala/kyo/Schedule.scala @@ -25,7 +25,7 @@ sealed abstract class Schedule derives CanEqual: * @return * a new schedule that produces the maximum delay of both schedules */ - final def max(that: Schedule): Schedule = + final infix def max(that: Schedule): Schedule = this match case Never => this case Done | Immediate => that @@ -42,7 +42,7 @@ sealed abstract class Schedule derives CanEqual: * @return * a new schedule that produces the minimum delay of both schedules */ - final def min(that: Schedule): Schedule = + final infix def min(that: Schedule): Schedule = this match case Never => that case Done | Immediate => this @@ -293,11 +293,11 @@ object Schedule: final case class Min(a: Schedule, b: Schedule) extends Schedule: def next = a.next match - case Maybe.Empty => b.next - case n @ Maybe.Defined((d1, s1)) => + case Absent => b.next + case n @ Present((d1, s1)) => b.next match - case Maybe.Empty => n - case Maybe.Defined((d2, s2)) => + case Absent => n + case Present((d2, s2)) => Maybe((d1.min(d2), s1.min(s2))) def show = s"(${a.show}).min(${b.show})" end Min