Skip to main content

shared_framework/service/
mod.rs

1//! Selectable service providers guarded by per-provider circuit breakers.
2//!
3//! Implement [`ServiceProvider`] for each backend, register them on
4//! [`ApplicationService`], and pick one with [`available`](ApplicationService::available).
5//! Each provider gets a [`CircuitBreaker`] that opens after repeated failures.
6//!
7//! Key types: [`ServiceProvider`] for backends, [`ApplicationService`] for
8//! selection, [`CircuitBreaker`] and [`BreakerState`] for failure tracking.
9//!
10//! Use this module when several equivalent backends exist and calls should go
11//! to a currently usable one.
12//!
13//! ```ignore
14//! # use crate::service::{ApplicationService, ServiceProvider};
15//! struct Primary;
16//! impl ServiceProvider for Primary {
17//!     fn name(&self) -> &str { "primary" }
18//! }
19//!
20//! let mut service = ApplicationService::new("billing");
21//! service.register_provider(Primary);
22//! let active = service.available();
23//! ```
24
25use std::sync::{Arc, Mutex};
26use std::time::Instant;
27
28/// A backend that an [`ApplicationService`] can select.
29///
30/// Implementors supply a stable [`name`](Self::name) and optionally override
31/// [`is_healthy`](Self::is_healthy) to signal current usability.
32#[async_trait::async_trait]
33pub trait ServiceProvider: Send + Sync {
34    /// Stable name identifying the provider.
35    fn name(&self) -> &str;
36    /// Whether the provider is currently usable. Defaults to `true`.
37    fn is_healthy(&self) -> bool {
38        true
39    }
40}
41
42/// Circuit state: accepting calls, rejecting calls, or testing recovery.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum BreakerState {
45    /// Calls flow normally.
46    Closed,
47    /// Calls are rejected after the failure threshold was reached.
48    Open,
49    /// A trial call is allowed to test whether the backend recovered.
50    HalfOpen,
51}
52
53/// Failure counter that opens after `threshold` failures.
54///
55/// A breaker starts `Closed`. [`record_failure`](Self::record_failure) opens it
56/// once failures reach the threshold; [`record_success`](Self::record_success)
57/// closes it and clears the count.
58pub struct CircuitBreaker {
59    state: Mutex<BreakerState>,
60    failures: Mutex<usize>,
61    last_failure: Mutex<Option<Instant>>,
62    threshold: usize,
63    reset_timeout: std::time::Duration,
64}
65
66impl CircuitBreaker {
67    /// Creates a breaker that opens after `threshold` failures.
68    ///
69    /// `reset_timeout` records how long after a failure a recovery may be
70    /// attempted; the stored timestamp is updated on each failure.
71    pub fn new(threshold: usize, reset_timeout: std::time::Duration) -> Self {
72        Self {
73            state: Mutex::new(BreakerState::Closed),
74            failures: Mutex::new(0),
75            last_failure: Mutex::new(None),
76            threshold,
77            reset_timeout,
78        }
79    }
80
81    /// Returns the failure threshold.
82    pub fn threshold(&self) -> usize {
83        self.threshold
84    }
85
86    /// Returns the reset timeout.
87    pub fn reset_timeout(&self) -> std::time::Duration {
88        self.reset_timeout
89    }
90    /// Returns the current [`BreakerState`].
91    pub fn state(&self) -> BreakerState {
92        *self.state.lock().unwrap()
93    }
94    /// Closes the breaker and clears the failure count.
95    pub fn record_success(&self) {
96        *self.state.lock().unwrap() = BreakerState::Closed;
97        *self.failures.lock().unwrap() = 0;
98    }
99    /// Records a failure and opens the breaker once the threshold is reached.
100    pub fn record_failure(&self) {
101        let mut f = self.failures.lock().unwrap();
102        *f += 1;
103        *self.last_failure.lock().unwrap() = Some(Instant::now());
104        if *f >= self.threshold {
105            *self.state.lock().unwrap() = BreakerState::Open;
106        }
107    }
108    /// Whether the breaker is currently closed.
109    pub fn is_closed(&self) -> bool {
110        self.state() == BreakerState::Closed
111    }
112}
113
114struct ProviderNode {
115    provider: Arc<dyn ServiceProvider>,
116    breaker: CircuitBreaker,
117}
118
119/// Named group of providers with per-provider breakers.
120///
121/// The first registered provider becomes the preferred one. New providers
122/// start with a breaker threshold of 5 failures and a 60-second reset timeout.
123pub struct ApplicationService {
124    name: String,
125    providers: Vec<ProviderNode>,
126    preferred_idx: Option<usize>,
127}
128
129impl ApplicationService {
130    /// Creates an empty service group with the given name.
131    pub fn new(name: impl Into<String>) -> Self {
132        Self {
133            name: name.into(),
134            providers: vec![],
135            preferred_idx: None,
136        }
137    }
138
139    /// Returns the name of the service group.
140    pub fn name(&self) -> &str {
141        &self.name
142    }
143
144    /// Registers a provider with a fresh circuit breaker.
145    ///
146    /// `P` is the concrete [`ServiceProvider`] implementation being stored.
147    pub fn register_provider<P: ServiceProvider + 'static>(&mut self, provider: P) {
148        let node = ProviderNode {
149            provider: Arc::new(provider),
150            breaker: CircuitBreaker::new(5, std::time::Duration::from_secs(60)),
151        };
152        if self.preferred_idx.is_none() {
153            self.preferred_idx = Some(0);
154        }
155        self.providers.push(node);
156    }
157
158    /// Returns the provider with the given name, if registered.
159    pub fn get_provider_by_name(&self, name: &str) -> Option<Arc<dyn ServiceProvider>> {
160        self.providers
161            .iter()
162            .find(|n| n.provider.name() == name)
163            .map(|n| n.provider.clone())
164    }
165
166    /// Returns the names of all registered providers in registration order.
167    pub fn provider_names(&self) -> Vec<String> {
168        self.providers
169            .iter()
170            .map(|n| n.provider.name().to_string())
171            .collect()
172    }
173
174    /// Returns the first provider whose breaker is closed.
175    ///
176    /// A provider signaling healthy via [`is_healthy`](ServiceProvider::is_healthy)
177    /// has its breaker reset and is returned. Returns `None` when no provider
178    /// is currently usable.
179    pub fn available(&self) -> Option<Arc<dyn ServiceProvider>> {
180        for node in &self.providers {
181            if node.breaker.is_closed() {
182                return Some(node.provider.clone());
183            }
184            if node.provider.is_healthy() {
185                // reset breaker as provider reports healthy now
186                node.breaker.record_success();
187                return Some(node.provider.clone());
188            }
189        }
190        None
191    }
192}