featherbit/traffic/
mod.rs1use std::sync::atomic::AtomicI64;
17use std::sync::Arc;
18use std::time::{Duration, Instant};
19
20use dashmap::DashMap;
21use tokio::sync::Mutex;
22
23pub mod cache;
24pub mod purge;
25#[cfg(feature = "redis-store")]
29pub use cache::CacheError;
30pub use cache::{CachedResponse, LocalResponseCache, ResponseCache};
31pub use purge::{collect_targets, purge_targets, CacheTarget, PurgeOutcome};
32
33#[derive(Default)]
39pub struct ConnRegistry {
40 counters: DashMap<String, Arc<AtomicI64>>,
41}
42
43impl ConnRegistry {
44 pub fn counter(&self, key: &str) -> Arc<AtomicI64> {
46 self.counters
47 .entry(key.to_string())
48 .or_insert_with(|| Arc::new(AtomicI64::new(0)))
49 .clone()
50 }
51}
52
53#[derive(Default)]
55pub struct BreakerState {
56 unhealthy_count: u32,
58 healthy_count: u32,
60 trip_round: u32,
63 open_until: Option<Instant>,
65}
66
67impl BreakerState {
68 pub fn allow(&mut self) -> bool {
72 match self.open_until {
73 Some(until) if Instant::now() < until => false,
74 Some(_) => {
75 self.open_until = None;
77 true
78 }
79 None => true,
80 }
81 }
82
83 pub fn record_healthy(&mut self, healthy_threshold: u32) {
86 self.unhealthy_count = 0;
87 self.healthy_count = self.healthy_count.saturating_add(1);
88 if self.healthy_count >= healthy_threshold {
89 self.healthy_count = 0;
90 self.trip_round = 0;
91 }
92 }
93
94 pub fn record_unhealthy(
98 &mut self,
99 unhealthy_threshold: u32,
100 break_base_sec: u64,
101 max_breaker_sec: u64,
102 ) {
103 self.healthy_count = 0;
104 self.unhealthy_count = self.unhealthy_count.saturating_add(1);
105 if self.unhealthy_count >= unhealthy_threshold {
106 self.unhealthy_count = 0;
107 let backoff = break_base_sec
108 .saturating_mul(1u64 << self.trip_round.min(16))
109 .min(max_breaker_sec.max(break_base_sec));
110 self.open_until = Some(Instant::now() + Duration::from_secs(backoff));
111 self.trip_round = self.trip_round.saturating_add(1);
112 }
113 }
114}
115
116#[derive(Default)]
118pub struct BreakerRegistry {
119 breakers: DashMap<String, Arc<Mutex<BreakerState>>>,
120}
121
122impl BreakerRegistry {
123 pub fn breaker(&self, key: &str) -> Arc<Mutex<BreakerState>> {
125 self.breakers
126 .entry(key.to_string())
127 .or_insert_with(|| Arc::new(Mutex::new(BreakerState::default())))
128 .clone()
129 }
130}
131
132#[derive(Default)]
134pub struct TrafficRegistries {
135 pub conn: ConnRegistry,
136 pub breakers: BreakerRegistry,
137 pub cache: Arc<LocalResponseCache>,
138}
139
140impl TrafficRegistries {
141 pub fn new(metrics: Option<Arc<crate::metrics::GatewayMetrics>>) -> Self {
144 Self {
145 conn: ConnRegistry::default(),
146 breakers: BreakerRegistry::default(),
147 cache: Arc::new(LocalResponseCache::new(metrics)),
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use std::sync::atomic::Ordering;
156
157 #[test]
158 fn test_conn_counter_shared() {
159 let reg = ConnRegistry::default();
160 let a = reg.counter("k");
161 let b = reg.counter("k");
162 a.fetch_add(1, Ordering::Relaxed);
163 assert_eq!(b.load(Ordering::Relaxed), 1);
164 assert_eq!(reg.counter("other").load(Ordering::Relaxed), 0);
165 }
166
167 #[test]
168 fn test_breaker_trips_and_recovers() {
169 let mut s = BreakerState::default();
170 assert!(s.allow());
171 s.record_unhealthy(2, 3600, 3600);
173 assert!(s.allow());
174 s.record_unhealthy(2, 3600, 3600);
175 assert!(!s.allow(), "breaker should be open after threshold");
176
177 let mut s = BreakerState::default();
179 s.record_unhealthy(3, 10, 100);
180 s.record_healthy(1);
181 s.record_unhealthy(3, 10, 100);
182 assert!(s.allow(), "healthy response should have reset the streak");
183 }
184
185 #[test]
186 fn test_breaker_backoff_grows() {
187 let mut s = BreakerState::default();
188 s.record_unhealthy(1, 2, 100); let first = s.open_until.unwrap();
190 s.open_until = None; s.record_unhealthy(1, 2, 100); let second = s.open_until.unwrap();
193 assert!(second > first, "cooldown should grow across trips");
194 }
195}