This repository has been archived by the owner on Jun 14, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
update readme for the new implementation
- Loading branch information
1 parent
fb99b2b
commit ad9f0a5
Showing
1 changed file
with
179 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,50 +1,202 @@ | ||
# pure-scheduler [![Build Status](https://travis-ci.org/emartech/pure-scheduler.svg?branch=master)](https://travis-ci.org/emartech/pure-scheduler) [![Maven Central](https://img.shields.io/maven-central/v/com.emarsys/pure-scheduler_2.12.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.emarsys%22%20AND%20a:%22pure-scheduler_2.12%22) | ||
|
||
A pure scheduler for referentially transparent effect types. | ||
A pure scheduler for _referentially transparent_ effect types. | ||
|
||
## DSL | ||
This is in a very early stage of development, API-s may change. The current implementation was heavily inspired by the awesome [ZIO Schedule]. The idea is that one can build up arbitrarily complex strategies - aka. schedules - and use them both for repeating effects or retrying them. There are only a few predefined schedules and a handful of combinators to do this. | ||
|
||
Building up the schedule is done with `cats.free.Free`. There are some smart constructors available in the `com.emarsys.scheduler.dsl` object. | ||
The aims of this library are | ||
|
||
* work smoothly with tagless final | ||
* have a cool DSL | ||
* aid type inference as much as possible | ||
|
||
## Include in your project | ||
|
||
``` | ||
libraryDependencies += "com.emarsys" % "pure-scheduler" % "x.y.z" | ||
``` | ||
|
||
The latest released version is on the maven badge at the top of this document. | ||
|
||
## The Schedule type | ||
|
||
```scala | ||
trait Schedule[F[+_], -A, +B] { | ||
type State | ||
val initial: F[Schedule.Init[State]] | ||
val update: (A, State) => F[Schedule.Decision[State, B]] | ||
} | ||
``` | ||
|
||
A schedule is a data structure that defines how repetition - or retrying - should be done. It defines two things: `initial` can tell if there should be an initial delay, and what the initial state is, `update` can make a decision based on a value of type `A` and the state. A decision carries the information whether the repetition should continue, what delay is to be applied, what is the new state and what is the current result (a value of type `B`). | ||
|
||
### Repeating effects | ||
|
||
A schedule of type `Schedule[F, A, B]` can be used to repeat effects of type `F[A]` producing a value of type `B` in the same context. A function for repeating effects are provided as an extension method for suitable `F`-s (having a `cats.Monad` and a `cats.effect.Timer` instance for them). | ||
|
||
```scala | ||
import com.emarsys.scheduler.Schedule | ||
import com.emarsys.scheduler.syntax._ | ||
|
||
val effect: F[A] | ||
val schedule: Schedule[F, A, B] | ||
|
||
val scheduled: F[B] = effect runOn schedule | ||
``` | ||
|
||
### Retrying effects - not yet implemented | ||
|
||
A schedule of type `Schedule[F, E, B]` can be used to retry effects of type `F[A]` that can fail with an error of type `E`. A function for repeating effects are provided as an extension method for suitable `F`-s (having a `cats.Moderror[?[_], E]` instance for them). | ||
|
||
> Note, that this `E` in practice will most likely be `Throwable`, given that the majority of proper effect types out there can only fail with `Throwable`-s. Except for ZIO, but users of ZIO are probably better off with using ZIO Schedules anyway. | ||
```scala | ||
import com.emarsys.scheduler.Schedule | ||
import com.emarsys.scheduler.syntax._ | ||
|
||
val effect: F[A] | ||
val policy: Schedule[F, E, B] | ||
|
||
// A MonadError[F, E] must be available implicitly | ||
|
||
val retried: F[A] = effect retry policy | ||
``` | ||
|
||
## Predefined schedules | ||
|
||
Predefined schedules are available on the `Schedule` companion object. | ||
|
||
Never executing schedule | ||
```scala | ||
def now[F[_]](F: F[Unit]) | ||
def after[F[_]](delay: FiniteDuration, F: F[Unit]) | ||
def repeat[F[_]](F: F[Unit], interval: FiniteDuration) | ||
def repeatAfter[F[_]](delay: FiniteDuration, F: F[Unit], interval: FiniteDuration) | ||
Schedule.never: Schedule[F, Any, Nothing] | ||
``` | ||
|
||
The results are instances of `Free`, they fit nicely in a `for` comprehension: | ||
Recurring forever, without delay, returning how many times it has run so far. | ||
```scala | ||
Schedule.forever: Schedule[F, Any, Int] | ||
``` | ||
|
||
Forever is implemented in terms of unfold | ||
```scala | ||
import com.emarsys.scheduler.dsl._ | ||
import scala.concurrent.duration._ | ||
Schedule.unfold[B](zero: => B)(f: B => B): Schedule[F, Any, B] | ||
|
||
val schedule = for { | ||
_ <- now(logStart) | ||
_ <- repeatAfter(10.minutes, doSomeWorkEveryHalfAnHourWithSomeInitialDelay, 30.minutes) | ||
_ <- repeat(doWork, 5.minutes) | ||
_ <- after(2.hours, shutdown) | ||
} yield () | ||
Schedule.forever = Schedule.unfold(0)(_ + 1) | ||
``` | ||
|
||
These effects, when run, start all at once in parallel, despite being composed this way. | ||
Building a schedule works with any `F[_]`. Running it, however, puts some constraints on it: | ||
Like forever, but with an initial delay | ||
```scala | ||
Schedule.after(delay: FiniteDuration): Schedule[F, Any, Int] | ||
``` | ||
|
||
Fixed number of occurences | ||
```scala | ||
def run[F[_]: Monad: Timer, G[_], A](schedule: Schedule[F, A])(implicit P: Parallel[F, G]): F[Unit] | ||
Schedule.occurs(times: Int): Schedule[F, Any, Int] | ||
``` | ||
|
||
It is also important for the effect type to be lazy and referentially transparent: repeating won't work with, say, `Future`. | ||
Fixed spacing between occurences (not considering the time the effect takes to produce an output) | ||
```scala | ||
Schedule.spaced(delay: FiniteDuration): Schedule[F, Any, Int] | ||
``` | ||
|
||
If we have picked `cats.effect.IO`, for example, then the following imports and implicits are required to run our example schedule. | ||
The identity schedule returns the latest output of the effect. | ||
```scala | ||
Schedule.identity[A]: Schedule[F, A, A] | ||
``` | ||
|
||
Recur while a predicate holds | ||
```scala | ||
import cats.effect.IO | ||
Schedule.whileInput[A](p: A => Boolean): Schedule[F, A, Int] | ||
``` | ||
|
||
val ec = scala.concurrent.ExecutionContext.global | ||
implicit val timer = IO.timer(ec) | ||
// The ContextShift instance is needed for deriving a `Parallel` instance via IO.ioParallel | ||
implicit val contextShift = IO.contextShift(ec) | ||
Recur until a predicate holds | ||
```scala | ||
Schedule.untilInput[A](p: A => Boolean): Schedule[F, A, Int] | ||
``` | ||
|
||
val result: IO[Unit] = run(schedule) | ||
Linearly increasing delays | ||
```scala | ||
Schedule.linear(unit: FiniteDuration): Schedule[F, Any, FiniteDuration] | ||
``` | ||
|
||
Increase delays according to the fibonacci sequence | ||
```scala | ||
Schedule.fibonacci(one: FiniteDuration): Schedule[F, Any, FiniteDuration] | ||
``` | ||
|
||
Exponentially increasing delays | ||
```scala | ||
Schedule.exponential(unit: FiniteDuration, base: Double = 2.0): Schedule[F, Any, FiniteDuration] | ||
``` | ||
|
||
|
||
## Combinators | ||
|
||
Add an initial delay to a schedule, it does not change anything else | ||
```scala | ||
#after(delay: FiniteDuration): Schedule[F, A, B] | ||
``` | ||
|
||
Reconsider the decision of a schedule. The boolean output of the function will be used to determine whether the schedule should continue. | ||
```scala | ||
#reconsider(f: Decision[State, B] => Boolean): Schedule[F, A, B] | ||
``` | ||
|
||
Fold over the outputs of a schedule. Changes the output type of the schedule. | ||
```scala | ||
#fold[Z](z: Z)((Z, B) => Z): Schedule[F, A, Z] | ||
``` | ||
|
||
Collect the outputs of a schedule (implemented via fold) | ||
```scala | ||
#collect: Schedule[F, A, List[B]] | ||
``` | ||
|
||
The intersection of two schedules continue only if both schedules want to continue and uses the maximum of the delays. | ||
|
||
```scala | ||
(Schedule[F, A, B] && Schedule[F, A, C]): Schedule[F, A, (B, C)] | ||
``` | ||
|
||
The union of two schedules is the opposite: it stops only when both of the schedules want to stop and uses the minimum of the delays | ||
|
||
```scala | ||
(Schedule[F, A, B] || Schedule[F, A, C]): Schedule[F, A, (B, C)] | ||
``` | ||
|
||
The `<*` operator works like `&&` but it only keeps the output of the schedule on the left hand side. | ||
```scala | ||
(Schedule[F, A, B] <* Schedule[F, A, C]): Schedule[F, A, B] | ||
``` | ||
|
||
The `*>` operator works like `&&` but it only keeps the output of the schedule on the right hand side. | ||
```scala | ||
(Schedule[F, A, B] *> Schedule[F, A, C]): Schedule[F, A, C] | ||
``` | ||
|
||
A schedule can be executed after the other with the `andAfterThat` operator. If two schedules are combined this way, then first the schedule on the left hand side is used until it terminates, and from there the schedule on the right hand side will be used. | ||
```scala | ||
(Schedule[F, A, B] andAfterThat Schedule[F, A, C]): Schedule[F, A, Either[B, C]] | ||
``` | ||
|
||
Schedules also have function-like composition. | ||
|
||
```scala | ||
(Schedule[F, A, B] >>> Schedule[F, B, C]): Schedule[F, A, C] | ||
(Schedule[F, A, B] <<< Schedule[F, A0, A]): Schedule[F, A0, C] | ||
``` | ||
|
||
## An example similar to the one mentioned in [John A. DeGoes's talk on ZIO Schedules] | ||
|
||
Produce a schedule that starts with an exponential spacing from 10 millis and after it reaches 60 seconds it switches over to a fixed 60s delay, but it will do that only up to 100 times, emitting the list of collected outputs from the effect. | ||
|
||
```scala | ||
(Schedule.exponential(10 millis).reconsider(_.delay < 60 seconds) | ||
andAfterThat (Schedule.spaced(60 seconds) && Schedule.occurs(100))) | ||
*> Schedule.collect | ||
``` | ||
|
||
|
||
|
||
|
||
[ZIO Schedule]: https://scalaz.github.io/scalaz-zio/datatypes/schedule.html | ||
[John A. DeGoes's talk on ZIO Schedules]: https://www.youtube.com/watch?v=onQSHiafAY8 |