This commit is contained in:
Mateusz Gruszczyński committed 2026-10-14 14:54:22 +02:00
1 parent 1b9135117c
commit 4f36de240c
32 files changed
+1175 -704

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@
}
if let Some(lease) = &active_lease {
lease.error_detail(
upstream_error_class(&cause, connect_failure),
upstream_health_error_class(&cause, connect_failure, &active_backend),
&detail,
final_attempt_elapsed,
);
+32 -1
View File
@@ -141,6 +141,7 @@ let mut active_uri = upstream_target;
let mut request_attempt = Some(request);
let mut attempted_backends = Vec::<String>::new();
let mut retries = 0u8;
let mut graceful_retry_done = false;
let upstream_result = loop {
let client = if handshake.is_some() && !websocket_upstream_h2 {
&active_backend.websocket_client
@@ -153,6 +154,36 @@ let upstream_result = loop {
)
.await;
let attempt_elapsed = attempt_started.elapsed();
if let Ok(Err(cause)) = &result {
if !graceful_retry_done
&& !admin_control_plane
&& handshake.is_none()
&& request_body_empty
&& retry_safe_method(&retry_method)
&& (is_graceful_h2_goaway(cause)
|| is_related_h2_close(cause, &active_backend))
{
if let Some(next_request) = retry_request(
&retry_method,
upstream_request_version(&active_backend, false),
&retry_headers,
&active_backend,
&original_host,
&origin_suffix,
retry_host_follows_backend,
) {
graceful_retry_done = true;
if let Some(lease) = &active_lease {
lease.retry_attempted();
}
tracing::debug!(backend = %active_backend.id,
"retrying safe empty request after graceful upstream H2 shutdown");
request_attempt = Some(next_request);
attempt_started = Instant::now();
continue;
}
}
}
let Some(policy) = retry_policy.as_ref() else {
break result;
};
@@ -220,7 +251,7 @@ let upstream_result = loop {
connect_failure,
);
lease.error_detail(
upstream_error_class(cause, connect_failure),
upstream_health_error_class(cause, connect_failure, &active_backend),
&detail,
attempt_elapsed,
);
+27
View File
@@ -0,0 +1,27 @@
fn is_graceful_h2_goaway(error: &(dyn std::error::Error + 'static)) -> bool {
crate::upstream::is_graceful_h2_goaway(error)
}
fn is_related_h2_close(
error: &(dyn std::error::Error + 'static),
backend: &crate::config::CompiledBackend,
) -> bool {
let chain = format!("{error:?}");
backend.client.recently_saw_graceful_goaway()
&& chain.contains("Canceled")
&& chain.contains("connection closed")
}
fn upstream_health_error_class(
error: &(dyn std::error::Error + 'static),
connect_failure: bool,
backend: &crate::config::CompiledBackend,
) -> &'static str {
if is_graceful_h2_goaway(error) {
"h2_graceful_goaway"
} else if is_related_h2_close(error, backend) {
"h2_graceful_close"
} else {
upstream_error_class(error, connect_failure)
}
}
+25
View File
@@ -571,3 +571,28 @@ mod authority_tests {
}
}
}
#[cfg(test)]
mod graceful_h2_tests {
use super::*;
use std::fmt;
#[derive(Debug)]
struct H2DebugError(&'static str);
impl fmt::Display for H2DebugError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "client error (SendRequest): {}", self.0)
}
}
impl std::error::Error for H2DebugError {}
#[test]
fn only_graceful_remote_h2_goaway_is_neutral() {
let valid = H2DebugError("GoAway(b\"\", NO_ERROR, Remote)");
let protocol = H2DebugError("GoAway(b\"\", PROTOCOL_ERROR, Remote)");
let local = H2DebugError("GoAway(b\"\", NO_ERROR, Local)");
assert!(is_graceful_h2_goaway(&valid));
assert!(!is_graceful_h2_goaway(&protocol));
assert!(!is_graceful_h2_goaway(&local));
}
}
+8 -5
View File
@@ -138,6 +138,8 @@ fn upstream_error_class(
}
}
include!("h2_graceful.rs");
fn retry_safe_method(method: &Method) -> bool {
method == Method::GET || method == Method::HEAD || method == Method::OPTIONS
}
@@ -171,11 +173,12 @@ fn upstream_request_target(
websocket_h1: bool,
websocket_h2: bool,
) -> Option<Uri> {
let h2_capable = !websocket_h1 && match backend.protocol {
crate::config::UpstreamProtocol::Http1 => false,
crate::config::UpstreamProtocol::Auto => backend.origin.starts_with("https://"),
crate::config::UpstreamProtocol::Http2 | crate::config::UpstreamProtocol::H2 => true,
};
let h2_capable = !websocket_h1
&& match backend.protocol {
crate::config::UpstreamProtocol::Http1 => false,
crate::config::UpstreamProtocol::Auto => backend.origin.starts_with("https://"),
crate::config::UpstreamProtocol::Http2 | crate::config::UpstreamProtocol::H2 => true,
};
if !h2_capable {
return Some(uri.clone());
}