reliakit-circuit 0.1.0

Clock-agnostic circuit breaker for fault isolation and fast failure. no_std and zero-dependency.
Documentation
  • Coverage
  • 100%
    18 out of 18 items documented1 out of 14 items with examples
  • Size
  • Source code size: 23.49 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 293.47 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 3s Average build duration of successful builds.
  • all releases: 2s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • satyakwok/reliakit
    2 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • satyakwok

reliakit-circuit

Crates.io Crates.io Downloads Docs.rs CI codecov License: MIT

A clock-agnostic circuit breaker for Rust.

When a dependency starts failing, retrying it immediately makes things worse: you pile load onto a service that is already struggling and make your own callers wait on calls that are almost certain to fail. A circuit breaker watches the recent outcome history and, once failures cross a threshold, "opens" — it rejects calls instantly (failing fast) for a cooldown period, then lets a single trial call through to check whether the dependency has recovered before resuming normal traffic.

reliakit-circuit implements that pattern as a small, Copy state machine with no dependencies, no std, and no hidden behavior. It does not read the clock, sleep, spawn tasks, or allocate — you pass the current time in and you make the call. That makes it equally usable from synchronous code, any async runtime, and no_std / embedded targets, and it makes every transition deterministic and trivial to unit-test.

How it works

A breaker moves between three states:

           failures >= failure_threshold
  Closed ───────────────────────────────▶ Open
    ▲                                       │
    │ successes >= success_threshold        │ cooldown elapsed
    │                                       ▼
    └────────────── HalfOpen ◀──────────────┘
                       │
                       │ any failure
                       └──────────────▶ Open
State Calls Behavior
Closed allowed Normal operation. Consecutive failures are counted; reaching failure_threshold trips the breaker to Open. A success resets the count.
Open rejected allow() returns false immediately. After cooldown time units the next allow() moves the breaker to HalfOpen.
HalfOpen allowed (trial) A probationary period. success_threshold consecutive successes close the breaker; the first failure reopens it (and restarts the cooldown).

Why "clock-agnostic"?

Most circuit breakers are tied to std::time::Instant or to a specific async runtime's timer. This one takes the current time as a u64 argument in whatever monotonic unit you choose (milliseconds is typical) and cooldown in that same unit. The benefits:

  • Runtime-neutral. Works under Tokio, async-std, blocking threads, or a bare metal loop — you decide where time comes from.
  • no_std-friendly. No clock dependency means it compiles for embedded targets (CI builds it for thumbv7em-none-eabi).
  • Deterministic & testable. Transitions depend only on the timestamps you pass, so tests assert exact behavior with no sleeping or mocking.

A clock that briefly moves backwards is handled with saturating arithmetic — the breaker simply stays open rather than panicking.

Installation

[dependencies]
reliakit-circuit = "0.1"

This crate is #![no_std] and has no feature flags; it depends only on core.

Usage

Wrap each call to a dependency in allow() / on_success() / on_failure():

use reliakit_circuit::CircuitBreaker;

// Trip after 5 consecutive failures; stay open for 10 seconds.
let mut breaker = CircuitBreaker::new(5, 10_000);

let now = now_millis(); // your own monotonic clock, in milliseconds
if breaker.allow(now) {
    match call_remote() {
        Ok(_)  => breaker.on_success(),
        Err(_) => breaker.on_failure(now),
    }
} else {
    // Fail fast: skip the call, serve a cached value or return an error.
}

Require several good calls before fully trusting the dependency again:

use reliakit_circuit::CircuitBreaker;

// Need 3 consecutive successes during the trial period to close.
let breaker = CircuitBreaker::new(5, 10_000).with_success_threshold(3);

See examples/basic.rs for a complete request loop that trips, rejects fast, recovers, and closes again.

API

Method Purpose
CircuitBreaker::new(failure_threshold, cooldown) Construct a breaker (const fn).
.with_success_threshold(n) Successes needed in HalfOpen to close (default 1).
allow(now) -> bool Whether a call may proceed; advances Open → HalfOpen when the cooldown elapses.
on_success() Record a successful call.
on_failure(now) Record a failed call.
state() -> State Current state (Closed / Open / HalfOpen).
trip(now) / reset() Force the breaker open or closed (e.g. from health signals).

Pairs well with reliakit-backoff

A circuit breaker decides whether to attempt a call; reliakit-backoff decides how long to wait before the next attempt. Used together, the breaker sheds load while a dependency is down and backoff spaces out the retries — both clock-agnostic, both no_std.

Concurrency

CircuitBreaker is a plain value and is not internally synchronized (no atomics — keeping it dependency-free and no_std). To share one across threads or tasks, wrap it in your own Mutex/lock. For per-task breakers, just give each its own copy.

When to use it

  • Calls to a remote service, database, or any dependency that can fail or stall.
  • Guarding a shared resource so one failing downstream does not cascade.
  • Embedded or runtime-agnostic code that still needs fail-fast behavior.

When not to use it

  • For a single retry with a delay, a backoff policy alone is enough — reach for reliakit-backoff.
  • It does not measure latency, error rates over sliding windows, or perform health checks on its own; it reacts to the success/failure outcomes you report.

Safety

This crate is #![forbid(unsafe_code)] and #![no_std]. All arithmetic saturates; no method panics on any input, including a non-monotonic clock.

Minimum Supported Rust Version

Rust 1.85 and newer. No nightly features are used.

License

Licensed under the MIT License. See LICENSE.