Skip to main content

reliakit_retry/
lib.rs

1//! Small, runtime-agnostic retry helpers for fallible operations.
2//!
3//! `reliakit-retry` turns a [`Backoff`] schedule and an attempt limit into a
4//! [`RetryPolicy`], then drives a fallible operation against it — synchronously
5//! or asynchronously. It is deliberately minimal: it decides *whether* to retry
6//! and *how long* the gap should be, but it never sleeps, spawns, or assumes an
7//! async runtime. You inject the waiting.
8//!
9//! It has no third-party dependencies, forbids unsafe code, and is
10//! `no_std`-friendly (it needs no allocation and no clock).
11//!
12//! # Why it does not sleep
13//!
14//! Blocking the current thread (`std::thread::sleep`) or awaiting a runtime
15//! timer is hidden runtime behavior, and it ties a small helper to one
16//! execution model. Instead:
17//!
18//! - [`retry`] runs attempts back-to-back and never waits.
19//! - [`retry_with_sleep`] hands each backoff [`Duration`](core::time::Duration)
20//!   to a `sleep` closure *you* provide (e.g. one that calls your timer).
21//! - [`retry_async`] awaits a `sleep` future *you* provide, so it works under
22//!   any executor without depending on Tokio, async-std, or `futures`.
23//!
24//! # Attempt counting
25//!
26//! [`RetryPolicy::max_attempts`] is the *total* number of attempts, including
27//! the first:
28//!
29//! - `max_attempts = 1` → try once, never retry (the backoff is never used).
30//! - `max_attempts = 3` → the first try plus up to two retries.
31//! - `max_attempts = 0` is rejected by [`RetryPolicy::new`] (returns `None`).
32//!
33//! The attempt count is the single authority for how many times the operation
34//! runs. The [`Backoff`] is consulted only for the delay *before each retry*
35//! (retry `0` is the first retry, zero-based); if it yields no delay, the gap is
36//! [`Duration::ZERO`](core::time::Duration::ZERO). The two limits therefore
37//! never conflict.
38//!
39//! # Retry predicate
40//!
41//! Every helper takes a `should_retry: FnMut(&E) -> bool` classifier. Returning
42//! `false` stops immediately — use it to retry only transient errors and fail
43//! fast on permanent ones. It is consulted only when another attempt is actually
44//! possible (so it is never called when `max_attempts` is already reached).
45//!
46//! # Example — sync, no sleeping
47//!
48//! ```
49//! use core::time::Duration;
50//! use reliakit_retry::{retry, Backoff, RetryError, RetryPolicy};
51//!
52//! let policy = RetryPolicy::new(3, Backoff::constant(Duration::from_millis(10))).unwrap();
53//!
54//! let mut calls = 0;
55//! let result: Result<u32, RetryError<&str>> = retry(
56//!     &policy,
57//!     || {
58//!         calls += 1;
59//!         if calls < 2 { Err("temporary") } else { Ok(42) }
60//!     },
61//!     |_error| true, // retry every error
62//! );
63//!
64//! assert_eq!(result.unwrap(), 42);
65//! assert_eq!(calls, 2);
66//! ```
67//!
68//! # Example — sync, with an injected sleeper
69//!
70//! ```
71//! use core::time::Duration;
72//! use reliakit_retry::{retry_with_sleep, Backoff, RetryError, RetryPolicy};
73//!
74//! let policy = RetryPolicy::new(4, Backoff::exponential(Duration::from_millis(1), 2)).unwrap();
75//!
76//! // Record the delays instead of really sleeping (a real caller would wait).
77//! let mut waited: Vec<Duration> = Vec::new();
78//! let mut attempts = 0;
79//! let result: Result<(), RetryError<&str>> = retry_with_sleep(
80//!     &policy,
81//!     || { attempts += 1; Err("always fails") },
82//!     |_error| true,
83//!     |delay| waited.push(delay),
84//! );
85//!
86//! assert!(matches!(result, Err(RetryError::Exhausted { attempts: 4, .. })));
87//! // Three gaps before retries 2, 3, 4: 1ms, 2ms, 4ms.
88//! assert_eq!(waited, [Duration::from_millis(1), Duration::from_millis(2), Duration::from_millis(4)]);
89//! ```
90//!
91//! For the async helper, see [`retry_async`] and the `async_retry` example,
92//! which drives it without any runtime.
93//!
94//! # Feature flags
95//!
96//! - `std` (default) adds `impl std::error::Error for RetryError`. With
97//!   `--no-default-features` the crate is pure `core`: no allocation, no clock,
98//!   no runtime.
99//!
100//! # What this is not
101//!
102//! This is a small retry helper, not a framework, middleware stack, async
103//! runtime, or a Tower replacement. It does not log, spawn, time, or schedule on
104//! your behalf.
105
106#![cfg_attr(not(feature = "std"), no_std)]
107#![forbid(unsafe_code)]
108#![warn(missing_docs)]
109
110mod error;
111mod policy;
112mod retry;
113
114pub use error::RetryError;
115pub use policy::RetryPolicy;
116pub use retry::{retry, retry_async, retry_with_sleep};
117
118/// Re-exported from `reliakit-backoff` so the backoff schedule is reachable
119/// without a separate dependency line.
120pub use reliakit_backoff::Backoff;