-
Couldn't load subscription status.
- Fork 1.4k
Schedule rework #1798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Schedule rework #1798
Conversation
|
@mschuwalow Thanks for taking the lead on this! This is really great work and goes a long way to cleaning up a complicated part of the codebase with a lot of dependencies. The main concern I have with the current implementation is that it pushes more work onto the user to specify whether a ZIO.effectTotal(println("Hello, World!")).repeat(ZSchedule.fixed(1.second))In the current version of ZIO, this would print "Hello, World!" to the console repeatedly with a one second delay. In this PR, that line would instead immediately print "Hello, World!" an infinite number of times with no delay. To get an actual delay we need to instead do: ZIO.effectTotal(println("Hello, World!")).repeat(ZSchedule.fixed(1.second).addDelay)And the user in general has to do that every time they call a I think broadly this PR goes in the right direction by continuing to model a schedule as a "description" of a series of steps with delays rather than in each step doing the delay, at least until the schedule is "run" with What I think we need to add is using the type system more to automatically "derive" whether a schedule should use If that is right then I think we conceptually end up at a similar place to #1720 (schedules are descriptions of a series of delays that include an instruction for how a client can "run" those delays). And then I think we have to see how different they. If schedules really are descriptions of a series of delays with a way for a client to effectually "run" them to potentially actually do a "sleep", then we can implement all downstream method with almost no change and remove the |
I too am not a fan of this. It impacts user-land ergonomics enormously. What is wrong with just sleeping? Seems like you almost had that working at the hackathon. Run into more problems there??? |
I guess the one that is specifically proving challenging is jittered. @jdegoes Do you have an idea how to express this? |
|
Isn’t the problem more fundamental? Take the example of: ZSchedule.spaced(10.seconds).modifyDelay(_ / 2)If a schedule just describes delays, so each step doesn’t actually sleep but returns some state indicating how long to sleep this is easy because we just divide the number by two. But if a schedule “sleeps” then how do we implement this? We just have an opaque effect that we can either do nothing with or run, in which case we actually sleep for 10 seconds and it is already too late to achieve the goal of sleeping for only 5 seconds. By converting schedules from descriptions to actions we have lost some ability to compose them. We could instead implement |
|
Just throwing something out here. What if we add a delay "instruction", represented by an ADT that is parameterized by Something like ... sealed trait Something[-R]
object Something {
case object DoesNothing extends Something[Any]
case object AddsDelay(delay: Duration) extends Something[Clock]
}Keep this on the decision, and you can both introspect it and translate it to a ZIO while keeping the R precise. What do you think @mschuwalow @adamgfraser ? |
|
@iravid Yes that is exactly the direction I think we need to go in. Here is what I had in #1720 that does this. Obviously we could change the names. sealed trait Delay[-R] extends (Duration => ZIO[R, Nothing, Unit]) {
def combine[R1 <: R](that: Delay[R1]): Delay[R1]
}
final case object Live extends Delay[Clock] {
def apply(d: Duration): ZIO[Clock, Nothing, Unit] =
clock.sleep(d)
def combine[R1 <: Clock](that: Delay[R1]): Delay[R1] =
this
}
final case object Mock extends Delay[Any] {
def apply(d: Duration): ZIO[Any, Nothing, Unit] =
ZIO.unit
def combine[R1 <: Any](that: Delay[R1]): Delay[R1] =
that
}In that version the length of the delay is a separate field in |
|
@adamgfraser It seems with that encoding, you will always end up writing sealed abstract class Decision[R, A, B](cont: Boolean, state: A, finish: () => B) {
def delay: ZIO[R, Nothing, Unit] = ZIO.unit
}
final case class Sleep(duration: Duration, cont: Boolean, state: A, finish: () => B) extends Decision[Clock, A, B](cont, state, finish) {
def delay = clock.sleep(duration)
}
final case class At(instant: Instant, cont: Boolean, state: A, finish: () => B) extends Decision[Clock, A, B](cont, state, finish) {
def delay = clock.now.map(instant.toEpochMillis - _).flatMap(n => if (n <= 0) ZIO.unit else clock.sleep(n))
} |
|
@regiskuckaertz nice!! |
|
@regiskuckaertz Yes you could definitely do that. You end up not having to write it very often because you do it in a few key combinators like |
|
One advantage of this approach is that it should be possible to implement all the methods built on top of |
|
I really like @regiskuckaertz encoding if we want to go with this style. Especially the At decision would be a very nice way to tackle the cron stuff |
|
Also adding a case that cannot make progress anymore would fix `cont`:
```scala
sealed abstract class Decision[R, A, B](cont: Boolean)
Final case class Done[B](b: B) extends Decision[Any, Nothing, B](false)
```
but I’m not sure it would be useful.
|
|
Hm maybe we can combine the two approaches. What if we keep the encoding in this pr, which allows nice fold-based usage instead of cont, but introduce an trait ZSchedule0[-R, -A, +B] {
type State
type RunningState <: State
val initial: ZIO[R, Nothing, State]
val extract: RunningState => B
val update: (A, State) => ZIO[R, RunningState, Delayed[R, RunningState]]
}
object ZSchedule0 {
sealed abstract class Delayed[-R, +A] {
def delay: ZIO[R, Nothing, A]
}
final case class Sleep[A](duration: Duration, result: A) extends Delayed[Clock, A] {
val delay = clock.sleep(duration).as(result)
}
final case class At[A](instant: Instant, result: A) extends Delayed[Clock, A] {
val delay = clock.currentTime(TimeUnit.MILLISECONDS).map(instant.toEpochMilli - _).flatMap(n => if (n <= 0) ZIO.unit else clock.sleep(n.millis)).as(result)
}
}This also makes it much clearer that a schedule is not supposed to delay when it short circuits. We could even make this more polymorphic in order to make this more explicit, at the cost of another parameter: trait ZSchedule0[-R0, -R, -A, +B] {
type State
type RunningState <: State
val initial: ZIO[R0, Nothing, State]
val extract: RunningState => B
val update: (A, State) => ZIO[R0, RunningState, Delayed[R, RunningState]]
} |
If you make In general, We have seen in #676 that it is not sufficient to have a Essentially, I believe that losing
I would press forward with the generic approach. It results in less code, cleaner code, and yes, fewer operations, but a more capable data type. The alternative will be adding even more and highly irregular structure (you can see that emerging in #676). For |
|
Alright, I'll update the pr and get back to you |
|
True, you would probably end up special casing behavior for the different sleep styles all the time O.o There is one further issue I ran into with this encoding. The prime candidates where this is necessary are I believe the following encoding works for all we want to do, but does not enforce the correct usage as strongly. trait ZSchedule[-R, -A, +B] extends Serializable { self =>
type State
val initial: ZIO[R, Nothing, State]
val extract: (A, State) => B
val update: (A, State) => ZIO[R, Unit, State]
}Where initial returns the state to be paired with the first element and update($a_1, $state_1) returns state_2 and so on. Failure with () signals that the schedule should stop and extract with the current a. Alternatively we might be able to do this, but I don't know whether this is any better and I haven't tried it out. trait ZSchedule[-R, -A, +B] extends Serializable { self =>
type State
val initial: A => ZIO[R, Nothing, State]
val extract: State => B
val update: (A, State) => ZIO[R, Unit, State]
}What do you think? /cc @jdegoes |
|
I admit that just by having a cursory look, there are quite a few semantic and signature changes than I feel comfortable with. Are all of them necessitated by the changes required to support a specific |
|
In particular I should note that the |
|
We could merge #1720, which would remove the |
|
@mschuwalow What limitations on #1720 led to exploring a different encoding? |
|
@iravid @adamgfraser |
|
Ok, I think this is at a point where it can be reviewed. I'm still unsure about the specific encoding of schedule, but I believe the current semantics work out nicely for everything we want to do. |
|
I will take a detailed look this PR and the other one this week. The main goal is allowing schedules that run at absolute times, so we can define compositional cron-like languages; secondary goal would be eliminating I think the main goal (cron) is worth pushing in a "big enough" change to get this through (it is a long requested feature); ideally the reworking would result in something not harder to use than the old API. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes look good to me and I definitely like the new encoding! update(...).foldM is much nicer than the previous pattern of update, check the cont flag, delay manually...
Great work @mschuwalow!
Thanks for reviewing! I'll address your remarks tomorrow and also resolve conflicts with master |
|
I checked out this branch and can confirm it solves the particular issue I described in #1909. Thanks @mschuwalow! Looking forward to this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor comments but this is looking really amazing. I did not review Stream but leave that for @iravid.
| f: (A1, ZSchedule.Decision[State, B]) => UIO[ZSchedule.Decision[State, C]] | ||
| ): ZSchedule[R, A1, C] = | ||
| final def reconsider[R1 <: R, A1 <: A]( | ||
| f: (A1, Either[State, State]) => ZIO[R1, Unit, State] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't quite equivalent to the old reconsider, nor can it be without fixing the env.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not quite sure what we are losing compared to the old reconsider.
Can you explain in a little more detail?
| * that in `(op: IO[E, A]).repeat(Schedule.recurs(0)) `, op is not done at all. | ||
| */ | ||
| final def recurs(n: Int): Schedule[Any, Int] = forever.whileOutput(_ <= n) | ||
| final def recurs(n: Int): Schedule[Any, Int] = forever.whileOutput(_ < n) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Intentional change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly check subtly changed its semantics as we now have to extract before updating and not after.
This is a consequence of that change
|
@jdegoes thanks for the review and the kind feedback 🙏 |
…nt (zio#1798) * initial changes towards new schedule * wip * update encoding and begin cleanup * update docs * finish updating tests & cleanup * add all the type annotation for 2.11 * apply scalafmt * add js and jvm specific jittered syntax * update ZStream#fixed * fixes after rebase * update schedule docs * followup * merge master * implement ZSchedule#jittered in terms of ZEnv#map* and remove ZScheduleSyntax * readd modifyDelay * update tests and mdoc * add type annotations * please dotty * retrigger circle * retrigger circle * fix ZSchedule#never and add ZSchedule#randomDelay * followup * update random#nextLong for 2.11 * apply fmt * changes requested in review * add type parameters for 2.11 * update ZSchedule#ensuring implementation * remove unused import * ensure that ZSchedule#ensuring only runs the finalizer once * apply fmt * add type annotations for 2.11 * retrigger ci * retrigger ci * retrigger ci
…nt (zio#1798) * initial changes towards new schedule * wip * update encoding and begin cleanup * update docs * finish updating tests & cleanup * add all the type annotation for 2.11 * apply scalafmt * add js and jvm specific jittered syntax * update ZStream#fixed * fixes after rebase * update schedule docs * followup * merge master * implement ZSchedule#jittered in terms of ZEnv#map* and remove ZScheduleSyntax * readd modifyDelay * update tests and mdoc * add type annotations * please dotty * retrigger circle * retrigger circle * fix ZSchedule#never and add ZSchedule#randomDelay * followup * update random#nextLong for 2.11 * apply fmt * changes requested in review * add type parameters for 2.11 * update ZSchedule#ensuring implementation * remove unused import * ensure that ZSchedule#ensuring only runs the finalizer once * apply fmt * add type annotations for 2.11 * retrigger ci * retrigger ci * retrigger ci
Sorry for taking forever with this.
This is an initial version of the schedule changes. I'll definitely need to another pass to update docs, double check / cleanup implementations and unfuck Stream#aggregateWithinEither which I currently broke. I plan to spend some more time tomorrow to polish this.
Still I believe this is developed enough to take a look at the new api and decide whether we want to stick with it.
In general I think this is very clean to program with and we lost only very few combinators in the process.
One thing that is a bit unintuitive with the new encoding is building delays in a composable fashion. In order to make this easier I split up all time-based schedules into functions that produce non-sleeping schedules that emit durations and a function addDelay that allows to 'materialize' this into sleeps. This seems to work alright and feels very similar to programming with streams. I don't know whether that is the right approach though. I think @adamgfraser had ideas for other possible encodings.
@jdegoes @adamgfraser It would be great if you could take a look