rust_web_server/proxy_config/
upstream.rs1use 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
45pub(crate) struct UpstreamPolicy {
47 pub(crate) name: String,
48 live: Arc<RwLock<Vec<String>>>,
50 weights: HashMap<String, u32>,
51 order: Vec<String>,
53 pub(crate) sticky: Option<StickyConfig>,
54 breaker: Option<Mutex<CircuitBreaker>>,
55 pub(crate) retry: Option<RetryConfig>,
56 current: Mutex<HashMap<String, i64>>,
58}
59
60impl UpstreamPolicy {
61 pub(crate) fn new(
62 name: &str,
63 live: Arc<RwLock<Vec<String>>>,
64 backends: &[String],
65 weights: &[u32],
66 sticky: Option<StickyConfig>,
67 breaker: Option<&BreakerConfig>,
68 retry: Option<RetryConfig>,
69 ) -> Self {
70 UpstreamPolicy {
71 name: name.to_string(),
72 live,
73 weights: backends.iter().enumerate().map(|(i, b)| (b.clone(), weights.get(i).copied().unwrap_or(1))).collect(),
75 order: backends.to_vec(),
76 sticky,
77 breaker: breaker.map(|b| Mutex::new(CircuitBreaker::new(b.failure_threshold.max(1), b.recovery_secs).max_half_open_probes(b.half_open_probes))),
78 retry,
79 current: Mutex::new(HashMap::new()),
80 }
81 }
82
83 pub(crate) fn weight(&self, backend: &str) -> u32 {
85 self.weights.get(backend).copied().unwrap_or(1).max(1)
86 }
87
88 pub(crate) fn weighted(&self) -> bool {
90 self.weights.values().any(|w| *w != 1)
91 }
92
93 pub(crate) fn weighted_round_robin(&self, candidates: &[String]) -> Option<String> {
97 let mut current = self.current.lock().unwrap_or_else(|e| e.into_inner());
98 let total: i64 = candidates.iter().map(|b| self.weight(b) as i64).sum();
99 let mut best: Option<(&String, i64)> = None;
100 for backend in candidates {
101 let value = current.entry(backend.clone()).or_insert(0);
102 *value += self.weight(backend) as i64;
103 if best.is_none_or(|(_, top)| *value > top) {
104 best = Some((backend, *value));
105 }
106 }
107 let (chosen, _) = best?;
108 *current.get_mut(chosen.as_str()).expect("just inserted") -= total;
109 Some(chosen.clone())
110 }
111
112 pub(crate) fn weighted_index(&self, candidates: &[String], roll: usize) -> usize {
114 let total: usize = candidates.iter().map(|b| self.weight(b) as usize).sum();
115 let mut point = roll % total.max(1);
116 for (i, backend) in candidates.iter().enumerate() {
117 let weight = self.weight(backend) as usize;
118 if point < weight {
119 return i;
120 }
121 point -= weight;
122 }
123 0
124 }
125
126 pub(crate) fn admits(&self, backend: &str) -> bool {
129 match &self.breaker {
130 Some(breaker) => breaker.lock().unwrap_or_else(|e| e.into_inner()).is_available(backend),
131 None => true,
132 }
133 }
134
135 pub(crate) fn any_open(&self, backends: &[String]) -> bool {
137 self.breaker.as_ref().is_some_and(|breaker| {
138 let breaker = breaker.lock().unwrap_or_else(|e| e.into_inner());
139 backends.iter().any(|b| breaker.state(b) != BreakerState::Closed)
140 })
141 }
142
143 pub(crate) fn record(&self, backend: &str, failed: bool) {
144 if let Some(breaker) = &self.breaker {
145 let mut breaker = breaker.lock().unwrap_or_else(|e| e.into_inner());
146 if failed {
147 breaker.record_failure(backend);
148 } else {
149 breaker.record_success(backend);
150 }
151 }
152 }
153
154 fn breaker_state(&self, backend: &str) -> Option<BreakerState> {
155 self.breaker.as_ref().map(|b| b.lock().unwrap_or_else(|e| e.into_inner()).state(backend))
156 }
157
158 pub(crate) fn retries_status(&self, status: i16) -> bool {
160 self.retry.as_ref().is_some_and(|r| r.status.contains(&status))
161 }
162}
163
164pub(crate) fn backend_id(backend: &str) -> String {
167 crate::core::sha256::sha256(backend.as_bytes())[..6].iter().map(|b| format!("{b:02x}")).collect()
168}
169
170pub(crate) fn cookie_value(request: &crate::request::Request, name: &str) -> Option<String> {
172 request
173 .headers
174 .iter()
175 .filter(|h| h.name.eq_ignore_ascii_case("cookie"))
176 .flat_map(|h| h.value.split(';').map(str::trim).map(str::to_string).collect::<Vec<_>>())
177 .find_map(|pair| pair.split_once('=').filter(|(n, _)| *n == name).map(|(_, v)| v.to_string()))
178}
179
180pub(crate) fn replayable(request: &crate::request::Request) -> bool {
183 matches!(request.method.as_str(), "GET" | "HEAD" | "OPTIONS" | "PUT" | "DELETE" | "TRACE") && !request.body.is_stream()
184}
185
186const 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];
189
190#[derive(Default)]
191struct BackendStats {
192 requests: HashMap<String, u64>,
194 buckets: [u64; 11],
195 sum: f64,
196 count: u64,
197}
198
199#[derive(Default)]
200struct Stats {
201 backends: HashMap<(String, String), BackendStats>,
203 retries: HashMap<String, u64>,
204 policies: Vec<Arc<UpstreamPolicy>>,
206}
207
208fn stats() -> &'static Mutex<Stats> {
209 static STATS: OnceLock<Mutex<Stats>> = OnceLock::new();
210 STATS.get_or_init(Default::default)
211}
212
213pub(crate) fn register(policies: Vec<Arc<UpstreamPolicy>>) {
215 stats().lock().unwrap_or_else(|e| e.into_inner()).policies = policies;
216}
217
218static ANY_DRAINED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
222
223fn drained_set() -> &'static RwLock<std::collections::HashSet<(String, String)>> {
224 static SET: OnceLock<RwLock<std::collections::HashSet<(String, String)>>> = OnceLock::new();
225 SET.get_or_init(RwLock::default)
226}
227
228pub(crate) fn set_drained(upstream: &str, backend: &str, drained: bool) {
232 let mut set = drained_set().write().unwrap_or_else(|e| e.into_inner());
233 let key = (upstream.to_string(), backend.to_string());
234 if drained {
235 set.insert(key);
236 } else {
237 set.remove(&key);
238 }
239 ANY_DRAINED.store(!set.is_empty(), std::sync::atomic::Ordering::SeqCst);
240}
241
242pub(crate) fn is_drained(upstream: &str, backend: &str) -> bool {
243 if !ANY_DRAINED.load(std::sync::atomic::Ordering::Relaxed) {
244 return false;
245 }
246 let set = drained_set().read().unwrap_or_else(|e| e.into_inner());
247 set.iter().any(|(u, b)| u == upstream && b == backend)
248}
249
250#[derive(Debug, Clone, PartialEq)]
252pub(crate) struct BackendView {
253 pub(crate) url: String,
254 pub(crate) weight: u32,
255 pub(crate) healthy: bool,
257 pub(crate) breaker: Option<&'static str>,
259 pub(crate) drained: bool,
260}
261
262pub(crate) fn snapshot() -> Vec<(String, Vec<BackendView>)> {
264 let policies = stats().lock().unwrap_or_else(|e| e.into_inner()).policies.clone();
265 policies
266 .iter()
267 .map(|p| {
268 let live = p.live.read().unwrap_or_else(|e| e.into_inner()).clone();
269 let mut backends: Vec<&String> = p.weights.keys().collect();
270 backends.sort_by_key(|b| p.order.iter().position(|o| o == *b).unwrap_or(usize::MAX));
271 let views = backends
272 .into_iter()
273 .map(|b| BackendView {
274 url: b.clone(),
275 weight: p.weight(b),
276 healthy: live.contains(b),
277 breaker: p.breaker_state(b).map(|s| match s {
278 BreakerState::Closed => "closed",
279 BreakerState::Open => "open",
280 BreakerState::HalfOpen => "half_open",
281 }),
282 drained: is_drained(&p.name, b),
283 })
284 .collect();
285 (p.name.clone(), views)
286 })
287 .collect()
288}
289
290pub(crate) fn observe(upstream: &str, backend: &str, status: Option<i16>, started: Instant) {
293 let secs = started.elapsed().as_secs_f64();
294 let mut stats = stats().lock().unwrap_or_else(|e| e.into_inner());
295 let entry = stats.backends.entry((upstream.to_string(), backend.to_string())).or_default();
296 *entry.requests.entry(status.map_or_else(|| "error".to_string(), |s| s.to_string())).or_default() += 1;
297 for (i, upper) in BUCKETS.iter().enumerate() {
298 if secs <= *upper {
299 entry.buckets[i] += 1;
300 }
301 }
302 entry.sum += secs;
303 entry.count += 1;
304}
305
306pub(crate) fn observe_retry(upstream: &str) {
307 *stats().lock().unwrap_or_else(|e| e.into_inner()).retries.entry(upstream.to_string()).or_default() += 1;
308}
309
310fn label(value: &str) -> String {
311 value.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n")
312}
313
314pub(crate) fn prometheus_text() -> String {
316 let stats = stats().lock().unwrap_or_else(|e| e.into_inner());
317 let mut out = String::new();
318 if !stats.backends.is_empty() {
319 let mut keys: Vec<&(String, String)> = stats.backends.keys().collect();
320 keys.sort();
321 out.push_str("# HELP rws_upstream_requests_total Requests sent to each upstream backend, by response status (\"error\": no response)\n");
322 out.push_str("# TYPE rws_upstream_requests_total counter\n");
323 for key in &keys {
324 let entry = &stats.backends[*key];
325 let mut codes: Vec<&String> = entry.requests.keys().collect();
326 codes.sort();
327 for code in codes {
328 out.push_str(&format!(
329 "rws_upstream_requests_total{{upstream=\"{}\",backend=\"{}\",code=\"{}\"}} {}\n",
330 label(&key.0), label(&key.1), code, entry.requests[code]
331 ));
332 }
333 }
334 out.push_str("# HELP rws_upstream_request_duration_seconds Time from sending a request to an upstream backend to its response head\n");
335 out.push_str("# TYPE rws_upstream_request_duration_seconds histogram\n");
336 for key in &keys {
337 let entry = &stats.backends[*key];
338 let labels = format!("upstream=\"{}\",backend=\"{}\"", label(&key.0), label(&key.1));
339 for (i, upper) in BUCKETS.iter().enumerate() {
340 out.push_str(&format!("rws_upstream_request_duration_seconds_bucket{{{labels},le=\"{upper}\"}} {}\n", entry.buckets[i]));
341 }
342 out.push_str(&format!("rws_upstream_request_duration_seconds_bucket{{{labels},le=\"+Inf\"}} {}\n", entry.count));
343 out.push_str(&format!("rws_upstream_request_duration_seconds_sum{{{labels}}} {:.6}\n", entry.sum));
344 out.push_str(&format!("rws_upstream_request_duration_seconds_count{{{labels}}} {}\n", entry.count));
345 }
346 }
347 if !stats.retries.is_empty() {
348 let mut names: Vec<&String> = stats.retries.keys().collect();
349 names.sort();
350 out.push_str("# HELP rws_upstream_retries_total Requests sent again to another backend\n");
351 out.push_str("# TYPE rws_upstream_retries_total counter\n");
352 for name in names {
353 out.push_str(&format!("rws_upstream_retries_total{{upstream=\"{}\"}} {}\n", label(name), stats.retries[name]));
354 }
355 }
356 if !stats.policies.is_empty() {
357 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");
358 out.push_str("# TYPE rws_upstream_backend_up gauge\n");
359 let mut breaker_lines = String::new();
360 for policy in &stats.policies {
361 let live = policy.live.read().unwrap_or_else(|e| e.into_inner()).clone();
362 let mut backends: Vec<&String> = policy.weights.keys().collect();
363 backends.sort();
364 for backend in backends {
365 let state = policy.breaker_state(backend);
366 let up = live.contains(backend) && state.as_ref().is_none_or(|s| *s != BreakerState::Open);
367 let labels = format!("upstream=\"{}\",backend=\"{}\"", label(&policy.name), label(backend));
368 out.push_str(&format!("rws_upstream_backend_up{{{labels}}} {}\n", u8::from(up)));
369 if let Some(state) = state {
370 let value = match state {
371 BreakerState::Closed => 0,
372 BreakerState::HalfOpen => 1,
373 BreakerState::Open => 2,
374 };
375 breaker_lines.push_str(&format!("rws_upstream_circuit_breaker_state{{{labels}}} {value}\n"));
376 }
377 }
378 }
379 if !breaker_lines.is_empty() {
380 out.push_str("# HELP rws_upstream_circuit_breaker_state Each backend's circuit breaker (0=closed, 1=half_open, 2=open)\n");
381 out.push_str("# TYPE rws_upstream_circuit_breaker_state gauge\n");
382 out.push_str(&breaker_lines);
383 }
384 }
385 out
386}
387
388#[cfg(test)]
389mod tests;