2 releases

0.1.1 May 28, 2024
0.1.0 May 22, 2024

#639 in Web programming

49 downloads per month

MIT license

22KB
266 lines

Zagreus

Both a reference to the rebirth of minor greek deity and the rogue-ness of the videogame he protaganizes, Zagreus is a featureful library for retries, backoffs, and logging.

Please note this library still has some features missing and needs both examples and tests written.

Features:

  • asynchronous API
  • Runtime Independent & Zero Dependencies
  • Logging
  • Short circuiting on unrecoverable errors
  • Interval randomization
  • Zero Dependencies!

Usage:

There are four traits with which users of Zagreus must familiarize themselves:

  • BackoffStrategy: Defines the algorithm used to create retry intervals.
  • Randomizer: Adds randomization to intervals generated by the BackoffStrategy.
  • BackoffLogger: Exposes methods for logging errors encountered both within and at the end of the attempt loop.
  • BackoffHandler: Generates (and sleeps for) the amount of time produced by the BackoffStrategy as randomized by the Randomizer.

Start by creating a BackoffStrategy. This is very simple and involves only defining a limit() to the number of attempts iterations and an interval() method to calculate the time between said iterations.

Then, create a Randomizer. Use the library, bounds, and source of randomness of your choosing: implementors need only expose the randomize method, which takes an un-randomized Duration and returns a new one with the desired variance.

Finally, the implementation of a BackoffLogger will determine how, when, and which errors encountered during the attempt loop will be logged.

From this, each implementation will either be pased to the BackoffHandler or composed as a member of it to generate the desired behavior.

While the API surface is large for what it is, this crate is intended to be used as a building block to cover the majority of behavior one may desire in a retrying implementation. To wrangle this complexity it is recommended to hide implementors of BackoffHandler within another (new)type which has the parameters for handle() predefined and stored within itself for each given usecase.

FAQ:

Q: How may the interval between retries be capped?

A: Return None from BackoffStrategy::interval(). There are too many distinct ways to track time than would be sound for this crate to opine upon. Instead, implementors of BackoffStrategy may track the time from the first call to interval() to the final, and return None when this value exceeds some maximum that the developer is comfortable with.

While this means any cost associated with timing requests is opt-in, it also makes cancelling in-flight requests that exceed the time alotted by a given BackoffStrategy impossible.

Cancelling in-flight requests is outside the scope of this library and should be handled elsewhere.

Q: Why are BackoffHandler and BackoffStrategy separate traits?

A: It may be convenient to make a handler implement distinct strategies for distinct fallibles. Say one endpoint rate limits requests made sooner than one second apart and another two seconds. For yet another, performance contraints may demand the interval between attempts be capped at 500ms.

Additionally, If BackoffHandler and BackoffStrategy were one and the same each strategy used to address these constraints would need its own source of randomness: either contained within itself (and exposed by BackoffHandler::randomizer()) or part of the the program's global state. Both of these solutions have negative design and performance implications for asynchronous, highly concurrent applications. Splitting these traits in two allows for easy customization of behavior per-endpoint without resorting to sourcing per-endpoint randomness.

Q: Zagreus is so small (~200 LOC) but the API is so big/ugly/unweildy. Why?

A: This library is meant to expose a very broad amount of functionality in a manner that is both lightweight and runtime-independent. The onus of wrangling the APIs into something less annoying is on the user. Do not expect consumers of your types to interface with these traits directly, instead, implement types which wrap them, encapsulating common functionality and exposing only the subset of what's left.

No runtime deps

Features