use std::{
collections::HashMap,
sync::atomic::{self, AtomicUsize},
};
use http::{Extensions, StatusCode};
use itertools::Itertools;
use reqwest::{Request, Response, ResponseBuilderExt};
use reqwest_middleware::{Middleware, Next, Result};
use url::Url;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Mirror {
pub url: Url,
pub no_zstd: bool,
pub no_bz2: bool,
pub no_jlap: bool,
pub max_failures: Option<usize>,
}
#[allow(dead_code)]
struct MirrorState {
failures: AtomicUsize,
mirror: Mirror,
}
impl MirrorState {
pub fn add_failure(&self) {
self.failures.fetch_add(1, atomic::Ordering::Relaxed);
}
}
pub struct MirrorMiddleware {
mirror_map: HashMap<Url, Vec<MirrorState>>,
sorted_keys: Vec<(String, Url)>,
}
impl MirrorMiddleware {
pub fn from_map(mirror_map: HashMap<Url, Vec<Mirror>>) -> Self {
let mirror_map: HashMap<Url, Vec<MirrorState>> = mirror_map
.into_iter()
.map(|(url, mirrors)| {
let mirrors = mirrors
.into_iter()
.map(|mirror| MirrorState {
failures: AtomicUsize::new(0),
mirror,
})
.collect();
(url, mirrors)
})
.collect();
let sorted_keys = mirror_map
.keys()
.cloned()
.sorted_by(|a, b| b.path().len().cmp(&a.path().len()))
.map(|k| (k.to_string(), k.clone()))
.collect::<Vec<(String, Url)>>();
Self {
mirror_map,
sorted_keys,
}
}
pub fn keys(&self) -> &[(String, Url)] {
&self.sorted_keys
}
}
fn select_mirror(mirrors: &[MirrorState]) -> Option<&MirrorState> {
let mut min_failures = usize::MAX;
let mut min_failures_index = usize::MAX;
for (i, mirror) in mirrors.iter().enumerate() {
let failures = mirror.failures.load(atomic::Ordering::Relaxed);
if failures < min_failures
&& mirror
.mirror
.max_failures
.map_or(true, |max| failures < max)
{
min_failures = failures;
min_failures_index = i;
}
}
if min_failures_index == usize::MAX {
return None;
}
Some(&mirrors[min_failures_index])
}
#[async_trait::async_trait]
impl Middleware for MirrorMiddleware {
async fn handle(
&self,
mut req: Request,
extensions: &mut Extensions,
next: Next<'_>,
) -> Result<Response> {
let url_str = req.url().to_string();
for (key, url) in self.keys() {
if let Some(url_rest) = url_str.strip_prefix(key) {
let url_rest = url_rest.trim_start_matches('/');
let mirrors = self.mirror_map.get(url).unwrap();
let selected_mirror = select_mirror(mirrors);
let Some(selected_mirror) = selected_mirror else {
return Ok(create_404_response(req.url(), "All mirrors are dead"));
};
let mirror = &selected_mirror.mirror;
let selected_url = mirror.url.join(url_rest).unwrap();
if url_rest.ends_with(".json.zst") && mirror.no_zstd {
return Ok(create_404_response(
&selected_url,
"Mirror does not support zstd",
));
}
if url_rest.ends_with(".json.bz2") && mirror.no_bz2 {
return Ok(create_404_response(
&selected_url,
"Mirror does not support bz2",
));
}
if url_rest.ends_with(".jlap") && mirror.no_jlap {
return Ok(create_404_response(
&selected_url,
"Mirror does not support jlap",
));
}
*req.url_mut() = selected_url;
let res = next.run(req, extensions).await;
match res.as_ref() {
Ok(res) if res.status().is_server_error() => selected_mirror.add_failure(),
Err(_) => selected_mirror.add_failure(),
_ => {}
}
return res;
}
}
next.run(req, extensions).await
}
}
pub(crate) fn create_404_response(url: &Url, body: &str) -> Response {
Response::from(
http::response::Builder::new()
.status(StatusCode::NOT_FOUND)
.url(url.clone())
.body(body.to_string())
.unwrap(),
)
}
#[cfg(test)]
mod test {
use std::{future::IntoFuture, net::SocketAddr};
use axum::{extract::State, http::StatusCode, routing::get, Router};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use url::Url;
use crate::MirrorMiddleware;
use super::Mirror;
async fn count(State(name): State<String>) -> String {
format!("Hi from counter: {name}")
}
async fn broken_return() -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
async fn test_server(name: &str, broken: bool) -> Url {
let state = String::from(name);
let router = if broken {
Router::new().route("/count", get(broken_return))
} else {
Router::new().route("/count", get(count)).with_state(state)
};
let addr = SocketAddr::new([127, 0, 0, 1].into(), 0);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
let addr = listener.local_addr().unwrap();
let service = router.into_make_service();
tokio::spawn(axum::serve(listener, service).into_future());
format!("http://{}:{}", addr.ip(), addr.port())
.parse()
.unwrap()
}
#[tokio::test]
async fn test_mirror_middleware() {
let addr_1 = test_server("server 1", false).await;
let addr_2 = test_server("server 2", false).await;
let mut mirror_map = std::collections::HashMap::new();
mirror_map.insert(
"http://bla.com".parse().unwrap(),
vec![mirror_setting(addr_1), mirror_setting(addr_2)],
);
let middleware = crate::MirrorMiddleware::from_map(mirror_map);
let client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new())
.with(middleware)
.build();
let res = client.get("http://bla.com/count").send().await.unwrap();
assert!(res.status().is_success());
let res = res.text().await.unwrap();
println!("{res}");
assert!(res == "Hi from counter: server 1");
}
fn mirror_setting(url: Url) -> Mirror {
Mirror {
url,
no_zstd: false,
no_bz2: false,
no_jlap: false,
max_failures: Some(3),
}
}
#[tokio::test]
async fn test_mirror_middleware_broken() {
let addr_1 = test_server("server 1", true).await;
let addr_2 = test_server("server 2", false).await;
let mut mirror_map = std::collections::HashMap::new();
mirror_map.insert(
"http://bla.com".parse().unwrap(),
vec![mirror_setting(addr_1), mirror_setting(addr_2)],
);
let middleware = MirrorMiddleware::from_map(mirror_map.clone());
let client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new())
.with(middleware)
.build();
let res = client.get("http://bla.com/count").send().await.unwrap();
assert!(res.status().is_server_error());
let res = client.get("http://bla.com/count").send().await.unwrap();
assert!(res.status().is_success());
assert!(res.text().await.unwrap() == "Hi from counter: server 2");
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
let middleware = MirrorMiddleware::from_map(mirror_map);
let client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new())
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.with(middleware)
.build();
let res = client.get("http://bla.com/count").send().await.unwrap();
assert!(res.status().is_success());
assert!(res.text().await.unwrap() == "Hi from counter: server 2");
}
#[test]
fn test_mirror_sort() {
let keys: Vec<Url> = vec![
"http://bla.com/abc/def".parse().unwrap(),
"http://bla.com/abc".parse().unwrap(),
"http://bla.com/abc/def/ghi".parse().unwrap(),
];
let mirror_middleware =
MirrorMiddleware::from_map(keys.into_iter().map(|k| (k.clone(), vec![])).collect());
let mut len = mirror_middleware.keys()[0].0.len();
for path in mirror_middleware.keys().iter() {
assert!(path.0.len() <= len);
len = path.0.len();
}
}
}