This PR proposes a new API for handling retries and timeouts.
The Task abstraction
The Task abstraction represents "Future templates", or, referential transparent
wrappers around asynchronous code that can be turned into a Future by running them.
While this is from a theoretical stand-point a good feature to include, when using
gears I find them not very useful as-is, since the alternative of just directly
using Async ?=> T function types is both more straightforward and easier to compose.
Tasks however provide a good story about managing asynchronous tasks, which includes
running them with timeouts and a retry policy (including exponential back-offs, attempt counting etc.).
Unfortunately I don't think the API presented by Task currently is very nice at doing so:
Task requires you to provide the operation before the policy, making chaining awkward:
Also, chaining Task.schedule creates a new Task with opaque scheduling policies, making it difficult
to inspect which has been applied, as well as making it simple to accidentally nest scheduling policies
(something I believe is never intended).
Running a Taskrequires it to create a runningFuture[T], which specifically violates the high-level
structured concurrency API design of gears, forcing the user to remember another possible point of introducing
dangling, unstructured Futures.
(This, however, could've been easily remedied by just making .run a blocking/suspending operation.)
The Retry API
In contrast, the Retry API provides up-front declaration of the retrying policy, and lets the user specify the operation
at the last moment:
class Retry:
def apply[T](body: => T)(using Async): T // blocking, runs `body` with the configured Retry policy
while allowing the user to build up the Retry policy by method chaining:
Retry
.untilSuccess
.withMaximumFailures(5)
.withDelay(Delay.backoff(starting = 5.millis, maximum = 1.second)):
// the operation
This API intentionally does not deal with encapsulating async operations in any way: the call is supposed to be
blocking, so it is safe to capture the enclosing Async instance.
Currently Retry supports:
[x] forever, an infinite loop on the operation, with possible delays in between.
Note that boundary can be used to force an early break.
[x] untilSuccess and untilFailure.
Delay policies are open through the Retry.Delay trait, but some default implementations are provided: no delays, constant delays and exponential backoff (with jittering).
Sidenote: withTimeout
This PR also adds a simple withTimeout function that runs an asynchronous operation with a given timeout.
This can be manually implemented by running the operation in a Future and racing it a Timer (or another sleeping future),
however I think this is a worthy addition to the standard operations, and will probably need some support once #46 lands.
Progress
[x] Add a Retry module that performs operations with delays
What is this?
This PR proposes a new API for handling retries and timeouts.
The
Task
abstractionThe
Task
abstraction represents "Future templates", or, referential transparent wrappers around asynchronous code that can be turned into aFuture
byrun
ning them. While this is from a theoretical stand-point a good feature to include, when usinggears
I find them not very useful as-is, since the alternative of just directly usingAsync ?=> T
function types is both more straightforward and easier to compose.Task
s however provide a good story about managing asynchronous tasks, which includes running them with timeouts and a retry policy (including exponential back-offs, attempt counting etc.). Unfortunately I don't think the API presented byTask
currently is very nice at doing so:Task
requires you to provide the operation before the policy, making chaining awkward:Task.schedule
creates a newTask
with opaque scheduling policies, making it difficult to inspect which has been applied, as well as making it simple to accidentally nest scheduling policies (something I believe is never intended).Task
requires it to create a runningFuture[T]
, which specifically violates the high-level structured concurrency API design ofgears
, forcing the user to remember another possible point of introducing dangling, unstructured Futures. (This, however, could've been easily remedied by just making.run
a blocking/suspending operation.)The
Retry
APIIn contrast, the
Retry
API provides up-front declaration of the retrying policy, and lets the user specify the operation at the last moment:while allowing the user to build up the
Retry
policy by method chaining:This API intentionally does not deal with encapsulating async operations in any way: the call is supposed to be blocking, so it is safe to capture the enclosing
Async
instance.Currently
Retry
supports:forever
, an infinite loop on the operation, with possible delays in between.boundary
can be used to force an early break.untilSuccess
anduntilFailure
.Retry.Delay
trait, but some default implementations are provided: no delays, constant delays and exponential backoff (with jittering).Sidenote:
withTimeout
This PR also adds a simple
withTimeout
function that runs an asynchronous operation with a given timeout. This can be manually implemented by running the operation in aFuture
and racing it aTimer
(or another sleeping future), however I think this is a worthy addition to the standard operations, and will probably need some support once #46 lands.Progress
Retry
module that performs operations with delays