#retry #macro #tokio #async-std #sync #async-await #function

retrying

General-purpose retrying library for Rust with macros and functions

1 unstable release

0.1.0 Dec 19, 2023

#631 in Rust patterns

MIT/Apache

24KB
419 lines

retrying

General-purpose retrying library, written in Rust, to simplify the task of adding retry behavior to rust functions.
Support sync and async (tokio, async-std) functions.

[dependencies]
retrying = "0.1.0"

Macros

The main public interface in retrying is retrying::retry macros.

#[retrying::retry]
fn my_function(){}

This macros has a lot of configuration options and allows developers to write fault tolerant functions without thinking about implementation of retry functionality.
⚠️ The macros generates code and adds variables with prefix retrying_ into code. To avoid variable conflicts please don't use variables with this prefix in functions with retry macros.

Configuration option

  • Stop

This section describes configuration options that specify when method execution should stop retrying.

Config option OS Environments Default Description
stop=attempts(u32) {PREFIX}__RETRYING__STOP__ATTEMPTS - Number of retries
stop=delay(f32) {PREFIX}__RETRYING__STOP__DELAY - Retrying period (seconds)

It is possible to combine several stop conditions by using the or operator(|) operator. For example, configuration

#[retrying::retry(stop=(attempts(10)|delay(60.8)))]
fn my_function(){}

means the function should retry 10 times but doesn't make new attempt after 60 seconds.

If stop configuration is not specified then retry macros makes new attempts until function be finished without Err.

  • Wait

This section describes configuration options that specify delay between each attempt.

Config option OS Environments Default Description
wait=fixed(f32) {PREFIX}__RETRYING__WAIT__FIXED 0 Number of seconds between retries
wait=random(min=f32, max=f32) {PREFIX}__RETRYING__WAIT__RANDOM__(MIN|MAX) min=0,max=3600 Randomly wait min to max seconds between retries
wait=exponential(multiplier=f32, min=f32, max=f32, exp_base=u32) {PREFIX}__RETRYING__WAIT__EXPONENTIAL__(MULTIPLIER|MIN|MAX|EXP_BASE) multiplier=1, min=0, max=3600, exp_base=2 Wait multiplier * _exp_base_^(num of retry - 1) + min seconds between each retry starting with min seconds, then up to max seconds, then max seconds afterwards

Using only one wait option is possible.

  • Retry

This section describes configuration options that specify retrying conditions.

Config option OS Environments Default Description
retry=if_errors(error_1, error_2, error_2) Not applicable - Retry only on specific errors
retry=if_not_errors(error_1, error_2, error_3) Not applicable - Don't retry on specific errors

Using only one retry option is possible.

Using OS environment variables for updating retry configuration

There are certain list of use cases when retry configuration requires updating configuration values in runtime. For example, It is useful when we need a different number of attempts per environment (dev, prod, stage), systems, unit tests etc.

Retrying allows overriding macros configuration in runtime using env variables with special configuration option envs_prefix like

#[retrying::retry(<retry configurations>,envs_prefix="test")]

⚠️ Limitations

  • It is possible to override only configuration value, not configuration option. It means, for example, if configuration option stop=attempts(1)) is not defined in macros code then the OS env variable {PREFIX}__RETRYING__STOP__ATTEMPTS doesn't affect code execution. In other words, the OS environment variable can override only the value of the configured option and it is not able to change the option itself.
  • Configuration option from the OS environment variable has a higher priority than options in source code.
  • If OS environment variables are not set then macros uses the value from its configuration (source code).
  • If OS environment variable has the wrong format (for example, non-numeric value is specified for numeric configuration) then retrying macros ignores such configuration, logs error in stderr and continues using values from code.

Example of usage:

#[retrying::retry(stop=attempts(2),envs_prefix="test")]

With above configuration macros checks in runtime the availability of OS env variable TEST__RETRYING__STOP__ATTEMPTS (case-insensitive) and if variable is set then number of retry attempt will be the value of TEST__RETRYING__STOP__ATTEMPTS. If the list of OS environment contains more than one configuration option with the same prefix then macros ignores OS env variable and take configuration value from code.

Features

tokio - builds retrying library for using with tokio asynchronous runtime. async_std - builds retrying library for using with async_std asynchronous runtime.

Examples

Examples are available in ./crates/retrying/example and can be tested using cargo. Sync:

cargo run --example sync

Async tokio:

cargo run --features="tokio" --example tokio

Async async-std:

cargo run --features="async_std" --example async_std

Understanding of macros

retrying::retry macros is not a magic and it only helps developers to avoid writing extra code. The resulting code depends on the provided configuration and may be changed in future releases. Examples of code generation:

#[retry(stop=(attempts(4)|duration(2)),wait=fixed(0.9))]
fn my_method(in_param: &str) -> Result<i32, ParseIntError> {
    in_param.parse::<i32>()
}

Generated code:

fn my_method(in_param: &str) -> Result<i32, ParseIntError> {
    let mut retrying_context = ::retrying::RetryingContext::new();
    use ::retrying::stop::Stop;
    let retrying_stop =
        ::retrying::stop::StopAttemptsOrDuration::new(4u32, 2f32);
    use ::retrying::wait::Wait;
    let retrying_wait = ::retrying::wait::WaitFixed::new(0.9f32);
    loop {
        match { in_param.parse::<i32>() } {
            Ok(result) => return Ok(result),
            Err(err) if !retrying_stop.stop_execution(&retrying_context) => {
                match err {
                    std::num::ParseIntError { .. } => (),
                    _ => break Err(err),
                };
                retrying_context.add_attempt();
                ::retrying::sleep_sync(retrying_wait.wait_duration(&retrying_context));
            }
            Err(err) => break Err(err),
        }
    }
}

It is possible to use cargo for checking generated code. For examples, below command shows generated code for all examples,

cd ./crates/retrying
cargo rustc --profile=check --examples -- -Zunpretty=expanded

Dependencies

~0.6–12MB
~130K SLoC