use crate::error::Error;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Breaker {
pub first: Duration,
pub longest: Duration,
pub settled: Duration,
}
impl Default for Breaker {
fn default() -> Self {
Self {
first: Duration::from_secs(1),
longest: Duration::from_secs(60),
settled: Duration::from_secs(300),
}
}
}
impl Breaker {
pub fn new(first: Duration, longest: Duration, settled: Duration) -> Self {
Self {
first,
longest,
settled,
}
}
pub fn opening_for(&self, failures: u32, error: &Error) -> Option<Duration> {
match error {
Error::RateLimited {
retry_after: Some(wait),
} => Some(*wait),
Error::RateLimited { retry_after: None } => Some(self.backoff(failures)),
Error::Transient(_) | Error::Timeout { .. } => Some(self.backoff(failures)),
Error::Auth(_) | Error::NotFound(_) => Some(self.settled),
Error::Refused { .. }
| Error::InvalidRequest(_)
| Error::Unsupported(_)
| Error::Unreadable(_)
| Error::OverBudget(_) => None,
}
}
fn backoff(&self, failures: u32) -> Duration {
let doublings = failures.saturating_sub(1).min(32);
let scaled = self
.first
.checked_mul(1u32.checked_shl(doublings).unwrap_or(u32::MAX))
.unwrap_or(self.longest);
scaled.min(self.longest)
}
}
#[derive(Debug, Default)]
pub(crate) struct Health {
closed_until: AtomicU64,
consecutive_failures: AtomicU32,
denied: AtomicBool,
}
impl Health {
pub(crate) fn closed_for(&self, since: Instant) -> Option<Duration> {
let until = self.closed_until.load(Ordering::Relaxed);
if until == 0 {
return None;
}
let now = millis(since);
(until > now).then(|| Duration::from_millis(until - now))
}
pub(crate) fn failures(&self) -> u32 {
self.consecutive_failures.load(Ordering::Relaxed)
}
pub(crate) fn failed(&self) -> u32 {
self.consecutive_failures
.fetch_add(1, Ordering::Relaxed)
.saturating_add(1)
}
pub(crate) fn rest(&self, breaker: &Breaker, failures: u32, error: &Error, since: Instant) {
if let Some(wait) = breaker.opening_for(failures, error) {
self.close_for(wait, since);
}
}
pub(crate) fn close_for(&self, wait: Duration, since: Instant) {
let capped = u64::try_from(wait.as_millis()).unwrap_or(u64::MAX);
self.closed_until
.store(millis(since).saturating_add(capped), Ordering::Relaxed);
}
pub(crate) fn denied_by(&self, wait: Duration, since: Instant) {
self.denied.store(true, Ordering::Relaxed);
self.close_for(wait, since);
}
pub(crate) fn why(&self, left: Duration) -> String {
if self.denied.load(Ordering::Relaxed) {
return format!(
"denied at preflight, not tried again for another {}ms",
left.as_millis()
);
}
format!(
"circuit open for another {}ms after {} failures",
left.as_millis(),
self.failures()
)
}
pub(crate) fn served(&self) {
self.consecutive_failures.store(0, Ordering::Relaxed);
self.closed_until.store(0, Ordering::Relaxed);
self.denied.store(false, Ordering::Relaxed);
}
}
fn millis(since: Instant) -> u64 {
u64::try_from(since.elapsed().as_millis()).unwrap_or(u64::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_failure_about_the_request_never_opens_the_circuit() {
let breaker = Breaker::default();
assert_eq!(
breaker.opening_for(5, &Error::Refused { category: None }),
None
);
assert_eq!(
breaker.opening_for(5, &Error::InvalidRequest("bad".into())),
None
);
assert_eq!(
breaker.opening_for(5, &Error::Unsupported("no tools".into())),
None
);
assert_eq!(
breaker.opening_for(5, &Error::Unreadable("no content".into())),
None
);
}
#[test]
fn a_rejected_credential_is_left_alone_for_a_long_time() {
let breaker = Breaker::default();
assert_eq!(
breaker.opening_for(1, &Error::Auth("401".into())),
Some(breaker.settled)
);
assert_eq!(
breaker.opening_for(9, &Error::Auth("401".into())),
Some(breaker.settled),
"and it does not grow: it was already settled the first time"
);
}
#[test]
fn a_rate_limit_is_honoured_exactly_rather_than_guessed_at() {
let breaker = Breaker::default();
let told = Error::RateLimited {
retry_after: Some(Duration::from_secs(90)),
};
assert_eq!(breaker.opening_for(1, &told), Some(Duration::from_secs(90)));
assert!(Duration::from_secs(90) > breaker.longest);
let untold = Error::RateLimited { retry_after: None };
assert_eq!(breaker.opening_for(1, &untold), Some(breaker.first));
}
#[test]
fn the_backoff_doubles_and_then_stops() {
let breaker = Breaker::default();
let down = Error::Transient("503".into());
let wait = |n| breaker.opening_for(n, &down);
assert_eq!(wait(1), Some(Duration::from_secs(1)));
assert_eq!(wait(2), Some(Duration::from_secs(2)));
assert_eq!(wait(4), Some(Duration::from_secs(8)));
assert_eq!(
wait(1_000_000),
Some(breaker.longest),
"a provider down for an hour is still re-checked every minute"
);
}
#[test]
fn a_route_that_answers_forgets_everything_it_was_holding() {
let since = Instant::now();
let health = Health::default();
let breaker = Breaker::default();
for _ in 0..2 {
let failures = health.failed();
health.rest(&breaker, failures, &Error::Transient("503".into()), since);
}
assert_eq!(health.failures(), 2);
assert!(health.closed_for(since).is_some());
health.served();
assert_eq!(health.failures(), 0);
assert_eq!(health.closed_for(since), None);
}
#[test]
fn a_failure_that_does_not_open_the_circuit_still_leaves_the_route_usable() {
let since = Instant::now();
let health = Health::default();
let failures = health.failed();
health.rest(
&Breaker::default(),
failures,
&Error::Refused { category: None },
since,
);
assert_eq!(
failures, 1,
"it did fail, and the count is what Healthiest reads"
);
assert_eq!(health.closed_for(since), None, "still selectable");
}
#[test]
fn a_closed_circuit_opens_again_on_its_own() {
let since = Instant::now();
let health = Health::default();
health.close_for(Duration::from_millis(0), since);
assert_eq!(health.closed_for(since), None);
}
}