use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{sync_channel, SyncSender, TrySendError};
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::time::Duration;
use crate::application::Application;
use crate::middleware::Middleware;
use crate::request::Request;
use crate::response::Response;
use crate::server::ConnectionInfo;
pub const QUEUE: usize = 1024;
pub const WORKERS: usize = 4;
pub const MAX_MIRRORED_BODY: usize = 64 * 1024;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
const READ_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Clone)]
pub struct Mirror {
inner: Arc<Inner>,
}
struct Inner {
name: String,
backends: Arc<RwLock<Vec<String>>>,
percent: f64,
seen: AtomicU64,
next: AtomicU64,
}
impl std::fmt::Debug for Mirror {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Mirror").field("name", &self.inner.name).field("percent", &self.inner.percent).finish_non_exhaustive()
}
}
impl Mirror {
pub fn new(name: impl Into<String>, backends: Vec<String>) -> Mirror {
Mirror::with_live_backends(name, Arc::new(RwLock::new(backends)))
}
pub fn with_live_backends(name: impl Into<String>, backends: Arc<RwLock<Vec<String>>>) -> Mirror {
Mirror { inner: Arc::new(Inner { name: name.into(), backends, percent: 100.0, seen: AtomicU64::new(0), next: AtomicU64::new(0) }) }
}
pub fn percent(self, percent: f64) -> Mirror {
let percent = if percent.is_finite() { percent.clamp(0.0, 100.0) } else { 0.0 };
let inner = &self.inner;
Mirror {
inner: Arc::new(Inner {
name: inner.name.clone(),
backends: Arc::clone(&inner.backends),
percent,
seen: AtomicU64::new(0),
next: AtomicU64::new(0),
}),
}
}
pub fn name(&self) -> &str {
&self.inner.name
}
fn sampled(&self) -> bool {
let p = self.inner.percent;
if p >= 100.0 {
return true;
}
if p <= 0.0 {
return false;
}
let n = self.inner.seen.fetch_add(1, Ordering::Relaxed) as f64;
((n + 1.0) * p / 100.0).floor() > (n * p / 100.0).floor()
}
pub fn send(&self, request: &Request, connection: &ConnectionInfo) -> bool {
if !self.sampled() {
return false;
}
if request.body.is_stream() || request.body.len() > MAX_MIRRORED_BODY {
count(&self.inner.name, "skipped");
return false;
}
let backend = {
let backends = self.inner.backends.read().unwrap_or_else(|e| e.into_inner());
if backends.is_empty() {
count(&self.inner.name, "failed");
return false;
}
let i = self.inner.next.fetch_add(1, Ordering::Relaxed) as usize % backends.len();
backends[i].clone()
};
let job = Job { mirror: self.inner.name.clone(), backend, request: request.clone(), client_ip: connection.client.ip.clone(), tls: connection.tls };
match queue().try_send(job) {
Ok(()) => true,
Err(TrySendError::Full(job)) | Err(TrySendError::Disconnected(job)) => {
count(&job.mirror, "dropped");
false
}
}
}
}
struct Job {
mirror: String,
backend: String,
request: Request,
client_ip: String,
tls: bool,
}
fn queue() -> &'static SyncSender<Job> {
static QUEUE_TX: OnceLock<SyncSender<Job>> = OnceLock::new();
QUEUE_TX.get_or_init(|| {
let (tx, rx) = sync_channel::<Job>(QUEUE);
let rx = Arc::new(Mutex::new(rx));
for i in 0..WORKERS {
let rx = Arc::clone(&rx);
let _ = std::thread::Builder::new().name(format!("rws-mirror-{i}")).spawn(move || loop {
let job = match rx.lock().unwrap_or_else(|e| e.into_inner()).recv() {
Ok(job) => job,
Err(_) => return,
};
let outcome = if deliver(&job) { "sent" } else { "failed" };
count(&job.mirror, outcome);
});
}
tx
})
}
fn deliver(job: &Job) -> bool {
let Some((host, port, tls)) = crate::proxy_config::health::parse_backend_url(&job.backend) else { return false };
crate::proxy::forward_upstream(
crate::proxy::shared_pool(),
&job.request,
crate::proxy::Peer { ip: &job.client_ip, tls: job.tls },
&host,
port,
tls,
CONNECT_TIMEOUT,
READ_TIMEOUT,
)
.map(|response| {
if let Some(mut pipe) = response.stream_pipe {
let _ = std::io::copy(&mut pipe, &mut std::io::sink());
}
})
.is_ok()
}
fn counts() -> &'static Mutex<BTreeMap<(String, &'static str), u64>> {
static COUNTS: OnceLock<Mutex<BTreeMap<(String, &'static str), u64>>> = OnceLock::new();
COUNTS.get_or_init(Mutex::default)
}
fn count(mirror: &str, outcome: &'static str) {
*counts().lock().unwrap_or_else(|e| e.into_inner()).entry((mirror.to_string(), outcome)).or_default() += 1;
}
pub(crate) fn prometheus_text() -> String {
let counts = counts().lock().unwrap_or_else(|e| e.into_inner());
if counts.is_empty() {
return String::new();
}
let mut out = String::from(
"# 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",
);
for ((mirror, outcome), n) in counts.iter() {
let mirror = mirror.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
out.push_str(&format!("rws_mirror_requests_total{{mirror=\"{mirror}\",outcome=\"{outcome}\"}} {n}\n"));
}
out
}
#[derive(Clone, Debug)]
pub struct MirrorLayer {
mirror: Mirror,
}
impl MirrorLayer {
pub fn new(mirror: Mirror) -> MirrorLayer {
MirrorLayer { mirror }
}
}
impl Middleware for MirrorLayer {
fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
self.mirror.send(request, connection);
next.execute(request, connection)
}
fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
true
}
fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
true
}
}
#[cfg(test)]
mod tests;