reliakit-circuit 0.2.1

Clock-agnostic circuit breaker for fault isolation and fast failure. no_std and zero-dependency.
Documentation

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.2"

This crate is #![no_std] with no required dependencies. It has one optional feature, core (off by default), which pulls in reliakit-core and adds *_now(clock) convenience methods on CircuitBreaker and RollingBreaker backed by its Clock trait; the existing now: u64 methods are unchanged.

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).

Failure rate over a window: RollingBreaker

CircuitBreaker counts consecutive failures. When you want a failure rate — "trip if N of the last M calls failed" — use RollingBreaker<const WINDOW>, a const-generic variant that stores the last WINDOW outcomes inline (a [bool; WINDOW] ring, zero allocation, no_std). It shares the same cooldown and half-open recovery.

use reliakit_circuit::{RollingBreaker, State};

// Trip if 3 of the last 5 calls fail (not necessarily consecutive).
let mut breaker = RollingBreaker::<5>::new(3, 1_000);
breaker.on_failure(0);
breaker.on_success();
breaker.on_failure(0);
breaker.on_success();
breaker.on_failure(0); // 3 failures within the window
assert_eq!(breaker.state(), State::Open);

It exposes the same methods as CircuitBreaker plus window_size() and failures_in_window().

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.