1use std::collections::BTreeMap;
26use std::sync::atomic::{AtomicU64, Ordering};
27use std::sync::mpsc::{sync_channel, SyncSender, TrySendError};
28use std::sync::{Arc, Mutex, OnceLock, RwLock};
29use std::time::Duration;
30
31use crate::application::Application;
32use crate::middleware::Middleware;
33use crate::request::Request;
34use crate::response::Response;
35use crate::server::ConnectionInfo;
36
37pub const QUEUE: usize = 1024;
39pub const WORKERS: usize = 4;
41pub const MAX_MIRRORED_BODY: usize = 64 * 1024;
43
44const CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
45const READ_TIMEOUT: Duration = Duration::from_secs(10);
46
47#[derive(Clone)]
50pub struct Mirror {
51 inner: Arc<Inner>,
52}
53
54struct Inner {
55 name: String,
56 backends: Arc<RwLock<Vec<String>>>,
59 percent: f64,
61 seen: AtomicU64,
62 next: AtomicU64,
63}
64
65impl std::fmt::Debug for Mirror {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("Mirror").field("name", &self.inner.name).field("percent", &self.inner.percent).finish_non_exhaustive()
68 }
69}
70
71impl Mirror {
72 pub fn new(name: impl Into<String>, backends: Vec<String>) -> Mirror {
74 Mirror::with_live_backends(name, Arc::new(RwLock::new(backends)))
75 }
76
77 pub fn with_live_backends(name: impl Into<String>, backends: Arc<RwLock<Vec<String>>>) -> Mirror {
80 Mirror { inner: Arc::new(Inner { name: name.into(), backends, percent: 100.0, seen: AtomicU64::new(0), next: AtomicU64::new(0) }) }
81 }
82
83 pub fn percent(self, percent: f64) -> Mirror {
86 let percent = if percent.is_finite() { percent.clamp(0.0, 100.0) } else { 0.0 };
87 let inner = &self.inner;
88 Mirror {
89 inner: Arc::new(Inner {
90 name: inner.name.clone(),
91 backends: Arc::clone(&inner.backends),
92 percent,
93 seen: AtomicU64::new(0),
94 next: AtomicU64::new(0),
95 }),
96 }
97 }
98
99 pub fn name(&self) -> &str {
100 &self.inner.name
101 }
102
103 fn sampled(&self) -> bool {
105 let p = self.inner.percent;
106 if p >= 100.0 {
107 return true;
108 }
109 if p <= 0.0 {
110 return false;
111 }
112 let n = self.inner.seen.fetch_add(1, Ordering::Relaxed) as f64;
113 ((n + 1.0) * p / 100.0).floor() > (n * p / 100.0).floor()
114 }
115
116 pub fn send(&self, request: &Request, connection: &ConnectionInfo) -> bool {
119 if !self.sampled() {
120 return false;
121 }
122 if request.body.is_stream() || request.body.len() > MAX_MIRRORED_BODY {
123 count(&self.inner.name, "skipped");
124 return false;
125 }
126 let backend = {
127 let backends = self.inner.backends.read().unwrap_or_else(|e| e.into_inner());
128 if backends.is_empty() {
129 count(&self.inner.name, "failed");
130 return false;
131 }
132 let i = self.inner.next.fetch_add(1, Ordering::Relaxed) as usize % backends.len();
133 backends[i].clone()
134 };
135 let job = Job { mirror: self.inner.name.clone(), backend, request: request.clone(), client_ip: connection.client.ip.clone(), tls: connection.tls };
136 match queue().try_send(job) {
137 Ok(()) => true,
138 Err(TrySendError::Full(job)) | Err(TrySendError::Disconnected(job)) => {
139 count(&job.mirror, "dropped");
140 false
141 }
142 }
143 }
144}
145
146struct Job {
147 mirror: String,
148 backend: String,
149 request: Request,
150 client_ip: String,
151 tls: bool,
152}
153
154fn queue() -> &'static SyncSender<Job> {
155 static QUEUE_TX: OnceLock<SyncSender<Job>> = OnceLock::new();
156 QUEUE_TX.get_or_init(|| {
157 let (tx, rx) = sync_channel::<Job>(QUEUE);
158 let rx = Arc::new(Mutex::new(rx));
159 for i in 0..WORKERS {
160 let rx = Arc::clone(&rx);
161 let _ = std::thread::Builder::new().name(format!("rws-mirror-{i}")).spawn(move || loop {
162 let job = match rx.lock().unwrap_or_else(|e| e.into_inner()).recv() {
163 Ok(job) => job,
164 Err(_) => return,
165 };
166 let outcome = if deliver(&job) { "sent" } else { "failed" };
167 count(&job.mirror, outcome);
168 });
169 }
170 tx
171 })
172}
173
174fn deliver(job: &Job) -> bool {
176 let Some((host, port, tls)) = crate::proxy_config::health::parse_backend_url(&job.backend) else { return false };
177 crate::proxy::forward_upstream(
178 crate::proxy::shared_pool(),
179 &job.request,
180 crate::proxy::Peer { ip: &job.client_ip, tls: job.tls },
181 &host,
182 port,
183 tls,
184 CONNECT_TIMEOUT,
185 READ_TIMEOUT,
186 )
187 .map(|response| {
188 if let Some(mut pipe) = response.stream_pipe {
191 let _ = std::io::copy(&mut pipe, &mut std::io::sink());
192 }
193 })
194 .is_ok()
195}
196
197fn counts() -> &'static Mutex<BTreeMap<(String, &'static str), u64>> {
198 static COUNTS: OnceLock<Mutex<BTreeMap<(String, &'static str), u64>>> = OnceLock::new();
199 COUNTS.get_or_init(Mutex::default)
200}
201
202fn count(mirror: &str, outcome: &'static str) {
203 *counts().lock().unwrap_or_else(|e| e.into_inner()).entry((mirror.to_string(), outcome)).or_default() += 1;
204}
205
206pub(crate) fn prometheus_text() -> String {
208 let counts = counts().lock().unwrap_or_else(|e| e.into_inner());
209 if counts.is_empty() {
210 return String::new();
211 }
212 let mut out = String::from(
213 "# HELP rws_mirror_requests_total Request copies sent to mirrors, by outcome: sent, failed, dropped (queue full), skipped (body streamed or too large)\n# TYPE rws_mirror_requests_total counter\n",
214 );
215 for ((mirror, outcome), n) in counts.iter() {
216 let mirror = mirror.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
217 out.push_str(&format!("rws_mirror_requests_total{{mirror=\"{mirror}\",outcome=\"{outcome}\"}} {n}\n"));
218 }
219 out
220}
221
222#[derive(Clone, Debug)]
224pub struct MirrorLayer {
225 mirror: Mirror,
226}
227
228impl MirrorLayer {
229 pub fn new(mirror: Mirror) -> MirrorLayer {
230 MirrorLayer { mirror }
231 }
232}
233
234impl Middleware for MirrorLayer {
235 fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
236 self.mirror.send(request, connection);
237 next.execute(request, connection)
238 }
239
240 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
242 true
243 }
244
245 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
247 true
248 }
249}
250
251#[cfg(test)]
252mod tests;