v0.34.0_2

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-18 06:13:42 +02:00
1 parent 434e789144
commit 9cfc4588cc
18 files changed
+392 -121

No files matched your search

+1 -1
View File
@@ -4193,7 +4193,7 @@
"type": "integer",
"minimum": 1,
"maximum": 64,
"default": 4
"default": 64
},
"tcp_keepalive_seconds": {
"type": "integer",
+1 -1
View File
@@ -195,7 +195,7 @@ function load(value = defaults(), migrationIds = [], show = true) {
note(g, "vhRetryHelp");
field(g, "keepalive", "vhKeepalive", keepalive.enabled !== false, "checkbox");
field(g, "idle-timeout", "vhIdleTimeout", keepalive.idle_timeout_seconds ?? 30, "number", { required: true, min: 1, max: 3600 });
field(g, "max-idle", "vhMaxIdle", keepalive.max_idle_per_host ?? 4, "number", { required: true, min: 1, max: 64 });
field(g, "max-idle", "vhMaxIdle", keepalive.max_idle_per_host ?? 64, "number", { required: true, min: 1, max: 64 });
field(g, "tcp-keepalive", "vhTcpKeepalive", keepalive.tcp_keepalive_seconds ?? 30, "number", { required: true, min: 1, max: 3600 });
field(g, "pool-recycle-requests", "vhPoolRecycleRequests", keepalive.pool_recycle_requests ?? 25000, "number", { required: true, min: 0, max: 10000000 });
field(g, "pool-recycle-seconds", "vhPoolRecycleSeconds", keepalive.pool_recycle_seconds ?? 3600, "number", { required: true, min: 0, max: 604800 });
+4 -1
View File
@@ -70,7 +70,10 @@ impl Default for Keepalive {
Self {
enabled: true,
idle_timeout_seconds: 30,
max_idle_per_host: 4,
// Retain enough reusable H1 upstream sockets under concurrent
// small-response traffic. The existing 1..=64 bound, idle
// timeout and pool recycling still limit resource retention.
max_idle_per_host: 64,
tcp_keepalive_seconds: 30,
pool_recycle_requests: 25_000,
pool_recycle_seconds: 3_600,
+5
View File
@@ -102,6 +102,11 @@ mod tests {
assert!(parsed.retry.retry_connect_errors);
assert!(parsed.retry.retry_timeouts);
assert_eq!(parsed.retry.retry_statuses, vec![502, 503, 504]);
assert_eq!(parsed.keepalive.max_idle_per_host, 64);
let explicit: Settings = serde_json::from_value(serde_json::json!({
"keepalive": { "max_idle_per_host": 4 }
})).unwrap();
assert_eq!(explicit.keepalive.max_idle_per_host, 4);
assert_eq!(parsed.keepalive.pool_recycle_requests, 25_000);
assert_eq!(parsed.keepalive.pool_recycle_seconds, 3_600);
assert_eq!(parsed.keepalive.http1_max_buffer_bytes, 131_072);
+14 -15
View File
@@ -176,30 +176,28 @@ impl Drop for Exchange {
_ => &self.metrics.status_5xx,
}
.fetch_add(1, Ordering::Relaxed);
// Final Arc drop has exclusive access: move strings without locking.
let backend = self
.backend
.lock()
.get_mut()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
.map(|l| l.member.backend.id.clone());
let websocket_phase = *self
.websocket_phase
.lock()
.unwrap_or_else(|e| e.into_inner());
let route = self.route.lock().unwrap_or_else(|e| e.into_inner()).clone();
let websocket_phase = *self.websocket_phase.get_mut().unwrap_or_else(|e| e.into_inner());
let route = self.route.get_mut().unwrap_or_else(|e| e.into_inner()).take();
let source = ExchangeSource::name(self.source.load(Ordering::Relaxed));
let cache_effective_enabled = read_effective(self.cache_effective_enabled.load(Ordering::Relaxed));
let transform_effective_enabled = read_effective(self.transform_effective_enabled.load(Ordering::Relaxed));
let client_ip = self
.client_ip
.lock()
.get_mut()
.unwrap_or_else(|e| e.into_inner())
.clone();
.take();
let country = self
.country
.lock()
.get_mut()
.unwrap_or_else(|e| e.into_inner())
.clone();
.take();
let request_bytes = self.request_bytes.load(Ordering::Relaxed);
let response_bytes = self.response_bytes.load(Ordering::Relaxed);
let duration_ms = duration.as_millis() as u64;
@@ -248,7 +246,7 @@ impl Drop for Exchange {
sni: None,
authority: None,
matched_by: None,
cache_reason: *self.cache_reason.lock().unwrap_or_else(|e| e.into_inner()),
cache_reason: *self.cache_reason.get_mut().unwrap_or_else(|e| e.into_inner()),
vhost: self.vhost.take(),
route,
cache_effective_enabled,
@@ -266,18 +264,18 @@ impl Drop for Exchange {
websocket_phase,
websocket_error: *self
.websocket_error
.lock()
.get_mut()
.unwrap_or_else(|e| e.into_inner()),
h3_reset_code: *self.h3_reset_code.lock().unwrap_or_else(|e| e.into_inner()),
h3_reset_code: *self.h3_reset_code.get_mut().unwrap_or_else(|e| e.into_inner()),
upstream_status: match self.upstream_status.load(Ordering::Relaxed) {
0 => None,
value => Some(value),
},
upstream_error: self
.upstream_error
.lock()
.get_mut()
.unwrap_or_else(|e| e.into_inner())
.clone(),
.take(),
client_ip,
user_agent: self.user_agent.take(),
country,
@@ -290,6 +288,7 @@ impl Drop for Exchange {
});
}
}
#[derive(Clone, Copy)]
pub enum Direction {
Request,
+24
View File
@@ -226,6 +226,30 @@ mod tests {
}
}
#[tokio::test]
async fn completion_moves_owned_fields_but_preserves_log_identity() {
let (exchange, slots) = observation().await;
let logs = exchange.logs.clone();
*exchange.route.lock().unwrap() = Some("route-1".into());
*exchange.client_ip.lock().unwrap() = Some("198.51.100.9".into());
*exchange.country.lock().unwrap() = Some("DE".into());
*exchange.upstream_error.lock().unwrap() = Some("upstream closed".into());
*exchange.cache_reason.lock().unwrap() = "cache_disabled";
*exchange.websocket_error.lock().unwrap() = Some("upstream_http_error");
*exchange.h3_reset_code.lock().unwrap() = Some("h3_general_protocol_error");
drop(exchange);
assert_eq!(slots.available_permits(), 1);
let entries = logs.recent();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].route.as_deref(), Some("route-1"));
assert_eq!(entries[0].client_ip.as_deref(), Some("198.51.100.9"));
assert_eq!(entries[0].country.as_deref(), Some("DE"));
assert_eq!(entries[0].upstream_error.as_deref(), Some("upstream closed"));
assert_eq!(entries[0].cache_reason, "cache_disabled");
assert_eq!(entries[0].websocket_error, Some("upstream_http_error"));
assert_eq!(entries[0].h3_reset_code, Some("h3_general_protocol_error"));
}
#[tokio::test]
async fn atomic_exchange_tags_keep_original_observability_fields() {
let (exchange, slots) = observation().await;
+6
View File
@@ -45,6 +45,9 @@ include!("headers/tests.rs");
/// guarded self-admin reverse-proxy path may restore only those fragments after
/// the actual backend has been selected and verified.
pub fn strip_admin_cookie(headers: &mut HeaderMap) -> Vec<String> {
if !headers.contains_key(http::header::COOKIE) {
return Vec::new();
}
let mut retained = Vec::new();
let mut admin = Vec::new();
for part in headers
@@ -91,6 +94,9 @@ pub fn restore_admin_cookie(headers: &mut HeaderMap, admin: &[String]) {
}
/// Upstreams cannot overwrite the reserved admin cookie through this proxy listener.
pub fn strip_admin_set_cookie(headers: &mut HeaderMap) {
if !headers.contains_key(http::header::SET_COOKIE) {
return;
}
let retained: Vec<_> = headers
.get_all(http::header::SET_COOKIE)
.iter()
+23
View File
@@ -19,6 +19,29 @@ mod tests {
h.insert("connection", "keep-alive, Upgrade".parse().unwrap());
assert!(requests_upgrade(&h));
}
#[test]
fn absent_cookies_leave_headers_unchanged() {
let mut headers = HeaderMap::new();
headers.insert("x-pass", "yes".parse().unwrap());
assert!(strip_admin_cookie(&mut headers).is_empty());
strip_admin_set_cookie(&mut headers);
assert_eq!(headers.get("x-pass").unwrap(), "yes");
assert!(!headers.contains_key(http::header::COOKIE));
assert!(!headers.contains_key(http::header::SET_COOKIE));
}
#[test]
fn set_cookie_keeps_valid_unreserved_values() {
let mut headers = HeaderMap::new();
headers.append("set-cookie", "a=1; Secure".parse().unwrap());
headers.append("set-cookie", "rustproxy_session=secret".parse().unwrap());
headers.append("set-cookie", "b=2; HttpOnly".parse().unwrap());
strip_admin_set_cookie(&mut headers);
let cookies: Vec<_> = headers.get_all("set-cookie").iter()
.map(|value| value.to_str().unwrap()).collect();
assert_eq!(cookies, vec!["a=1; Secure", "b=2; HttpOnly"]);
}
#[test]
fn admin_cookie_can_be_restored_only_from_captured_fragment() {
let mut h = HeaderMap::new();