From 06c25d9baa0dad18b47a2220e9d8d7875fc8d528 Mon Sep 17 00:00:00 2001 From: Kamil-Lontkowski Date: Tue, 31 Dec 2024 13:52:29 +0100 Subject: [PATCH 1/5] CronSchedule --- build.sbt | 16 +++- .../main/scala/ox/scheduling/Schedule.scala | 6 +- .../ox/scheduling/cron/CronSchedule.scala | 34 ++++++++ .../main/scala/ox/scheduling/cron/cron.scala | 72 +++++++++++++++++ doc/integrations/cron4s.md | 77 +++++++++++++++++++ doc/utils/retries.md | 2 +- 6 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala create mode 100644 cron/src/main/scala/ox/scheduling/cron/cron.scala create mode 100644 doc/integrations/cron4s.md diff --git a/build.sbt b/build.sbt index e00b5ba9..f7c81f75 100644 --- a/build.sbt +++ b/build.sbt @@ -29,7 +29,7 @@ compileDocumentation := { lazy val rootProject = (project in file(".")) .settings(commonSettings) .settings(publishArtifact := false, name := "ox") - .aggregate(core, examples, kafka, mdcLogback, flowReactiveStreams) + .aggregate(core, examples, kafka, mdcLogback, flowReactiveStreams, cron) lazy val core: Project = (project in file("core")) .settings(commonSettings) @@ -94,6 +94,17 @@ lazy val flowReactiveStreams: Project = (project in file("flow-reactive-streams" ) .dependsOn(core) +lazy val cron: Project = (project in file("cron")) + .settings(commonSettings) + .settings( + name := "cron", + libraryDependencies ++= Seq( + "com.github.alonsodomin.cron4s" %% "cron4s-core" % "0.7.0", + scalaTest + ) + ) + .dependsOn(core) + lazy val documentation: Project = (project in file("generated-doc")) // important: it must not be doc/ .enablePlugins(MdocPlugin) .settings(commonSettings) @@ -113,5 +124,6 @@ lazy val documentation: Project = (project in file("generated-doc")) // importan core, kafka, mdcLogback, - flowReactiveStreams + flowReactiveStreams, + cron ) diff --git a/core/src/main/scala/ox/scheduling/Schedule.scala b/core/src/main/scala/ox/scheduling/Schedule.scala index e4e7d1fc..a8570ac3 100644 --- a/core/src/main/scala/ox/scheduling/Schedule.scala +++ b/core/src/main/scala/ox/scheduling/Schedule.scala @@ -3,18 +3,18 @@ package ox.scheduling import scala.concurrent.duration.* import scala.util.Random -sealed trait Schedule: +trait Schedule: def nextDuration(invocation: Int, lastDuration: Option[FiniteDuration]): FiniteDuration def initialDelay: FiniteDuration = Duration.Zero object Schedule: - private[scheduling] sealed trait Finite extends Schedule: + private[scheduling] trait Finite extends Schedule: def maxRepeats: Int def andThen(nextSchedule: Finite): Finite = FiniteAndFiniteSchedules(this, nextSchedule) def andThen(nextSchedule: Infinite): Infinite = FiniteAndInfiniteSchedules(this, nextSchedule) - private[scheduling] sealed trait Infinite extends Schedule + private[scheduling] trait Infinite extends Schedule /** A schedule that represents an initial delay applied before the first invocation of operation being scheduled. Usually used in * combination with other schedules using [[andThen]] diff --git a/cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala b/cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala new file mode 100644 index 00000000..271fc418 --- /dev/null +++ b/cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala @@ -0,0 +1,34 @@ +package ox.scheduling.cron + +import cron4s.lib.javatime.* +import cron4s.{Cron, CronExpr, toDateTimeCronOps} +import ox.scheduling.Schedule + +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.{Duration, FiniteDuration} + +case class CronSchedule(cron: CronExpr) extends Schedule.Infinite: + override def initialDelay: FiniteDuration = + val now = LocalDateTime.now() + val next = cron.next(now) + val duration = next.map(n => ChronoUnit.MILLIS.between(now, n)) + duration.map(FiniteDuration.apply(_, TimeUnit.MILLISECONDS)).getOrElse(Duration.Zero) + end initialDelay + + def nextDuration(invocation: Int, lastDuration: Option[FiniteDuration]): FiniteDuration = + initialDelay +end CronSchedule + +object CronSchedule: + /** @param expression + * cron expression to parse + * @return + * [[CronSchedule]] from cron expression + * @throws cron4s.Error + * in case the cron expression is invalid + */ + def unsafeFromString(expression: String): CronSchedule = + CronSchedule(Cron.unsafeParse(expression)) +end CronSchedule diff --git a/cron/src/main/scala/ox/scheduling/cron/cron.scala b/cron/src/main/scala/ox/scheduling/cron/cron.scala new file mode 100644 index 00000000..76c5896d --- /dev/null +++ b/cron/src/main/scala/ox/scheduling/cron/cron.scala @@ -0,0 +1,72 @@ +package ox.scheduling.cron + +import cron4s.CronExpr +import ox.scheduling.* +import ox.{EitherMode, ErrorMode} + +import scala.util.Try + +/** Repeats an operation returning a direct result based on cron expression until it succeeds we decide to stop. + * + * [[repeat]] is a special case of [[scheduled]] with a given set of defaults. + * + * @param cron + * [[CronExpr]] to create schedule from + * @param shouldContinueOnResult + * A function that determines whether to continue the loop after a success. The function receives the value that was emitted by the last + * invocation. Defaults to [[_ => true]]. + * @param operation + * The operation to repeat. + * @return + * The result of the last invocation if the config decides to stop. + * @throws anything + * The exception thrown by the last invocation if the config decides to stop. + * @see + * [[scheduled]] + */ +def repeat[T](cron: CronExpr, shouldContinueOnResult: T => Boolean = (_: T) => true)(operation: => T): T = + repeatEither[Throwable, T](cron, shouldContinueOnResult)(Try(operation).toEither).fold(throw _, identity) + +/** Repeats an operation based on cron expression returning an [[scala.util.Either]] until we decide to stop. Note that any exceptions + * thrown by the operation aren't caught and effectively interrupt the repeat loop. + * + * [[repeatEither]] is a special case of [[scheduledEither]] with a given set of defaults. + * + * @param cron + * [[CronExpr]] to create schedule from + * @param shouldContinueOnResult + * A function that determines whether to continue the loop after a success. The function receives the value that was emitted by the last + * invocation. Defaults to [[_ => true]]. + * @param operation + * The operation to repeat. + * @return + * The result of the last invocation if the config decides to stop. + * @see + * [[scheduledEither]] + */ +def repeatEither[E, T](cron: CronExpr, shouldContinueOnResult: T => Boolean = (_: T) => true)(operation: => Either[E, T]): Either[E, T] = + repeatWithErrorMode(EitherMode[E])(cron, shouldContinueOnResult)(operation) + +/** Repeats an operation based on cron expression using the given error mode until we decide to stop. Note that any exceptions thrown by the + * operation aren't caught and effectively interrupt the repeat loop. + * + * [[repeatWithErrorMode]] is a special case of [[scheduledWithErrorMode]] with a given set of defaults. + * + * @param em + * The error mode to use, which specifies when a result value is considered success, and when a failure. + * @param cron + * [[CronExpr]] to create schedule from + * @param shouldContinueOnResult + * A function that determines whether to continue the loop after a success. The function receives the value that was emitted by the last + * invocation. Defaults to [[_ => true]]. + * @param operation + * The operation to repeat. + * @return + * The result of the last invocation if the config decides to stop. + * @see + * [[scheduledWithErrorMode]] + */ +def repeatWithErrorMode[E, F[_], T](em: ErrorMode[E, F])(cron: CronExpr, shouldContinueOnResult: T => Boolean = (_: T) => true)( + operation: => F[T] +): F[T] = + scheduledWithErrorMode[E, F, T](em)(RepeatConfig(CronSchedule(cron), shouldContinueOnResult).toScheduledConfig)(operation) diff --git a/doc/integrations/cron4s.md b/doc/integrations/cron4s.md new file mode 100644 index 00000000..29ab1207 --- /dev/null +++ b/doc/integrations/cron4s.md @@ -0,0 +1,77 @@ +# Cron scheduler + +Dependency: + +```scala +"com.softwaremill.ox" %% "cron" % "@VERSION@" +``` + +This module allows to run schedules based on cron expressions from [cron4s](https://github.com/alonsodomin/cron4s). + +`CronSchedule` can be used in all places that requires `Schedule` especially in repeat scenarios. + +For defining `CronExpr` see [cron4s documentation](https://www.alonsodomin.me/cron4s/userguide/index.html). + +## Api + +The basic syntax for `cron.repeat` is similar to `repeat`: + +```scala +import ox.scheduling.cron.repeat + +repeat(cronExpr)(operation) +``` + +The `repeat` API uses `CronSchedule` underneath, but since it does not provide any configuration beyond `CronExpr` there is no need to provide instance of `CronSchedule` directly. + +## Operation definition + +Similarly to the `repeat` API, the `operation` can be defined: +* directly using a by-name parameter, i.e. `f: => T` +* using a by-name `Either[E, T]` +* or using an arbitrary [error mode](../basics/error-handling.md), accepting the computation in an `F` context: `f: => F[T]`. + + +## Configuration + +The `cron.repeat` requires a `CronExpr`, which defines cron expression on which the schedule will run. + +In addition, it is possible to define a custom `shouldContinueOnResult` strategy for deciding if the operation +should continue to be repeated after a successful result returned by the previous operation (defaults to `_: T => true`). + +If an operation returns an error, the repeat loop will always be stopped. If an error handling within the operation +is needed, you can use a `retry` inside it (see an example below) or use `scheduled` with `CronSchedule` instead of `cron.repeat`, which allows +full customization. + + +## Examples + +```scala mdoc:compile-only +import ox.UnionMode +import ox.scheduling.cron.{repeat, repeatEither, repeatWithErrorMode} +import scala.concurrent.duration.* +import ox.resilience.{RetryConfig, retry} +import cron4s.* + +def directOperation: Int = ??? +def eitherOperation: Either[String, Int] = ??? +def unionOperation: String | Int = ??? + +val cronExpr: CronExpr = Cron.unsafeParse("10-35 2,4,6 * ? * *") + +// various operation definitions - same syntax +repeat(cronExpr)(directOperation) +repeatEither(cronExpr)(eitherOperation) + +// infinite repeats with a custom strategy +def customStopStrategy: Int => Boolean = ??? +repeat(cronExpr, shouldContinueOnResult = customStopStrategy)(directOperation) + +// custom error mode +repeatWithErrorMode(UnionMode[String])(cronExpr)(unionOperation) + +// repeat with retry inside +repeat(cronExpr) { + retry(RetryConfig.backoff(3, 100.millis))(directOperation) +} +``` diff --git a/doc/utils/retries.md b/doc/utils/retries.md index 622d99b1..6108c0bb 100644 --- a/doc/utils/retries.md +++ b/doc/utils/retries.md @@ -160,7 +160,7 @@ Instance with default configuration can be obtained with `AdaptiveRetry.default` `retry` will attempt to retry an operation if it throws an exception; `retryEither` will additionally retry, if the result is a `Left`. Finally `retryWithErrorMode` is the most flexible, and allows retrying operations using custom failure modes (such as union types). -The methods have an additional parameter, `shouldPayPenaltyCost`, which determines if result `T` should be considered failure in terms of paying cost for retry. Penalty is paid only if it is decided to retry operation, the penalty will not be paid for successful operation. +The methods have an additional parameter, `shouldPayPenaltyCost`, which determines if result `Either[E, T]` should be considered as a failure in terms of paying cost for retry. Penalty is paid only if it is decided to retry operation, the penalty will not be paid for successful operation. ### Examples From ce590f539972aceb4f485c6d3a1c89e514d18315 Mon Sep 17 00:00:00 2001 From: Kamil-Lontkowski Date: Thu, 2 Jan 2025 17:23:30 +0100 Subject: [PATCH 2/5] leave schedule sealed --- build.sbt | 2 +- .../main/scala/ox/scheduling/Schedule.scala | 13 +++- .../ox/scheduling/cron/CronSchedule.scala | 36 ++++++---- .../main/scala/ox/scheduling/cron/cron.scala | 72 ------------------- .../ox/scheduling/cron/CronScheduleTest.scala | 49 +++++++++++++ doc/integrations/cron4s.md | 36 ++++------ 6 files changed, 96 insertions(+), 112 deletions(-) delete mode 100644 cron/src/main/scala/ox/scheduling/cron/cron.scala create mode 100644 cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala diff --git a/build.sbt b/build.sbt index f7c81f75..88c43bf2 100644 --- a/build.sbt +++ b/build.sbt @@ -103,7 +103,7 @@ lazy val cron: Project = (project in file("cron")) scalaTest ) ) - .dependsOn(core) + .dependsOn(core % "test->test;compile->compile") lazy val documentation: Project = (project in file("generated-doc")) // important: it must not be doc/ .enablePlugins(MdocPlugin) diff --git a/core/src/main/scala/ox/scheduling/Schedule.scala b/core/src/main/scala/ox/scheduling/Schedule.scala index a8570ac3..17375b4e 100644 --- a/core/src/main/scala/ox/scheduling/Schedule.scala +++ b/core/src/main/scala/ox/scheduling/Schedule.scala @@ -3,18 +3,25 @@ package ox.scheduling import scala.concurrent.duration.* import scala.util.Random -trait Schedule: +sealed trait Schedule: def nextDuration(invocation: Int, lastDuration: Option[FiniteDuration]): FiniteDuration def initialDelay: FiniteDuration = Duration.Zero object Schedule: - private[scheduling] trait Finite extends Schedule: + private[scheduling] sealed trait Finite extends Schedule: def maxRepeats: Int def andThen(nextSchedule: Finite): Finite = FiniteAndFiniteSchedules(this, nextSchedule) def andThen(nextSchedule: Infinite): Infinite = FiniteAndInfiniteSchedules(this, nextSchedule) - private[scheduling] trait Infinite extends Schedule + private[scheduling] sealed trait Infinite extends Schedule + + private[scheduling] final case class ExternalInfinite( + computeNextDuration: (Int, Option[FiniteDuration]) => FiniteDuration + ) extends Infinite: + def nextDuration(invocation: Int, lastDuration: Option[FiniteDuration]): FiniteDuration = computeNextDuration(invocation, lastDuration) + + override def initialDelay: FiniteDuration = computeNextDuration(0, None) /** A schedule that represents an initial delay applied before the first invocation of operation being scheduled. Usually used in * combination with other schedules using [[andThen]] diff --git a/cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala b/cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala index 271fc418..9c0bf6f1 100644 --- a/cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala +++ b/cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala @@ -9,26 +9,32 @@ import java.time.temporal.ChronoUnit import java.util.concurrent.TimeUnit import scala.concurrent.duration.{Duration, FiniteDuration} -case class CronSchedule(cron: CronExpr) extends Schedule.Infinite: - override def initialDelay: FiniteDuration = - val now = LocalDateTime.now() - val next = cron.next(now) - val duration = next.map(n => ChronoUnit.MILLIS.between(now, n)) - duration.map(FiniteDuration.apply(_, TimeUnit.MILLISECONDS)).getOrElse(Duration.Zero) - end initialDelay - - def nextDuration(invocation: Int, lastDuration: Option[FiniteDuration]): FiniteDuration = - initialDelay -end CronSchedule - +/** Methods in this object provide + */ object CronSchedule: /** @param expression * cron expression to parse * @return * [[CronSchedule]] from cron expression * @throws cron4s.Error - * in case the cron expression is invalid + * in case of invalid expression */ - def unsafeFromString(expression: String): CronSchedule = - CronSchedule(Cron.unsafeParse(expression)) + def unsafeFromString(expression: String): Schedule = + fromCronExpr(Cron.unsafeParse(expression)) + + /** @param cron + * expression to base [[Schedule]] on. + * @return + * [[Schedule]] from cron expression + */ + def fromCronExpr(cron: CronExpr): Schedule = + def computeNext(invocation: Int, lastDuration: Option[FiniteDuration]): FiniteDuration = + val now = LocalDateTime.now() + val next = cron.next(now) + val duration = next.map(n => ChronoUnit.MILLIS.between(now, n)) + duration.map(FiniteDuration.apply(_, TimeUnit.MILLISECONDS)).getOrElse(Duration.Zero) + end computeNext + + Schedule.ExternalInfinite(computeNext) + end fromCronExpr end CronSchedule diff --git a/cron/src/main/scala/ox/scheduling/cron/cron.scala b/cron/src/main/scala/ox/scheduling/cron/cron.scala deleted file mode 100644 index 76c5896d..00000000 --- a/cron/src/main/scala/ox/scheduling/cron/cron.scala +++ /dev/null @@ -1,72 +0,0 @@ -package ox.scheduling.cron - -import cron4s.CronExpr -import ox.scheduling.* -import ox.{EitherMode, ErrorMode} - -import scala.util.Try - -/** Repeats an operation returning a direct result based on cron expression until it succeeds we decide to stop. - * - * [[repeat]] is a special case of [[scheduled]] with a given set of defaults. - * - * @param cron - * [[CronExpr]] to create schedule from - * @param shouldContinueOnResult - * A function that determines whether to continue the loop after a success. The function receives the value that was emitted by the last - * invocation. Defaults to [[_ => true]]. - * @param operation - * The operation to repeat. - * @return - * The result of the last invocation if the config decides to stop. - * @throws anything - * The exception thrown by the last invocation if the config decides to stop. - * @see - * [[scheduled]] - */ -def repeat[T](cron: CronExpr, shouldContinueOnResult: T => Boolean = (_: T) => true)(operation: => T): T = - repeatEither[Throwable, T](cron, shouldContinueOnResult)(Try(operation).toEither).fold(throw _, identity) - -/** Repeats an operation based on cron expression returning an [[scala.util.Either]] until we decide to stop. Note that any exceptions - * thrown by the operation aren't caught and effectively interrupt the repeat loop. - * - * [[repeatEither]] is a special case of [[scheduledEither]] with a given set of defaults. - * - * @param cron - * [[CronExpr]] to create schedule from - * @param shouldContinueOnResult - * A function that determines whether to continue the loop after a success. The function receives the value that was emitted by the last - * invocation. Defaults to [[_ => true]]. - * @param operation - * The operation to repeat. - * @return - * The result of the last invocation if the config decides to stop. - * @see - * [[scheduledEither]] - */ -def repeatEither[E, T](cron: CronExpr, shouldContinueOnResult: T => Boolean = (_: T) => true)(operation: => Either[E, T]): Either[E, T] = - repeatWithErrorMode(EitherMode[E])(cron, shouldContinueOnResult)(operation) - -/** Repeats an operation based on cron expression using the given error mode until we decide to stop. Note that any exceptions thrown by the - * operation aren't caught and effectively interrupt the repeat loop. - * - * [[repeatWithErrorMode]] is a special case of [[scheduledWithErrorMode]] with a given set of defaults. - * - * @param em - * The error mode to use, which specifies when a result value is considered success, and when a failure. - * @param cron - * [[CronExpr]] to create schedule from - * @param shouldContinueOnResult - * A function that determines whether to continue the loop after a success. The function receives the value that was emitted by the last - * invocation. Defaults to [[_ => true]]. - * @param operation - * The operation to repeat. - * @return - * The result of the last invocation if the config decides to stop. - * @see - * [[scheduledWithErrorMode]] - */ -def repeatWithErrorMode[E, F[_], T](em: ErrorMode[E, F])(cron: CronExpr, shouldContinueOnResult: T => Boolean = (_: T) => true)( - operation: => F[T] -): F[T] = - scheduledWithErrorMode[E, F, T](em)(RepeatConfig(CronSchedule(cron), shouldContinueOnResult).toScheduledConfig)(operation) diff --git a/cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala b/cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala new file mode 100644 index 00000000..f5c3745e --- /dev/null +++ b/cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala @@ -0,0 +1,49 @@ +package ox.scheduling.cron + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import cron4s.* +import ox.scheduling.{RepeatConfig, repeat} +import scala.concurrent.duration.* +import ox.util.ElapsedTime + +class CronScheduleTest extends AnyFlatSpec with Matchers with ElapsedTime: + behavior of "repeat with cron schedule" + + it should "repeat a function every second" in { + // given + val cronExpr = Cron.unsafeParse("* * * ? * *") // every second + val cronSchedule = CronSchedule.fromCronExpr(cronExpr) + + var counter = 0 + + def f = + if counter > 0 then throw new RuntimeException("boom") + else counter += 1 + + // when + val (ex, elapsedTime) = measure(the[RuntimeException] thrownBy repeat(RepeatConfig(cronSchedule))(f)) + + // then + ex.getMessage shouldBe "boom" + counter shouldBe 1 + elapsedTime.toMillis should be < 2200L // Run 2 times, so at most 2 secs - 200ms for tolerance + } + + it should "provide initial delay" in { + // give + val cronExpr = Cron.unsafeParse("* * * ? * *") // every second + val cronSchedule = CronSchedule.fromCronExpr(cronExpr) + + def f = + throw new RuntimeException("boom") + + // when + val (ex, elapsedTime) = measure(the[RuntimeException] thrownBy repeat(RepeatConfig(cronSchedule))(f)) + + // then + ex.getMessage shouldBe "boom" + elapsedTime.toMillis should be < 1200L // Run 2 times, so at most 2 secs - 200ms for tolerance + elapsedTime.toNanos should be > 0L + } +end CronScheduleTest diff --git a/doc/integrations/cron4s.md b/doc/integrations/cron4s.md index 29ab1207..67ad8127 100644 --- a/doc/integrations/cron4s.md +++ b/doc/integrations/cron4s.md @@ -14,33 +14,26 @@ For defining `CronExpr` see [cron4s documentation](https://www.alonsodomin.me/cr ## Api -The basic syntax for `cron.repeat` is similar to `repeat`: +The cron module exposes methods for creating `Schedule` based on `CronExpr`. That `Schedule` can be plugged ```scala -import ox.scheduling.cron.repeat +import ox.scheduling.cron.* +import cron4s.* -repeat(cronExpr)(operation) +repeat(RepeatConfig(CronSchedule.unsafeFromString("10-35 2,4,6 * ? * *")))(operation) ``` -The `repeat` API uses `CronSchedule` underneath, but since it does not provide any configuration beyond `CronExpr` there is no need to provide instance of `CronSchedule` directly. +The API uses `Schedule` underneath, so it can be plugged wherever `Schedule` is needed. ## Operation definition -Similarly to the `repeat` API, the `operation` can be defined: -* directly using a by-name parameter, i.e. `f: => T` -* using a by-name `Either[E, T]` -* or using an arbitrary [error mode](../basics/error-handling.md), accepting the computation in an `F` context: `f: => F[T]`. +Methods from `ox.scheduling.cron.CronSchedule` define `Schedule`, so they can be plugged into `RepeatConfig` and used with `repeat` API. ## Configuration -The `cron.repeat` requires a `CronExpr`, which defines cron expression on which the schedule will run. - -In addition, it is possible to define a custom `shouldContinueOnResult` strategy for deciding if the operation -should continue to be repeated after a successful result returned by the previous operation (defaults to `_: T => true`). - -If an operation returns an error, the repeat loop will always be stopped. If an error handling within the operation -is needed, you can use a `retry` inside it (see an example below) or use `scheduled` with `CronSchedule` instead of `cron.repeat`, which allows +All configuration beyond `CronExpr` is provided by the `repeat` API. If an error handling within the operation +is needed, you can use a `retry` inside it (see an example below) or use `scheduled` with `CronSchedule` instead of `repeat`, which allows full customization. @@ -48,9 +41,10 @@ full customization. ```scala mdoc:compile-only import ox.UnionMode -import ox.scheduling.cron.{repeat, repeatEither, repeatWithErrorMode} +import ox.scheduling.cron.CronSchedule import scala.concurrent.duration.* import ox.resilience.{RetryConfig, retry} +import ox.scheduling.* import cron4s.* def directOperation: Int = ??? @@ -60,18 +54,18 @@ def unionOperation: String | Int = ??? val cronExpr: CronExpr = Cron.unsafeParse("10-35 2,4,6 * ? * *") // various operation definitions - same syntax -repeat(cronExpr)(directOperation) -repeatEither(cronExpr)(eitherOperation) +repeat(RepeatConfig(CronSchedule.fromCronExpr(cronExpr)))(directOperation) +repeatEither(RepeatConfig(CronSchedule.fromCronExpr(cronExpr)))(eitherOperation) // infinite repeats with a custom strategy def customStopStrategy: Int => Boolean = ??? -repeat(cronExpr, shouldContinueOnResult = customStopStrategy)(directOperation) +repeat(RepeatConfig(CronSchedule.fromCronExpr(cronExpr), customStopStrategy))(directOperation) // custom error mode -repeatWithErrorMode(UnionMode[String])(cronExpr)(unionOperation) +repeatWithErrorMode(UnionMode[String])(RepeatConfig(CronSchedule.fromCronExpr(cronExpr)))(unionOperation) // repeat with retry inside -repeat(cronExpr) { +repeat(RepeatConfig(CronSchedule.fromCronExpr(cronExpr))) { retry(RetryConfig.backoff(3, 100.millis))(directOperation) } ``` From 07b85281ae01e7bf214b1edb1832d99433efde62 Mon Sep 17 00:00:00 2001 From: Kamil-Lontkowski Date: Thu, 2 Jan 2025 17:29:34 +0100 Subject: [PATCH 3/5] leave schedule sealed --- cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala b/cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala index f5c3745e..b5e86c37 100644 --- a/cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala +++ b/cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala @@ -43,7 +43,7 @@ class CronScheduleTest extends AnyFlatSpec with Matchers with ElapsedTime: // then ex.getMessage shouldBe "boom" - elapsedTime.toMillis should be < 1200L // Run 2 times, so at most 2 secs - 200ms for tolerance - elapsedTime.toNanos should be > 0L + elapsedTime.toMillis should be < 1200L // Run 1 times, so at most 1 sec - 200ms for tolerance + elapsedTime.toMillis should be > 0L } end CronScheduleTest From 414cc821432db2974d6996ebeaf3dccb78672ba4 Mon Sep 17 00:00:00 2001 From: Kamil-Lontkowski Date: Fri, 3 Jan 2025 12:18:04 +0100 Subject: [PATCH 4/5] Rename to ComputedInfinite --- core/src/main/scala/ox/scheduling/Schedule.scala | 6 +++++- cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala | 6 +++--- .../test/scala/ox/scheduling/cron/CronScheduleTest.scala | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala/ox/scheduling/Schedule.scala b/core/src/main/scala/ox/scheduling/Schedule.scala index 17375b4e..624d8917 100644 --- a/core/src/main/scala/ox/scheduling/Schedule.scala +++ b/core/src/main/scala/ox/scheduling/Schedule.scala @@ -16,7 +16,11 @@ object Schedule: private[scheduling] sealed trait Infinite extends Schedule - private[scheduling] final case class ExternalInfinite( + /** @param computeNextDuration + * computes time between next invocations of operation. Invocation = 0 represents initialDelay before invoking operation for the first + * time. + */ + private[scheduling] final case class ComputedInfinite( computeNextDuration: (Int, Option[FiniteDuration]) => FiniteDuration ) extends Infinite: def nextDuration(invocation: Int, lastDuration: Option[FiniteDuration]): FiniteDuration = computeNextDuration(invocation, lastDuration) diff --git a/cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala b/cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala index 9c0bf6f1..bdfde97b 100644 --- a/cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala +++ b/cron/src/main/scala/ox/scheduling/cron/CronSchedule.scala @@ -9,7 +9,7 @@ import java.time.temporal.ChronoUnit import java.util.concurrent.TimeUnit import scala.concurrent.duration.{Duration, FiniteDuration} -/** Methods in this object provide +/** Methods in this object provide [[Schedule]] based on supplied cron expression. */ object CronSchedule: /** @param expression @@ -23,7 +23,7 @@ object CronSchedule: fromCronExpr(Cron.unsafeParse(expression)) /** @param cron - * expression to base [[Schedule]] on. + * [[CronExpr]] to base [[Schedule]] on. * @return * [[Schedule]] from cron expression */ @@ -35,6 +35,6 @@ object CronSchedule: duration.map(FiniteDuration.apply(_, TimeUnit.MILLISECONDS)).getOrElse(Duration.Zero) end computeNext - Schedule.ExternalInfinite(computeNext) + Schedule.ComputedInfinite(computeNext) end fromCronExpr end CronSchedule diff --git a/cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala b/cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala index b5e86c37..38451fe4 100644 --- a/cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala +++ b/cron/src/test/scala/ox/scheduling/cron/CronScheduleTest.scala @@ -43,7 +43,7 @@ class CronScheduleTest extends AnyFlatSpec with Matchers with ElapsedTime: // then ex.getMessage shouldBe "boom" - elapsedTime.toMillis should be < 1200L // Run 1 times, so at most 1 sec - 200ms for tolerance + elapsedTime.toMillis should be < 1200L // Run 1 time, so at most 1 sec - 200ms for tolerance elapsedTime.toMillis should be > 0L } end CronScheduleTest From 5461946ca9da917f59b7b0cbd6cbb57734b9baa0 Mon Sep 17 00:00:00 2001 From: Kamil-Lontkowski Date: Fri, 3 Jan 2025 12:22:22 +0100 Subject: [PATCH 5/5] update docs --- doc/integrations/cron4s.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/integrations/cron4s.md b/doc/integrations/cron4s.md index 67ad8127..c7c6a80c 100644 --- a/doc/integrations/cron4s.md +++ b/doc/integrations/cron4s.md @@ -14,7 +14,7 @@ For defining `CronExpr` see [cron4s documentation](https://www.alonsodomin.me/cr ## Api -The cron module exposes methods for creating `Schedule` based on `CronExpr`. That `Schedule` can be plugged +The cron module exposes methods for creating `Schedule` based on `CronExpr`. ```scala import ox.scheduling.cron.* @@ -23,8 +23,6 @@ import cron4s.* repeat(RepeatConfig(CronSchedule.unsafeFromString("10-35 2,4,6 * ? * *")))(operation) ``` -The API uses `Schedule` underneath, so it can be plugged wherever `Schedule` is needed. - ## Operation definition Methods from `ox.scheduling.cron.CronSchedule` define `Schedule`, so they can be plugged into `RepeatConfig` and used with `repeat` API.