1use std::collections::HashMap;
11use std::sync::{Arc, Mutex, OnceLock, RwLock};
12use std::time::Instant;
13
14use crate::circuit_breaker::{BreakerState, CircuitBreaker};
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct StickyConfig {
19 pub cookie: String,
21 pub max_age_secs: Option<u64>,
23}
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct BreakerConfig {
28 pub failure_threshold: u32,
30 pub recovery_secs: u64,
32 pub half_open_probes: u32,
34}
35
36#[derive(Debug, Clone, PartialEq)]
38pub struct RetryConfig {
39 pub attempts: u32,
41 pub status: Vec<i16>,
43}
44
45#[doc(hidden)]
47pub struct UpstreamPolicy {
48 #[doc(hidden)]
49 pub name: String,
50 live: Arc<RwLock<Vec<String>>>,
52 weights: HashMap<String, u32>,
53 order: Vec<String>,
55 #[doc(hidden)]
56 pub sticky: Option<StickyConfig>,
57 breaker: Option<Mutex<CircuitBreaker>>,
58 #[doc(hidden)]
59 pub retry: Option<RetryConfig>,
60 current: Mutex<HashMap<String, i64>>,
62}
63
64impl UpstreamPolicy {
65 #[doc(hidden)]
66 pub fn new(
67 name: &str,
68 live: Arc<RwLock<Vec<String>>>,
69 backends: &[String],
70 weights: &[u32],
71 sticky: Option<StickyConfig>,
72 breaker: Option<&BreakerConfig>,
73 retry: Option<RetryConfig>,
74 ) -> Self {
75 UpstreamPolicy {
76 name: name.to_string(),
77 live,
78 weights: backends.iter().enumerate().map(|(i, b)| (b.clone(), weights.get(i).copied().unwrap_or(1))).collect(),
80 order: backends.to_vec(),
81 sticky,
82 breaker: breaker.map(|b| Mutex::new(CircuitBreaker::new(b.failure_threshold.max(1), b.recovery_secs).max_half_open_probes(b.half_open_probes))),
83 retry,
84 current: Mutex::new(HashMap::new()),
85 }
86 }
87
88 #[doc(hidden)]
90 pub fn weight(&self, backend: &str) -> u32 {
91 self.weights.get(backend).copied().unwrap_or(1).max(1)
92 }
93
94 #[doc(hidden)]
96 pub fn weighted(&self) -> bool {
97 self.weights.values().any(|w| *w != 1)
98 }
99
100 #[doc(hidden)]
104 pub fn weighted_round_robin(&self, candidates: &[String]) -> Option<String> {
105 let mut current = self.current.lock().unwrap_or_else(|e| e.into_inner());
106 let total: i64 = candidates.iter().map(|b| self.weight(b) as i64).sum();
107 let mut best: Option<(&String, i64)> = None;
108 for backend in candidates {
109 let value = current.entry(backend.clone()).or_insert(0);
110 *value += self.weight(backend) as i64;
111 if best.is_none_or(|(_, top)| *value > top) {
112 best = Some((backend, *value));
113 }
114 }
115 let (chosen, _) = best?;
116 *current.get_mut(chosen.as_str()).expect("just inserted") -= total;
117 Some(chosen.clone())
118 }
119
120 #[doc(hidden)]
122 pub fn weighted_index(&self, candidates: &[String], roll: usize) -> usize {
123 let total: usize = candidates.iter().map(|b| self.weight(b) as usize).sum();
124 let mut point = roll % total.max(1);
125 for (i, backend) in candidates.iter().enumerate() {
126 let weight = self.weight(backend) as usize;
127 if point < weight {
128 return i;
129 }
130 point -= weight;
131 }
132 0
133 }
134
135 #[doc(hidden)]
138 pub fn admits(&self, backend: &str) -> bool {
139 match &self.breaker {
140 Some(breaker) => breaker.lock().unwrap_or_else(|e| e.into_inner()).is_available(backend),
141 None => true,
142 }
143 }
144
145 #[doc(hidden)]
147 pub fn any_open(&self, backends: &[String]) -> bool {
148 self.breaker.as_ref().is_some_and(|breaker| {
149 let breaker = breaker.lock().unwrap_or_else(|e| e.into_inner());
150 backends.iter().any(|b| breaker.state(b) != BreakerState::Closed)
151 })
152 }
153
154 #[doc(hidden)]
155 pub fn record(&self, backend: &str, failed: bool) {
156 if let Some(breaker) = &self.breaker {
157 let mut breaker = breaker.lock().unwrap_or_else(|e| e.into_inner());
158 if failed {
159 breaker.record_failure(backend);
160 } else {
161 breaker.record_success(backend);
162 }
163 }
164 }
165
166 fn breaker_state(&self, backend: &str) -> Option<BreakerState> {
167 self.breaker.as_ref().map(|b| b.lock().unwrap_or_else(|e| e.into_inner()).state(backend))
168 }
169
170 #[doc(hidden)]
172 pub fn retries_status(&self, status: i16) -> bool {
173 self.retry.as_ref().is_some_and(|r| r.status.contains(&status))
174 }
175}
176
177#[doc(hidden)]
180pub fn backend_id(backend: &str) -> String {
181 crate::core::sha256::sha256(backend.as_bytes())[..6].iter().map(|b| format!("{b:02x}")).collect()
182}
183
184#[doc(hidden)]
186pub fn cookie_value(request: &crate::request::Request, name: &str) -> Option<String> {
187 request
188 .headers
189 .iter()
190 .filter(|h| h.name.eq_ignore_ascii_case("cookie"))
191 .flat_map(|h| h.value.split(';').map(str::trim).map(str::to_string).collect::<Vec<_>>())
192 .find_map(|pair| pair.split_once('=').filter(|(n, _)| *n == name).map(|(_, v)| v.to_string()))
193}
194
195#[doc(hidden)]
198pub fn replayable(request: &crate::request::Request) -> bool {
199 matches!(request.method.as_str(), "GET" | "HEAD" | "OPTIONS" | "PUT" | "DELETE" | "TRACE") && !request.body.is_stream()
200}
201
202const BUCKETS: [f64; 11] = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0];
205
206#[derive(Default)]
207struct BackendStats {
208 requests: HashMap<String, u64>,
210 buckets: [u64; 11],
211 sum: f64,
212 count: u64,
213}
214
215#[derive(Default)]
216struct Stats {
217 backends: HashMap<(String, String), BackendStats>,
219 retries: HashMap<String, u64>,
220 h3_fallbacks: HashMap<String, u64>,
222 policies: Vec<Arc<UpstreamPolicy>>,
224}
225
226fn stats() -> &'static Mutex<Stats> {
227 static STATS: OnceLock<Mutex<Stats>> = OnceLock::new();
228 STATS.get_or_init(|| {
229 crate::metrics::register_section(crate::metrics::section::UPSTREAMS, prometheus_text);
230 Default::default()
231 })
232}
233
234#[cfg(feature = "http3")]
236#[doc(hidden)]
237pub fn observe_h3_fallback(upstream: &str) {
238 *stats().lock().unwrap_or_else(|e| e.into_inner()).h3_fallbacks.entry(upstream.to_string()).or_default() += 1;
239}
240
241#[doc(hidden)]
243pub fn register(policies: Vec<Arc<UpstreamPolicy>>) {
244 stats().lock().unwrap_or_else(|e| e.into_inner()).policies = policies;
245}
246
247static ANY_DRAINED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
251
252fn drained_set() -> &'static RwLock<std::collections::HashSet<(String, String)>> {
253 static SET: OnceLock<RwLock<std::collections::HashSet<(String, String)>>> = OnceLock::new();
254 SET.get_or_init(RwLock::default)
255}
256
257#[doc(hidden)]
261pub fn set_drained(upstream: &str, backend: &str, drained: bool) {
262 let mut set = drained_set().write().unwrap_or_else(|e| e.into_inner());
263 let key = (upstream.to_string(), backend.to_string());
264 if drained {
265 set.insert(key);
266 } else {
267 set.remove(&key);
268 }
269 ANY_DRAINED.store(!set.is_empty(), std::sync::atomic::Ordering::SeqCst);
270}
271
272#[doc(hidden)]
273pub fn is_drained(upstream: &str, backend: &str) -> bool {
274 if !ANY_DRAINED.load(std::sync::atomic::Ordering::Relaxed) {
275 return false;
276 }
277 let set = drained_set().read().unwrap_or_else(|e| e.into_inner());
278 set.iter().any(|(u, b)| u == upstream && b == backend)
279}
280
281#[derive(Debug, Clone, PartialEq)]
283#[doc(hidden)]
284pub struct BackendView {
285 #[doc(hidden)]
286 pub url: String,
287 #[doc(hidden)]
288 pub weight: u32,
289 #[doc(hidden)]
291 pub healthy: bool,
292 #[doc(hidden)]
294 pub breaker: Option<&'static str>,
295 #[doc(hidden)]
296 pub drained: bool,
297}
298
299#[doc(hidden)]
301pub fn snapshot() -> Vec<(String, Vec<BackendView>)> {
302 let policies = stats().lock().unwrap_or_else(|e| e.into_inner()).policies.clone();
303 policies
304 .iter()
305 .map(|p| {
306 let live = p.live.read().unwrap_or_else(|e| e.into_inner()).clone();
307 let mut backends: Vec<&String> = p.weights.keys().collect();
308 backends.sort_by_key(|b| p.order.iter().position(|o| o == *b).unwrap_or(usize::MAX));
309 let views = backends
310 .into_iter()
311 .map(|b| BackendView {
312 url: b.clone(),
313 weight: p.weight(b),
314 healthy: live.contains(b),
315 breaker: p.breaker_state(b).map(|s| match s {
316 BreakerState::Closed => "closed",
317 BreakerState::Open => "open",
318 BreakerState::HalfOpen => "half_open",
319 }),
320 drained: is_drained(&p.name, b),
321 })
322 .collect();
323 (p.name.clone(), views)
324 })
325 .collect()
326}
327
328#[doc(hidden)]
331pub fn observe(upstream: &str, backend: &str, status: Option<i16>, started: Instant) {
332 let secs = started.elapsed().as_secs_f64();
333 let mut stats = stats().lock().unwrap_or_else(|e| e.into_inner());
334 let entry = stats.backends.entry((upstream.to_string(), backend.to_string())).or_default();
335 *entry.requests.entry(status.map_or_else(|| "error".to_string(), |s| s.to_string())).or_default() += 1;
336 for (i, upper) in BUCKETS.iter().enumerate() {
337 if secs <= *upper {
338 entry.buckets[i] += 1;
339 }
340 }
341 entry.sum += secs;
342 entry.count += 1;
343}
344
345#[doc(hidden)]
346pub fn observe_retry(upstream: &str) {
347 *stats().lock().unwrap_or_else(|e| e.into_inner()).retries.entry(upstream.to_string()).or_default() += 1;
348}
349
350fn label(value: &str) -> String {
351 value.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n")
352}
353
354#[doc(hidden)]
356pub fn prometheus_text() -> String {
357 let stats = stats().lock().unwrap_or_else(|e| e.into_inner());
358 let mut out = String::new();
359 if !stats.backends.is_empty() {
360 let mut keys: Vec<&(String, String)> = stats.backends.keys().collect();
361 keys.sort();
362 out.push_str("# HELP rws_upstream_requests_total Requests sent to each upstream backend, by response status (\"error\": no response)\n");
363 out.push_str("# TYPE rws_upstream_requests_total counter\n");
364 for key in &keys {
365 let entry = &stats.backends[*key];
366 let mut codes: Vec<&String> = entry.requests.keys().collect();
367 codes.sort();
368 for code in codes {
369 out.push_str(&format!(
370 "rws_upstream_requests_total{{upstream=\"{}\",backend=\"{}\",code=\"{}\"}} {}\n",
371 label(&key.0), label(&key.1), code, entry.requests[code]
372 ));
373 }
374 }
375 out.push_str("# HELP rws_upstream_request_duration_seconds Time from sending a request to an upstream backend to its response head\n");
376 out.push_str("# TYPE rws_upstream_request_duration_seconds histogram\n");
377 for key in &keys {
378 let entry = &stats.backends[*key];
379 let labels = format!("upstream=\"{}\",backend=\"{}\"", label(&key.0), label(&key.1));
380 for (i, upper) in BUCKETS.iter().enumerate() {
381 out.push_str(&format!("rws_upstream_request_duration_seconds_bucket{{{labels},le=\"{upper}\"}} {}\n", entry.buckets[i]));
382 }
383 out.push_str(&format!("rws_upstream_request_duration_seconds_bucket{{{labels},le=\"+Inf\"}} {}\n", entry.count));
384 out.push_str(&format!("rws_upstream_request_duration_seconds_sum{{{labels}}} {:.6}\n", entry.sum));
385 out.push_str(&format!("rws_upstream_request_duration_seconds_count{{{labels}}} {}\n", entry.count));
386 }
387 }
388 if !stats.retries.is_empty() {
389 let mut names: Vec<&String> = stats.retries.keys().collect();
390 names.sort();
391 out.push_str("# HELP rws_upstream_retries_total Requests sent again to another backend\n");
392 out.push_str("# TYPE rws_upstream_retries_total counter\n");
393 for name in names {
394 out.push_str(&format!("rws_upstream_retries_total{{upstream=\"{}\"}} {}\n", label(name), stats.retries[name]));
395 }
396 }
397 if !stats.policies.is_empty() {
398 out.push_str("# HELP rws_upstream_backend_up Whether a backend is in rotation: passing health checks and its circuit breaker not open (1) or not (0)\n");
399 out.push_str("# TYPE rws_upstream_backend_up gauge\n");
400 let mut breaker_lines = String::new();
401 for policy in &stats.policies {
402 let live = policy.live.read().unwrap_or_else(|e| e.into_inner()).clone();
403 let mut backends: Vec<&String> = policy.weights.keys().collect();
404 backends.sort();
405 for backend in backends {
406 let state = policy.breaker_state(backend);
407 let up = live.contains(backend) && state.as_ref().is_none_or(|s| *s != BreakerState::Open);
408 let labels = format!("upstream=\"{}\",backend=\"{}\"", label(&policy.name), label(backend));
409 out.push_str(&format!("rws_upstream_backend_up{{{labels}}} {}\n", u8::from(up)));
410 if let Some(state) = state {
411 let value = match state {
412 BreakerState::Closed => 0,
413 BreakerState::HalfOpen => 1,
414 BreakerState::Open => 2,
415 };
416 breaker_lines.push_str(&format!("rws_upstream_circuit_breaker_state{{{labels}}} {value}\n"));
417 }
418 }
419 }
420 if !breaker_lines.is_empty() {
421 out.push_str("# HELP rws_upstream_circuit_breaker_state Each backend's circuit breaker (0=closed, 1=half_open, 2=open)\n");
422 out.push_str("# TYPE rws_upstream_circuit_breaker_state gauge\n");
423 out.push_str(&breaker_lines);
424 }
425 }
426 if !stats.h3_fallbacks.is_empty() {
427 let mut names: Vec<&String> = stats.h3_fallbacks.keys().collect();
428 names.sort();
429 out.push_str("# HELP rws_upstream_h3_fallbacks_total Requests to a protocol = \"h3\" upstream sent over TCP because QUIC couldn't connect\n");
430 out.push_str("# TYPE rws_upstream_h3_fallbacks_total counter\n");
431 for name in names {
432 let upstream = name.replace('\\', "\\\\").replace('"', "\\\"");
433 out.push_str(&format!("rws_upstream_h3_fallbacks_total{{upstream=\"{upstream}\"}} {}\n", stats.h3_fallbacks[name]));
434 }
435 }
436 out
437}
438
439#[cfg(test)]
440mod tests;