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}