-
Notifications
You must be signed in to change notification settings - Fork 29
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
Scheduled and repeat API + retry refactoring #172
Conversation
policy.onRetry, | ||
shouldContinueOnError = policy.resultPolicy.isWorthRetrying, | ||
shouldContinueOnResult = t => !policy.resultPolicy.isSuccess(t), | ||
delayPolicy = DelayPolicy.SinceTheEndOfTheLastInvocation |
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.
shouldn't the delay policy be part of RetryPolicy
, so that it's also user-configurable if needed?
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.
My idea was to create a public API runScheduled*
with the all configurable options and make retry*
and repeat*
as "thin" as possible - basically an opinionated variants of runScheduled*
, specifically:
retry
- that behaves similarly to resillience4j retries, where sleep time is counted since the end of previous operation, has an optionalonRetry
callback and initial delay (sleep before first operation) is not relevantrepeat
- that behaves similarly to Akka'sSource.tick
, where sleep time is counted since the start of previous operation (interval), doesn't exposeonRepeat
callback (not relevant) and initial delay is allowed
WDYT?
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.
Ok, sounds good. Let's just document these defaults quite clearly, plus all the flexibility that runScheduled
(or maybe simply scheduled
?) gives you
import scala.concurrent.duration.FiniteDuration | ||
import scala.concurrent.duration.DurationInt | ||
|
||
case class RepeatConfig[E, T]( |
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.
we have RepeatConfig
, but RetryPolicy
- why?
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.
RetryPolicy
has been refactored to RetryConfig
private[scheduling] sealed trait Finite extends Schedule: | ||
def maxRepeats: Int | ||
def initialDelay: FiniteDuration = Duration.Zero | ||
def fallbackTo(fallback: Finite): Finite = FallingBack(this, fallback) |
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.
maybe .andThen
would be a better name?
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.
done
// TODO: shouldn't this trait be unsealed so that users can create their own schedules? | ||
sealed trait Schedule: | ||
// TODO: consider better name that `attempt` | ||
def nextDelay(attempt: Int, lastDelay: Option[FiniteDuration]): FiniteDuration |
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.
attempt - that's the attempt number, yes?
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.
For retries it is an attempt number, for repeats it's an repetition number. I'm still looking for some common name :)
import scala.util.Random | ||
|
||
// TODO: shouldn't this trait be unsealed so that users can create their own schedules? | ||
sealed trait Schedule: |
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.
let's keep it sealed for now, and wait for a use-case to unseal it ;)
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.
todo removed
enum DelayPolicy: | ||
case SinceTheStartOfTheLastInvocation, SinceTheEndOfTheLastInvocation | ||
|
||
case class RunScheduledConfig[E, T]( |
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.
these classes/methods can all be private[ox]
I suppose? they're not meant to be used by the end-user?
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.
it has been decided that the scheduled
and ScheduledConfig
will be public API
Looks good but it would be great to see some examples of the API in action (in docs, or stand-alone) to see how the API can be used |
import scala.util.Random | ||
|
||
sealed trait Schedule: | ||
// TODO: consider better name that `attempt` |
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.
iteration?
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.
Yes, I think it would be the best in that case
/** The mode that specifies how to interpret the duration provided by the schedule. */ | ||
enum SleepMode: | ||
/** Interval (since the start of the last operation), i.e. guarantees that the next operation will start no sooner than the specified | ||
* duration after the previous operations has finished. If the previous operation takes longer than the interval, the next operation will |
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.
started, not finished?
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.
fixed
@@ -0,0 +1,18 @@ | |||
# 8. Retries |
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.
Good idea to add an ADR :)
doc/repeat.md
Outdated
The `repeat` config requires a `Schedule`, which indicates how many times and with what interval should the `operation` | ||
be repeated. | ||
|
||
In addition, it is possible to define strategies for handling the results and errors returned by the `operation`: |
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.
question: Shouldn't we have a stronger division between retry
, which is related to "trying and possibly failing" and repeat
, which has no notion of errors? I think it would be simpler from the user perspective, otherwise they may be confused when to use retry and when to use repeat + shouldContinueOnError
.
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.
Right, I don't think that shouldContinueOnError
handler would be used often.
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.
shouldContinueOnError
removed and mentioned that repeat
can be used with retry
+ example
import scala.util.Try | ||
|
||
/** Retries an operation returning a direct result until it succeeds or the policy decides to stop. | ||
/** Retries an operation returning a direct result until it succeeds or the config decides to stop. |
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 think I'd also mention here that retry
is a special-case of schedule
, with a given set of defaults (and enumarte these defaults). Maybe this should be in the @see
section?
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.
done
import scala.util.Random | ||
|
||
sealed trait Schedule: | ||
def nextDuration(iteration: Int, lastDuration: Option[FiniteDuration]): FiniteDuration |
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.
or ... we're using invocation
in the docs quite often, so maybe that would be a good name as well?
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.
good idea!
private[scheduling] object FiniteAndFiniteSchedules: | ||
/** A schedule that repeats indefinitely, using [[first]] first [[first.maxRepeats]] number of times, and then always using [[second]]. | ||
*/ | ||
def forever(first: Finite, second: Infinite): Infinite = FiniteAndInfiniteSchedules(first, second) |
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.
is this correct that "finite and finite" schedule contains a method which takes a Finite
and Infinite
schedule?
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.
yes, it doesn't make sense after refactoring. Fixed!
- `RepeatConfig.fixedRate[E, T](maxInvocations: Int, interval: FiniteDuration, initialDelay: Option[FiniteDuration] = None)` | ||
- `RepeatConfig.fixedRateForever[E, T](interval: FiniteDuration, initialDelay: Option[FiniteDuration] = None)` | ||
- `RepeatConfig.backoff[E, T](maxInvocations: Int, firstInterval: FiniteDuration, maxInterval: FiniteDuration = 1.minute, jitter: Jitter = Jitter.None, initialDelay: Option[FiniteDuration] = None)` | ||
- `RepeatConfig.backoffForever[E, T](firstInterval: FiniteDuration, maxInterval: FiniteDuration = 1.minute, jitter: Jitter = Jitter.None, initialDelay: Option[FiniteDuration] = None)` |
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.
it would be good to give an example of combinining schedules using .andThen
(here and in the code snippet)
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.
added some examples to scheduled.md
and See [scheduled](scheduled.md) for details on how to create custom schedules.
to repeat.md
and retries.md
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.
Just some minor adjustments, but looks great otherwise!
closes #57
Summary:
retry*
API has become more generic and renamed toscheduled*
retry*
API usesscheduled*
internally and retry-specific DSL has remained unchangedrepeat*
API also uses scheduled*` internally with repeat-specific DSLscheduled
API,Schedule
trait and its implementations should be decoupled fromretry
API, specifically it calculates "duration" instead of "delay", which can later be interpreted as a delay or interval.scheduled
implementation is covered by tests forretry
andrepeat