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