shared-framework 0.0.17

Reusable building blocks for HTTP services — Hyper routing, SeaORM data layer, validation, OpenAPI docs, jobs, queues, cache.
Documentation
//! Selectable service providers guarded by per-provider circuit breakers.
//!
//! Implement [`ServiceProvider`] for each backend, register them on
//! [`ApplicationService`], and pick one with [`available`](ApplicationService::available).
//! Each provider gets a [`CircuitBreaker`] that opens after repeated failures.
//!
//! Key types: [`ServiceProvider`] for backends, [`ApplicationService`] for
//! selection, [`CircuitBreaker`] and [`BreakerState`] for failure tracking.
//!
//! Use this module when several equivalent backends exist and calls should go
//! to a currently usable one.
//!
//! ```ignore
//! # use crate::service::{ApplicationService, ServiceProvider};
//! struct Primary;
//! impl ServiceProvider for Primary {
//!     fn name(&self) -> &str { "primary" }
//! }
//!
//! let mut service = ApplicationService::new("billing");
//! service.register_provider(Primary);
//! let active = service.available();
//! ```

use std::sync::{Arc, Mutex};
use std::time::Instant;

/// A backend that an [`ApplicationService`] can select.
///
/// Implementors supply a stable [`name`](Self::name) and optionally override
/// [`is_healthy`](Self::is_healthy) to signal current usability.
#[async_trait::async_trait]
pub trait ServiceProvider: Send + Sync {
    /// Stable name identifying the provider.
    fn name(&self) -> &str;
    /// Whether the provider is currently usable. Defaults to `true`.
    fn is_healthy(&self) -> bool {
        true
    }
}

/// Circuit state: accepting calls, rejecting calls, or testing recovery.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakerState {
    /// Calls flow normally.
    Closed,
    /// Calls are rejected after the failure threshold was reached.
    Open,
    /// A trial call is allowed to test whether the backend recovered.
    HalfOpen,
}

/// Failure counter that opens after `threshold` failures.
///
/// A breaker starts `Closed`. [`record_failure`](Self::record_failure) opens it
/// once failures reach the threshold; [`record_success`](Self::record_success)
/// closes it and clears the count.
pub struct CircuitBreaker {
    state: Mutex<BreakerState>,
    failures: Mutex<usize>,
    last_failure: Mutex<Option<Instant>>,
    threshold: usize,
    reset_timeout: std::time::Duration,
}

impl CircuitBreaker {
    /// Creates a breaker that opens after `threshold` failures.
    ///
    /// `reset_timeout` records how long after a failure a recovery may be
    /// attempted; the stored timestamp is updated on each failure.
    pub fn new(threshold: usize, reset_timeout: std::time::Duration) -> Self {
        Self {
            state: Mutex::new(BreakerState::Closed),
            failures: Mutex::new(0),
            last_failure: Mutex::new(None),
            threshold,
            reset_timeout,
        }
    }

    /// Returns the failure threshold.
    pub fn threshold(&self) -> usize {
        self.threshold
    }

    /// Returns the reset timeout.
    pub fn reset_timeout(&self) -> std::time::Duration {
        self.reset_timeout
    }
    /// Returns the current [`BreakerState`].
    pub fn state(&self) -> BreakerState {
        *self.state.lock().unwrap()
    }
    /// Closes the breaker and clears the failure count.
    pub fn record_success(&self) {
        *self.state.lock().unwrap() = BreakerState::Closed;
        *self.failures.lock().unwrap() = 0;
    }
    /// Records a failure and opens the breaker once the threshold is reached.
    pub fn record_failure(&self) {
        let mut f = self.failures.lock().unwrap();
        *f += 1;
        *self.last_failure.lock().unwrap() = Some(Instant::now());
        if *f >= self.threshold {
            *self.state.lock().unwrap() = BreakerState::Open;
        }
    }
    /// Whether the breaker is currently closed.
    pub fn is_closed(&self) -> bool {
        self.state() == BreakerState::Closed
    }
}

struct ProviderNode {
    provider: Arc<dyn ServiceProvider>,
    breaker: CircuitBreaker,
}

/// Named group of providers with per-provider breakers.
///
/// The first registered provider becomes the preferred one. New providers
/// start with a breaker threshold of 5 failures and a 60-second reset timeout.
pub struct ApplicationService {
    name: String,
    providers: Vec<ProviderNode>,
    preferred_idx: Option<usize>,
}

impl ApplicationService {
    /// Creates an empty service group with the given name.
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            providers: vec![],
            preferred_idx: None,
        }
    }

    /// Returns the name of the service group.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Registers a provider with a fresh circuit breaker.
    ///
    /// `P` is the concrete [`ServiceProvider`] implementation being stored.
    pub fn register_provider<P: ServiceProvider + 'static>(&mut self, provider: P) {
        let node = ProviderNode {
            provider: Arc::new(provider),
            breaker: CircuitBreaker::new(5, std::time::Duration::from_secs(60)),
        };
        if self.preferred_idx.is_none() {
            self.preferred_idx = Some(0);
        }
        self.providers.push(node);
    }

    /// Returns the provider with the given name, if registered.
    pub fn get_provider_by_name(&self, name: &str) -> Option<Arc<dyn ServiceProvider>> {
        self.providers
            .iter()
            .find(|n| n.provider.name() == name)
            .map(|n| n.provider.clone())
    }

    /// Returns the names of all registered providers in registration order.
    pub fn provider_names(&self) -> Vec<String> {
        self.providers
            .iter()
            .map(|n| n.provider.name().to_string())
            .collect()
    }

    /// Returns the first provider whose breaker is closed.
    ///
    /// A provider signaling healthy via [`is_healthy`](ServiceProvider::is_healthy)
    /// has its breaker reset and is returned. Returns `None` when no provider
    /// is currently usable.
    pub fn available(&self) -> Option<Arc<dyn ServiceProvider>> {
        for node in &self.providers {
            if node.breaker.is_closed() {
                return Some(node.provider.clone());
            }
            if node.provider.is_healthy() {
                // reset breaker as provider reports healthy now
                node.breaker.record_success();
                return Some(node.provider.clone());
            }
        }
        None
    }
}