sisyphus 0.1.0

Execution-agnostic, time-agnostic, no_std, zero-allocation backoff & retry state machine.
Documentation
  • Coverage
  • 100%
    33 out of 33 items documented9 out of 9 items with examples
  • Size
  • Source code size: 66.14 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 1.65 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 7s Average build duration of successful builds.
  • all releases: 7s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • niklabh/sisyphus
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • niklabh

sisyphus

CI crates.io docs.rs License: MIT OR Apache-2.0 no_std MSRV

A generic, execution-agnostic and time-agnostic backoff & retry utility — a pure state machine for no_std, zero-allocation environments.

Sisyphus was condemned by the gods to roll a boulder up a hill for eternity, only to watch it roll back down every time — the original retry loop. This crate is the part of that punishment worth keeping: it computes when to push again, and nothing else.

Why?

Most retry crates bake in std::time::Instant and std::thread::sleep, welding policy (how long to wait) to execution (how to wait). That breaks down in:

  • WebAssembly engines executing Wasm directly, with no host threads;
  • Deterministic blockchain / consensus state machines, where reading the wall clock or panicking is a fault;
  • no_std embedded targets with no allocator and no std::time.

sisyphus is a pure state machine instead:

  • #![no_std], zero allocation, no dyn (unless you opt in via alloc).
  • Time is just core::time::Duration; instants come from your Clock.
  • Randomness is injected via Jitter (deterministic by default) — great for reproducible P2P tests and avoiding thundering herds.
  • Execution is driven by your sleep — a sync closure or an async future, with no runtime lock-in (no Tokio dependency).
  • #![forbid(unsafe_code)].

Quick start

use core::ops::ControlFlow;
use core::time::Duration;
use sisyphus::{retry_sync, ExponentialBackoff, PolicyExt, RetryError};

// Compose pure state-machine policies.
let policy = ExponentialBackoff::new(Duration::from_millis(50), 2.0)
    .with_max_delay(Duration::from_secs(5))
    .max_attempts(4);

// Drive it with your own operation + sleep.
let mut tries = 0u32;
let result: Result<&str, RetryError<&str>> = retry_sync(
    policy,
    || {
        tries += 1;
        if tries >= 3 { ControlFlow::Break("connected") }
        else { ControlFlow::Continue("connection refused") }
    },
    |delay| { /* std::thread::sleep(delay) — or advance a virtual clock */ },
);

assert_eq!(result, Ok("connected"));

The ControlFlow contract

Your operation returns core::ops::ControlFlow<B, C>:

  • Break(value)terminal. The loop stops and returns Ok(value). Encode both success and fatal errors here (e.g. Break(Result<T, E>)).
  • Continue(state)transient. Retry after a backoff delay. If the policy gives up, the last state is returned in RetryError::Exhausted.

Async, runtime-free

retry_async(
    policy,
    || async { /* ... */ ControlFlow::Continue("transient") },
    |delay| tokio::time::sleep(delay), // or embassy_time::Timer, or a WASM timer
).await

retry_async is a plain async fn over core::future::Future; it never names a specific executor.

Building blocks

Item Role
BackoffPolicy trait: next_delay() -> Option<Duration> + reset()
ExponentialBackoff<J> exponential growth, saturating (never panics)
Constant fixed-interval delay
MaxAttempts<P> caps the number of retries
WithMaxDelay<P> clamps the maximum delay
MaxElapsedTime<P, C> gives up after a time budget (uses a Clock)
PolicyExt fluent combinators: .max_attempts(..), .with_max_delay(..), .max_elapsed_time(..)
Clock host-provided time source
Jitter / NoJitter / SplitMix64 injected randomness
retry_sync / retry_async execution drivers

Feature flags

feature default adds
alloc off BoxedPolicy + impl BackoffPolicy for Box<dyn …>
std off SystemClock backed by std::time::Instant

The default build pulls in neither: pure core, allocation-free.

Examples

Runnable examples live in examples/:

cargo run --example quick_start                 # sync retry with thread::sleep
cargo run --example custom_clock                # virtual Clock + elapsed-time budget
cargo run --example jitter                      # deterministic, reproducible jitter
cargo run --example async_retry                 # runtime-free async (hand-rolled block_on)
cargo run --example system_clock --features std # built-in SystemClock

Minimum supported Rust version (MSRV)

sisyphus supports Rust 1.66 and later. Bumping the MSRV is considered a minor, not patch, change.

License

Licensed under either of MIT or Apache-2.0 at your option.