#[cfg(test)]
mod tests;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::application::Application;
use crate::core::New;
use crate::middleware::Middleware;
use crate::mime_type::MimeType;
use crate::range::Range;
use crate::request::Request;
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
use crate::server::ConnectionInfo;
use crate::service_discovery::BackendPool;
#[derive(Clone)]
pub struct WeightedBackend {
pub url: String,
pub weight: u32,
}
impl WeightedBackend {
pub fn new(url: impl Into<String>, weight: u32) -> Self {
Self { url: url.into(), weight }
}
}
#[derive(Clone)]
pub struct WeightedPool {
pub pool: BackendPool,
pub weight: u32,
}
impl WeightedPool {
pub fn new(pool: BackendPool, weight: u32) -> Self {
Self { pool, weight }
}
}
enum Target {
Static(String, u16, bool),
Pool(BackendPool, AtomicUsize),
}
struct SwrrEntry {
target: Target,
weight: i64,
current_weight: i64,
}
struct CanaryState {
entries: Vec<SwrrEntry>,
total_weight: i64,
}
impl CanaryState {
fn new() -> Self {
Self { entries: Vec::new(), total_weight: 0 }
}
fn push_static(&mut self, host: String, port: u16, tls: bool, weight: u32) {
if weight == 0 {
return;
}
self.total_weight += weight as i64;
self.entries.push(SwrrEntry {
target: Target::Static(host, port, tls),
weight: weight as i64,
current_weight: 0,
});
}
fn push_pool(&mut self, pool: BackendPool, weight: u32) {
if weight == 0 {
return;
}
self.total_weight += weight as i64;
self.entries.push(SwrrEntry {
target: Target::Pool(pool, AtomicUsize::new(0)),
weight: weight as i64,
current_weight: 0,
});
}
fn tick(&mut self) -> Vec<usize> {
if self.entries.is_empty() {
return Vec::new();
}
for e in &mut self.entries {
e.current_weight += e.weight;
}
let best = self
.entries
.iter()
.enumerate()
.max_by_key(|(_, e)| e.current_weight)
.map(|(i, _)| i)
.unwrap();
self.entries[best].current_weight -= self.total_weight;
let weights: Vec<i64> = self.entries.iter().map(|e| e.current_weight).collect();
let mut order: Vec<usize> = (0..self.entries.len()).collect();
order.sort_by(|&a, &b| {
if a == best {
return std::cmp::Ordering::Less;
}
if b == best {
return std::cmp::Ordering::Greater;
}
weights[b].cmp(&weights[a])
});
order
}
}
#[derive(Clone)]
pub struct CanaryLayer {
state: Arc<Mutex<CanaryState>>,
connect_timeout: Duration,
read_timeout: Duration,
path_prefix: Option<String>,
}
impl CanaryLayer {
pub fn new(backends: Vec<WeightedBackend>) -> Self {
Self::from_parts(backends, Vec::new())
}
pub fn with_pools(pools: Vec<WeightedPool>) -> Self {
Self::from_parts(Vec::new(), pools)
}
pub fn from_parts(backends: Vec<WeightedBackend>, pools: Vec<WeightedPool>) -> Self {
let mut state = CanaryState::new();
for wb in backends {
if let Some((host, port, tls)) = parse_backend_url(&wb.url) {
state.push_static(host, port, tls, wb.weight);
}
}
for wp in pools {
state.push_pool(wp.pool, wp.weight);
}
Self {
state: Arc::new(Mutex::new(state)),
connect_timeout: Duration::from_secs(5),
read_timeout: Duration::from_secs(30),
path_prefix: None,
}
}
pub fn add_pool(self, pool: BackendPool, weight: u32) -> Self {
self.state.lock().unwrap().push_pool(pool, weight);
self
}
pub fn update(&self, backends: Vec<WeightedBackend>, pools: Vec<WeightedPool>) {
let mut new_state = CanaryState::new();
for wb in backends {
if let Some((host, port, tls)) = parse_backend_url(&wb.url) {
new_state.push_static(host, port, tls, wb.weight);
}
}
for wp in pools {
new_state.push_pool(wp.pool, wp.weight);
}
*self.state.lock().unwrap() = new_state;
}
pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
self.path_prefix = Some(prefix.into());
self
}
pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
self.connect_timeout = Duration::from_millis(ms);
self
}
pub fn read_timeout_ms(mut self, ms: u64) -> Self {
self.read_timeout = Duration::from_millis(ms);
self
}
fn next_candidates(&self) -> Vec<(String, u16, bool)> {
let mut state = self.state.lock().unwrap();
let order = state.tick();
let mut out = Vec::new();
for idx in order {
match &state.entries[idx].target {
Target::Static(host, port, tls) => out.push((host.clone(), *port, *tls)),
Target::Pool(pool, cursor) => {
let members = pool.backends();
if members.is_empty() {
continue;
}
let n = members.len();
let start = cursor.fetch_add(1, Ordering::Relaxed);
for i in 0..n {
if let Some((host, port)) = split_host_port(&members[(start + i) % n]) {
out.push((host, port, false));
}
}
}
}
}
out
}
fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
let candidates = self.next_candidates();
if candidates.is_empty() {
return Err("CanaryLayer: no backends in rotation".to_string());
}
for (host, port, tls) in &candidates {
let result = if *tls {
#[cfg(any(feature = "http-client", feature = "http2"))]
{
crate::proxy::proxy_https1(
request,
&connection.client.ip,
host,
*port,
self.connect_timeout,
self.read_timeout,
)
}
#[cfg(not(any(feature = "http-client", feature = "http2")))]
{
Err("CanaryLayer: TLS backend requires the http-client or http2 feature".to_string())
}
} else {
crate::proxy::proxy_http1(
request,
&connection.client.ip,
host,
*port,
self.connect_timeout,
self.read_timeout,
)
};
if let Ok(resp) = result {
return Ok(resp);
}
}
Err("CanaryLayer: all backends failed".to_string())
}
}
impl Middleware for CanaryLayer {
fn handle(
&self,
request: &Request,
connection: &ConnectionInfo,
next: &dyn Application,
) -> Result<Response, String> {
if let Some(prefix) = &self.path_prefix {
if !request.request_uri.starts_with(prefix.as_str()) {
return next.execute(request, connection);
}
}
match self.proxy(request, connection) {
Ok(resp) => Ok(resp),
Err(_) => Ok(bad_gateway()),
}
}
}
fn parse_backend_url(url: &str) -> Option<(String, u16, bool)> {
let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
(r, true, 443u16)
} else if let Some(r) = url.strip_prefix("h2s://") {
(r, true, 443u16)
} else if let Some(r) = url.strip_prefix("grpcs://") {
(r, true, 443u16)
} else if let Some(r) = url.strip_prefix("http://") {
(r, false, 80u16)
} else if let Some(r) = url.strip_prefix("h2://") {
(r, false, 80u16)
} else if let Some(r) = url.strip_prefix("grpc://") {
(r, false, 80u16)
} else {
(url, false, 80u16)
};
let host_port = rest.split('/').next().unwrap_or(rest);
let (host, port) = if let Some(colon) = host_port.rfind(':') {
let port_str = &host_port[colon + 1..];
if let Ok(p) = port_str.parse::<u16>() {
(host_port[..colon].to_string(), p)
} else {
(host_port.to_string(), default_port)
}
} else {
(host_port.to_string(), default_port)
};
if host.is_empty() { None } else { Some((host, port, tls)) }
}
fn split_host_port(addr: &str) -> Option<(String, u16)> {
let colon = addr.rfind(':')?;
let port: u16 = addr[colon + 1..].parse().ok()?;
let host = addr[..colon].to_string();
if host.is_empty() { None } else { Some((host, port)) }
}
fn bad_gateway() -> Response {
let cr = Range::get_content_range(
b"502 Bad Gateway".to_vec(),
MimeType::TEXT_PLAIN.to_string(),
);
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n502_bad_gateway.reason_phrase.to_string();
r.content_range_list = vec![cr];
r
}