This commit is contained in:
Mateusz Gruszczyński committed 2026-11-06 23:16:36 +01:00
1 parent 09414dfb09
commit f4606cce18
36 files changed
+641 -74

No files matched your search

+129
View File
@@ -832,4 +832,133 @@ mod tests {
assert_eq!(state.last_status.load(Ordering::Relaxed), 503);
assert_eq!(lock(&state.last_failure).detail, "status=503");
}
fn health_port_config(backend_port: u16, health: serde_json::Value) -> crate::config::Config {
serde_json::from_value(serde_json::json!({
"listeners":{"endpoints":[{"id":"http","binding":{"address":"127.0.0.1:8080"}}]},
"vhosts":[{
"id":"app","frontend":{"hosts":["app.localhost"],"listener_ids":["http"]},
"backends":[{"id":"a","url":format!("http://127.0.0.1:{backend_port}"),"allow_private":true}],
"load_balancing":{"health_check":health}
}]})).unwrap()
}
fn health_port_policy() -> crate::config::ValidationPolicy {
crate::config::ValidationPolicy {
proxy_addr: "127.0.0.1:8080".parse().unwrap(),
admin_addr: "127.0.0.1:9090".parse().unwrap(),
tls_addr: None,
cert_dir: None,
data_dir: None,
}
}
#[test]
fn health_check_port_compiles_separate_probe_target() {
let policy = health_port_policy();
let snapshot = crate::config::compile(
health_port_config(9000, serde_json::json!({"mode":"tcp","port":8079})), 0, &policy).unwrap();
let backend = &snapshot.pools[0].members[0].backend;
assert_eq!(backend.origin, "http://127.0.0.1:9000");
assert_eq!(backend.health_probe.as_ref().unwrap().origin, "http://127.0.0.1:8079");
for health in [
serde_json::json!({"mode":"tcp","port":9000}),
serde_json::json!({"mode":"tcp"}),
serde_json::json!({"mode":"off","port":8079}),
] {
let snapshot = crate::config::compile(health_port_config(9000, health), 0, &policy).unwrap();
assert!(snapshot.pools[0].members[0].backend.health_probe.is_none());
}
assert!(crate::config::compile(
health_port_config(9000, serde_json::json!({"mode":"tcp","port":0})), 0, &policy).is_err());
assert!(crate::config::compile(
health_port_config(9000, serde_json::json!({"mode":"http","port":9090})), 0, &policy).is_err());
}
#[tokio::test]
async fn health_check_probes_configured_port_instead_of_traffic_port() {
let traffic = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let traffic_port = traffic.local_addr().unwrap().port();
let closed_port = {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
listener.local_addr().unwrap().port()
};
let policy = health_port_policy();
let config = health_port_config(traffic_port, serde_json::json!({
"mode":"tcp","port":closed_port,"fall":1,"rise":1,"interval_ms":250,"timeout_ms":200,"slow_response_ms":0}));
let snapshot = crate::config::compile(config, 0, &policy).unwrap();
let member = snapshot.pools[0].members[0].clone();
let settings = member.health_settings.clone();
probe(member.clone(), settings.clone()).await;
assert!(!member.state.healthy(), "probe must use the closed health port");
let health = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let health_port = health.local_addr().unwrap().port();
drop(traffic);
let config = health_port_config(traffic_port, serde_json::json!({
"mode":"tcp","port":health_port,"fall":1,"rise":1,"interval_ms":250,"timeout_ms":200,"slow_response_ms":0}));
let snapshot = crate::config::compile(config, 0, &policy).unwrap();
let member = snapshot.pools[0].members[0].clone();
let settings = member.health_settings.clone();
probe(member.clone(), settings).await;
assert!(member.state.healthy(), "probe must succeed on the open health port while traffic port is closed");
tokio::time::timeout(Duration::from_secs(1), health.accept())
.await
.expect("health port must receive the probe connection")
.unwrap();
}
async fn one_shot_http(body: &'static str) -> (u16, tokio::task::JoinHandle<()>) {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let task = tokio::spawn(async move {
while let Ok((mut socket, _)) = listener.accept().await {
let mut buffer = [0u8; 2048];
let _ = socket.read(&mut buffer).await;
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
body.len(), body
);
let _ = socket.write_all(response.as_bytes()).await;
}
});
(port, task)
}
#[tokio::test]
async fn http_health_check_requires_expected_body_text() {
let policy = health_port_policy();
for (body, healthy) in [("{\"status\":\"ok\"}", true), ("{\"status\":\"degraded\"}", false)] {
let (port, server) = one_shot_http(body).await;
let config = health_port_config(port, serde_json::json!({
"mode":"http","path":"/health","expect_body":"\"status\":\"ok\"",
"fall":1,"rise":1,"interval_ms":1000,"timeout_ms":800,"slow_response_ms":0}));
let snapshot = crate::config::compile(config, 0, &policy).unwrap();
let member = snapshot.pools[0].members[0].clone();
probe(member.clone(), member.health_settings.clone()).await;
assert_eq!(member.state.healthy(), healthy, "body {body}");
if !healthy {
assert_eq!(lock(&member.state.health).result, "http_body");
}
server.abort();
}
for invalid in ["", "line\nbreak"] {
assert!(crate::config::compile(
health_port_config(9000, serde_json::json!({"mode":"http","expect_body":invalid})), 0, &policy).is_err());
}
}
#[tokio::test]
async fn health_check_port_survives_live_reload_with_reused_clients() {
let policy = health_port_policy();
let defaults = (policy.proxy_addr, policy.tls_addr);
let first = crate::config::compile_reusing(
health_port_config(9000, serde_json::json!({"mode":"tcp"})), 0, &policy, defaults, None).unwrap();
assert!(first.pools[0].members[0].health_probe.is_none());
let second = crate::config::compile_reusing(
health_port_config(9000, serde_json::json!({"mode":"tcp","port":8079})), 1, &policy, defaults, Some(&first)).unwrap();
let member = &second.pools[0].members[0];
assert!(Arc::ptr_eq(&member.backend, &first.pools[0].members[0].backend), "traffic clients are reused");
assert_eq!(member.health_probe.as_ref().unwrap().origin, "http://127.0.0.1:8079");
let third = crate::config::compile_reusing(
health_port_config(9000, serde_json::json!({"mode":"tcp"})), 2, &policy, defaults, Some(&second)).unwrap();
assert!(third.pools[0].members[0].health_probe.is_none());
}
}