diff --git a/README.md b/README.md index d03896a1..a1150560 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ the project! To test Ox, use the following dependency, using either [sbt](https://www.scala-sbt.org): ```scala -"com.softwaremill.ox" %% "core" % "0.3.0" +"com.softwaremill.ox" %% "core" % "0.3.1" ``` Or [scala-cli](https://scala-cli.virtuslab.org): ```scala -//> using dep "com.softwaremill.ox::core:0.3.0" +//> using dep "com.softwaremill.ox::core:0.3.1" ``` Documentation is available at [https://ox.softwaremill.com](https://ox.softwaremill.com), ScalaDocs can be browsed at [https://javadoc.io](https://www.javadoc.io/doc/com.softwaremill.ox). diff --git a/generated-doc/out/adr/0008-scheduled-repeat-retry.md b/generated-doc/out/adr/0008-scheduled-repeat-retry.md new file mode 100644 index 00000000..f3faca16 --- /dev/null +++ b/generated-doc/out/adr/0008-scheduled-repeat-retry.md @@ -0,0 +1,18 @@ +# 8. Retries + +Date: 2024-07-09 + +## Context + +How should the [retries](../retries.md) and [repeat](../repeat.md) APIs have the common implementation. + +## Decision + +We're introducing [scheduled](../scheduled.md) as a common API for both retries and repeats. + +In addition, `Schedule` trait and its implementations are decoupled from the retry DSL, so that they can be used for repeating as well. +`retry` API remains unchanged, but it now uses `scheduled` underneath. + +Also, `repeat` functions has been added as a sugar for `scheduled` with DSL focused on repeating. + +The main difference between `retry` and `repeat` is about interpretation of the duration provided by the `Schedule` (delay vs interval). diff --git a/generated-doc/out/basics/quick-example.md b/generated-doc/out/basics/quick-example.md index 4223ae4f..95d3ddc9 100644 --- a/generated-doc/out/basics/quick-example.md +++ b/generated-doc/out/basics/quick-example.md @@ -8,6 +8,7 @@ import ox.* import ox.either.ok import ox.channels.* import ox.resilience.* +import ox.scheduling.* import scala.concurrent.duration.* // run two computations in parallel @@ -35,7 +36,7 @@ supervised { // retry a computation def computationR: Int = ??? -retry(RetryPolicy.backoff(3, 100.millis, 5.minutes, Jitter.Equal))(computationR) +retry(RetryConfig.backoff(3, 100.millis, 5.minutes, Jitter.Equal))(computationR) // create channels & transform them using high-level operations supervised { diff --git a/generated-doc/out/basics/start-here.md b/generated-doc/out/basics/start-here.md index 578f4430..2f42b807 100644 --- a/generated-doc/out/basics/start-here.md +++ b/generated-doc/out/basics/start-here.md @@ -4,10 +4,10 @@ ```scala // sbt dependency -"com.softwaremill.ox" %% "core" % "0.3.0" +"com.softwaremill.ox" %% "core" % "0.3.1" // scala-cli dependency -//> using dep "com.softwaremill.ox::core:0.3.0" +//> using dep "com.softwaremill.ox::core:0.3.1" ``` ## Scope of the Ox project diff --git a/generated-doc/out/index.md b/generated-doc/out/index.md index 2d72f846..d6c3057d 100644 --- a/generated-doc/out/index.md +++ b/generated-doc/out/index.md @@ -50,6 +50,8 @@ In addition to this documentation, ScalaDocs can be browsed at [https://javadoc. oxapp io retries + repeat + scheduled resources control-flow utility diff --git a/generated-doc/out/io.md b/generated-doc/out/io.md index a0670895..06f0de1a 100644 --- a/generated-doc/out/io.md +++ b/generated-doc/out/io.md @@ -68,13 +68,13 @@ To use the plugin, add the following settings to your sbt configuration: ```scala autoCompilerPlugins := true -addCompilerPlugin("com.softwaremill.ox" %% "plugin" % "0.3.0") +addCompilerPlugin("com.softwaremill.ox" %% "plugin" % "0.3.1") ``` For scala-cli: ```scala -//> using plugin com.softwaremill.ox:::plugin:0.3.0 +//> using plugin com.softwaremill.ox:::plugin:0.3.1 ``` With the plugin enabled, the following code won't compile: diff --git a/generated-doc/out/kafka.md b/generated-doc/out/kafka.md index 875ac54b..348b92e7 100644 --- a/generated-doc/out/kafka.md +++ b/generated-doc/out/kafka.md @@ -3,7 +3,7 @@ Dependency: ```scala -"com.softwaremill.ox" %% "kafka" % "0.3.0" +"com.softwaremill.ox" %% "kafka" % "0.3.1" ``` `Source`s which read from a Kafka topic, mapping stages and drains which publish to Kafka topics are available through diff --git a/generated-doc/out/oxapp.md b/generated-doc/out/oxapp.md index 0d06e2cc..ca36e558 100644 --- a/generated-doc/out/oxapp.md +++ b/generated-doc/out/oxapp.md @@ -1,10 +1,10 @@ # OxApp -Ox provides a way to define application entry points in the "Ox way" using `OxApp` trait. Starting the app this way comes -with the benefit of the main `run` function being executed on a virtual thread, with a root `Ox` scope provided, -and application interruption handling built-in. The latter is handled using `Runtime.addShutdownHook` and will interrupt -the main virtual thread, should the app receive, for example, a SIGINT due to Ctrl+C being issued by the user. -Here's an example: +To properly handle application interruption and clean shutdown, Ox provides a way to define application entry points +using `OxApp` trait. The application's main `run` function is then executed on a virtual thread, with a root `Ox` scope +provided. + +Here's an example: ```scala import ox.* @@ -20,6 +20,23 @@ object MyApp extends OxApp: ExitCode.Success ``` +When the application receives a SIGINT/SIGTERM, e.g. due to a CTRL+C, the root scope (and hence any child scopes and +forks) are interrupted. This allows for a clean shutdown: any resources that are attached to scopes, or managed using +`try-finally` blocks, are released. Application shutdown is handled by adding a `Runtime.addShutdownHook`. + +In the code below, the resource is released when the application is interrupted: + +```scala +import ox.* + +object MyApp extends OxApp: + def run(args: Vector[String])(using Ox): ExitCode = + releaseAfterScope: + println("Releasing ...") + println("Waiting ...") + never +``` + The `run` function receives command line arguments as a `Vector` of `String`s, a given `Ox` capability and has to return an `ox.ExitCode` value which translates to the exit code returned from the program. `ox.ExitCode` is defined as: @@ -63,27 +80,29 @@ object MyApp extends OxApp.WithEitherErrors[MyAppError]: } def run(args: Vector[String])(using Ox, EitherError[MyAppError]): ExitCode = - doWork().ok() // will close the scope with MyAppError as `doWork` returns a Left + doWork().ok() // will end the scope with MyAppError as `doWork` returns a Left ExitCode.Success ``` ## Additional configuration -All `ox.OxApp` instances can be configured by overriding the `def settings: AppSettings` method. For now `AppSettings` -contains only the `gracefulShutdownExitCode` setting that allows one to decide what exit code should be returned by -the application once it gracefully shutdowns after it was interrupted (for example Ctrl+C was pressed by the user). +All `ox.OxApp` instances can be configured by overriding the `def settings: Settings` method. Settings include: + +* `interruptedExitCode`: what exit code should be returned by the application once it gracefully shutdowns after it + was interrupted (for example Ctrl+C was pressed by the user). By default `0` (graceful shutdown) +* `handleException` and `handleInterruptedException`: callbacks for exceptions that occur when evaluating the + application's body, or that are thrown when the application shuts down due to an interruption (SIGINT/SIGTERM). + By default, the stack traces are printed to stderr, unless a default uncaught exception handler is defined. -By default `OxApp` will exit in such scenario with exit code `0` meaning successful graceful shutdown, but it can be -overridden: +Settings can be overridden: ```scala import ox.* import scala.concurrent.duration.* -import OxApp.AppSettings object MyApp extends OxApp: - override def settings: AppSettings = AppSettings( - gracefulShutdownExitCode = ExitCode.Failure(130) + override def settings: OxApp.Settings = OxApp.Settings.Default.copy( + interruptedExitCode = ExitCode.Failure(130) ) def run(args: Vector[String])(using Ox): ExitCode = diff --git a/generated-doc/out/repeat.md b/generated-doc/out/repeat.md new file mode 100644 index 00000000..ad2a87ba --- /dev/null +++ b/generated-doc/out/repeat.md @@ -0,0 +1,80 @@ +# Repeat + +The `repeat` functions allow to repeat an operation according to a given schedule (e.g. repeat 3 times with a 100ms +interval and 50ms of initial delay). + +## API + +The basic syntax for `repeat` is: + +```scala +import ox.scheduling.repeat + +repeat(config)(operation) +``` + +The `repeat` API uses `scheduled` underneath with DSL focused on repeats. See [scheduled](scheduled.md) for more details. + +## Operation definition + +Similarly to the `retry` 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 `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 a custom `shouldContinueOnSuccess` 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` instead of `repeat`, which allows +full customization. + +### API shorthands + +You can use one of the following shorthands to define a `RepeatConfig` with a given schedule with an optional initial delay: +- `RepeatConfig.immediate(maxInvocations: Int, initialDelay: Option[FiniteDuration] = None)` +- `RepeatConfig.immediateForever[E, T](initialDelay: Option[FiniteDuration] = None)` +- `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)` + +See [scheduled](scheduled.md) for details on how to create custom schedules. + +## Examples + +```scala +import ox.UnionMode +import ox.scheduling.{Jitter, Schedule, repeat, repeatEither, repeatWithErrorMode, RepeatConfig} +import ox.resilience.{retry, RetryConfig} +import scala.concurrent.duration.* + +def directOperation: Int = ??? +def eitherOperation: Either[String, Int] = ??? +def unionOperation: String | Int = ??? + +// various operation definitions - same syntax +repeat(RepeatConfig.immediate(3))(directOperation) +repeatEither(RepeatConfig.immediate(3))(eitherOperation) + +// various schedules +repeat(RepeatConfig.fixedRate(3, 100.millis))(directOperation) +repeat(RepeatConfig.fixedRate(3, 100.millis, Some(50.millis)))(directOperation) + +// infinite repeats with a custom strategy +def customStopStrategy: Int => Boolean = ??? +repeat(RepeatConfig.fixedRateForever(100.millis).copy(shouldContinueOnResult = customStopStrategy))(directOperation) + +// custom error mode +repeatWithErrorMode(UnionMode[String])(RepeatConfig.fixedRate(3, 100.millis))(unionOperation) + +// repeat with retry inside +repeat(RepeatConfig.fixedRate(3, 100.millis)) { + retry(RetryConfig.backoff(3, 100.millis))(directOperation) +} +``` diff --git a/generated-doc/out/resources.md b/generated-doc/out/resources.md index 9fb9ab5d..6a40001e 100644 --- a/generated-doc/out/resources.md +++ b/generated-doc/out/resources.md @@ -16,6 +16,11 @@ useCloseable(new java.io.PrintWriter("test.txt")) { writer => If a concurrency scope is available (e.g. `supervised`), or there are multiple resources to allocate, consider using the approach described below, to avoid creating an additional syntactical scope. +```{warning} +To properly release resources when the entire application is interrupted, make sure to use [`OxApp`](oxapp.md) as the +application's main entry point. +``` + ## Within a concurrency scope Resources can be allocated within a concurrency scope. They will be released in reverse acquisition order, after all diff --git a/generated-doc/out/retries.md b/generated-doc/out/retries.md index 1e58aa67..d242f000 100644 --- a/generated-doc/out/retries.md +++ b/generated-doc/out/retries.md @@ -1,6 +1,6 @@ # Retries -The retries mechanism allows to retry a failing operation according to a given policy (e.g. retry 3 times with a 100ms +The retries mechanism allows to retry a failing operation according to a given configuration (e.g. retry 3 times with a 100ms delay between attempts). ## API @@ -10,9 +10,11 @@ The basic syntax for retries is: ```scala import ox.resilience.retry -retry(policy)(operation) +retry(config)(operation) ``` +The `retry` API uses `scheduled` underneath with DSL focused on retries. See [scheduled](scheduled.md) for more details. + ## Operation definition The `operation` can be provided directly using a by-name parameter, i.e. `f: => T`. @@ -20,9 +22,9 @@ The `operation` can be provided directly using a by-name parameter, i.e. `f: => There's also a `retryEither` variant which accepts a by-name `Either[E, T]`, i.e. `f: => Either[E, T]`, as well as one which accepts arbitrary [error modes](basics/error-handling.md), accepting the computation in an `F` context: `f: => F[T]`. -## Policies +## Configuration -A retry policy consists of three parts: +A retry config consists of three parts: - a `Schedule`, which indicates how many times and with what delay should we retry the `operation` after an initial failure, @@ -33,48 +35,6 @@ A retry policy consists of three parts: - a `onRetry`, which is a callback function that is invoked after each attempt to execute the operation. It is used to perform any necessary actions or checks after each attempt, regardless of whether the attempt was successful or not. -The available schedules are defined in the `Schedule` object. Each schedule has a finite and an infinite variant. - -### Finite schedules - -Finite schedules have a common `maxRetries: Int` parameter, which determines how many times the `operation` would be -retried after an initial failure. This means that the operation could be executed at most `maxRetries + 1` times. - -### Infinite schedules - -Each finite schedule has an infinite variant, whose settings are similar to those of the respective finite schedule, but -without the `maxRetries` setting. Using the infinite variant can lead to a possibly infinite number of retries (unless -the `operation` starts to succeed again at some point). The infinite schedules are created by calling `.forever` on the -companion object of the respective finite schedule (see examples below). - -### Schedule types - -The supported schedules (specifically - their finite variants) are: - -- `Immediate(maxRetries: Int)` - retries up to `maxRetries` times without any delay between subsequent attempts. -- `Delay(maxRetries: Int, delay: FiniteDuration)` - retries up to `maxRetries` times , sleeping for `delay` between - subsequent attempts. -- `Backoff(maxRetries: Int, initialDelay: FiniteDuration, maxDelay: FiniteDuration, jitter: Jitter)` - retries up - to `maxRetries` times , sleeping for `initialDelay` before the first retry, increasing the sleep between subsequent - attempts exponentially (with base `2`) up to an optional `maxDelay` (default: 1 minute). - - Optionally, a random factor (jitter) can be used when calculating the delay before the next attempt. The purpose of - jitter is to avoid clustering of subsequent retries, i.e. to reduce the number of clients calling a service exactly at - the same time, which can result in subsequent failures, contrary to what you would expect from retrying. By - introducing randomness to the delays, the retries become more evenly distributed over time. - - See - the [AWS Architecture Blog article on backoff and jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) - for a more in-depth explanation. - - The following jitter strategies are available (defined in the `Jitter` enum): - - `None` - the default one, when no randomness is added, i.e. a pure exponential backoff is used, - - `Full` - picks a random value between `0` and the exponential backoff calculated for the current attempt, - - `Equal` - similar to `Full`, but prevents very short delays by always using a half of the original backoff and - adding a random value between `0` and the other half, - - `Decorrelated` - uses the delay from the previous attempt (`lastDelay`) and picks a random value between - the `initalAttempt` and `3 * lastDelay`. - ### Result policies A result policy allows to customize how the results of the `operation` are treated. It consists of two predicates: @@ -107,16 +67,17 @@ Where: ### API shorthands -When you don't need to customize the result policy (i.e. use the default one), you can use one of the following -shorthands to define a retry policy with a given schedule (note that the parameters are the same as when manually -creating the respective `Schedule`): +When you don't need to customize the result policy (i.e. use the default one) or use complex schedules, +you can use one of the following shorthands to define a retry config with a given schedule: + +- `RetryConfig.immediate(maxRetries: Int)`, +- `RetryConfig.immediateForever`, +- `RetryConfig.delay(maxRetries: Int, delay: FiniteDuration)`, +- `RetryConfig.delayForever(delay: FiniteDuration)`, +- `RetryConfig.backoff(maxRetries: Int, initialDelay: FiniteDuration, maxDelay: FiniteDuration, jitter: Jitter)`, +- `RetryConfig.backoffForever(initialDelay: FiniteDuration, maxDelay: FiniteDuration, jitter: Jitter)`. -- `RetryPolicy.immediate(maxRetries: Int)`, -- `RetryPolicy.immediateForever`, -- `RetryPolicy.delay(maxRetries: Int, delay: FiniteDuration)`, -- `RetryPolicy.delayForever(delay: FiniteDuration)`, -- `RetryPolicy.backoff(maxRetries: Int, initialDelay: FiniteDuration, maxDelay: FiniteDuration, jitter: Jitter)`, -- `RetryPolicy.backoffForever(initialDelay: FiniteDuration, maxDelay: FiniteDuration, jitter: Jitter)`. +See [scheduled](scheduled.md) for details on how to create custom schedules. If you want to customize a part of the result policy, you can use the following shorthands: @@ -131,8 +92,8 @@ If you want to customize a part of the result policy, you can use the following ```scala import ox.UnionMode -import ox.resilience.{retry, retryEither, retryWithErrorMode} -import ox.resilience.{Jitter, ResultPolicy, RetryPolicy, Schedule} +import ox.resilience.{retry, retryEither, retryWithErrorMode, ResultPolicy, RetryConfig} +import ox.scheduling.{Jitter, Schedule} import scala.concurrent.duration.* def directOperation: Int = ??? @@ -140,27 +101,27 @@ def eitherOperation: Either[String, Int] = ??? def unionOperation: String | Int = ??? // various operation definitions - same syntax -retry(RetryPolicy.immediate(3))(directOperation) -retryEither(RetryPolicy.immediate(3))(eitherOperation) +retry(RetryConfig.immediate(3))(directOperation) +retryEither(RetryConfig.immediate(3))(eitherOperation) -// various policies with custom schedules and default ResultPolicy -retry(RetryPolicy.delay(3, 100.millis))(directOperation) -retry(RetryPolicy.backoff(3, 100.millis))(directOperation) // defaults: maxDelay = 1.minute, jitter = Jitter.None -retry(RetryPolicy.backoff(3, 100.millis, 5.minutes, Jitter.Equal))(directOperation) +// various configs with custom schedules and default ResultPolicy +retry(RetryConfig.delay(3, 100.millis))(directOperation) +retry(RetryConfig.backoff(3, 100.millis))(directOperation) // defaults: maxDelay = 1.minute, jitter = Jitter.None +retry(RetryConfig.backoff(3, 100.millis, 5.minutes, Jitter.Equal))(directOperation) // infinite retries with a default ResultPolicy -retry(RetryPolicy.delayForever(100.millis))(directOperation) -retry(RetryPolicy.backoffForever(100.millis, 5.minutes, Jitter.Full))(directOperation) +retry(RetryConfig.delayForever(100.millis))(directOperation) +retry(RetryConfig.backoffForever(100.millis, 5.minutes, Jitter.Full))(directOperation) // result policies // custom success -retry[Int](RetryPolicy(Schedule.Immediate(3), ResultPolicy.successfulWhen(_ > 0)))(directOperation) +retry[Int](RetryConfig(Schedule.Immediate(3), ResultPolicy.successfulWhen(_ > 0)))(directOperation) // fail fast on certain errors -retry(RetryPolicy(Schedule.Immediate(3), ResultPolicy.retryWhen(_.getMessage != "fatal error")))(directOperation) -retryEither(RetryPolicy(Schedule.Immediate(3), ResultPolicy.retryWhen(_ != "fatal error")))(eitherOperation) +retry(RetryConfig(Schedule.Immediate(3), ResultPolicy.retryWhen(_.getMessage != "fatal error")))(directOperation) +retryEither(RetryConfig(Schedule.Immediate(3), ResultPolicy.retryWhen(_ != "fatal error")))(eitherOperation) // custom error mode -retryWithErrorMode(UnionMode[String])(RetryPolicy(Schedule.Immediate(3), ResultPolicy.retryWhen(_ != "fatal error")))(unionOperation) +retryWithErrorMode(UnionMode[String])(RetryConfig(Schedule.Immediate(3), ResultPolicy.retryWhen(_ != "fatal error")))(unionOperation) ``` See the tests in `ox.resilience.*` for more. diff --git a/generated-doc/out/scheduled.md b/generated-doc/out/scheduled.md new file mode 100644 index 00000000..6454e02c --- /dev/null +++ b/generated-doc/out/scheduled.md @@ -0,0 +1,94 @@ +# Scheduled + +The `scheduled` functions allow to run an operation according to a given schedule. +It is preferred to use `repeat`, `retry`, or combination of both functions for most use cases, as they provide a more convenient DSL. +In fact `retry` and `repeat` use `scheduled` internally. + +## Operation definition + +Similarly to the `retry` and `repeat` APIs, 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 `scheduled` config consists of: +- a `Schedule`, which indicates how many times the `operation` should be run, provides a duration based on which + a sleep is calculated and provides an initial delay if configured. +- a `SleepMode`, which determines how the sleep between subsequent operations should be calculated: + - `Interval` - default for `repeat` operations, where the sleep is calculated as the duration provided by schedule + minus the duration of the last operation (can be negative, in which case the next operation occurs immediately). + - `Delay` - default for `retry` operations, where the sleep is just the duration provided by schedule. +- `onOperationResult` - a callback function that is invoked after each operation. Used primarily for `onRetry` in `retry` API. + +In addition, it is possible to define strategies for handling the results and errors returned by the `operation`: +- `shouldContinueOnError` - defaults to `_: E => false`, which allows to decide if the scheduler loop should continue + after an error returned by the previous operation. +- `shouldContinueOnSuccess` - defaults to `_: T => true`, which allows to decide if the scheduler loop should continue + after a successful result returned by the previous operation. + +## Schedule + +### Finite schedules + +Finite schedules have a common `maxRepeats: Int` parameter, which determines how many times the `operation` can be +repeated. This means that the operation could be executed at most `maxRepeats + 1` times. + +### Infinite schedules + +Each finite schedule has an infinite variant, whose settings are similar to those of the respective finite schedule, but +without the `maxRepeats` setting. Using the infinite variant can lead to a possibly infinite number of retries (unless +the `operation` starts to succeed again at some point). The infinite schedules are created by calling `.forever` on the +companion object of the respective finite schedule (see examples below). + +### Schedule types + +The supported schedules (specifically - their finite variants) are: + +- `InitialDelay(delay: FiniteDuration)` - used to configure the initial delay (the delay before the first invocation of + the operation, used in `repeat` API only). +- `Immediate(maxRepeats: Int)` - repeats up to `maxRepeats` times, always returning duration equal to 0. +- `Fixed(maxRepeats: Int, duration: FiniteDuration)` - repeats up to `maxRepeats` times, always returning + the provided `duration`. +- `Backoff(maxRepeats: Int, firstDuration: FiniteDuration, maxDuration: FiniteDuration, jitter: Jitter)` - repeats up + to `maxRepeats` times, returning `firstDuration` after the first invocation, increasing the duration between subsequent + invocations exponentially (with base `2`) up to an optional `maxDuration` (default: 1 minute). + + Optionally, a random factor (jitter) can be used when calculating the delay before the next invocation. The purpose of + jitter is to avoid clustering of subsequent invocations, i.e. to reduce the number of clients calling a service exactly at + the same time, which can result in subsequent failures, contrary to what you would expect from retrying. By + introducing randomness to the delays, the retries become more evenly distributed over time. + + See + the [AWS Architecture Blog article on backoff and jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) + for a more in-depth explanation. + + The following jitter strategies are available (defined in the `Jitter` enum): + - `None` - the default one, when no randomness is added, i.e. a pure exponential backoff is used, + - `Full` - picks a random value between `0` and the exponential backoff calculated for the current attempt, + - `Equal` - similar to `Full`, but prevents very short delays by always using a half of the original backoff and + adding a random value between `0` and the other half, + - `Decorrelated` - uses the delay from the previous attempt (`lastDelay`) and picks a random value between + the `initalAttempt` and `3 * lastDelay`. + +## Combining schedules + +It is possible to combine schedules using `.andThen` method. The left side schedule must be a `Finite` +or `InitialDelay` schedule and the right side can be any schedule. + +### Examples + +```scala +import ox.scheduling.Schedule +import scala.concurrent.duration.* + +// schedule 3 times immediately and then 3 times with fixed duration +Schedule.Immediate(3).andThen(Schedule.Fixed(3, 100.millis)) + +// schedule 3 times immediately and then forever with fixed duration +Schedule.Immediate(3).andThen(Schedule.Fixed.forever(100.millis)) + +// schedule with an initial delay and then forever with fixed duration +Schedule.InitialDelay(100.millis).andThen(Schedule.Fixed.forever(100.millis)) +```