mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-28 16:07:05 +00:00
fix(rio): warn once when internode HTTP/2 tuning is inert on plaintext (backlog#805-C3) (#4587)
The internode HTTP/2 window/keepalive tuning (apply_http_client_tuning + http2_keep_alive_* in build_http_client) only takes effect when the connection negotiates HTTP/2, which happens via TLS-ALPN. Over plaintext internode transport the connection is HTTP/1.1 and all the tuning is silently inert, with no signal to the operator. Make this honest without changing protocol behavior (no h2c, no prior_knowledge, no opt-in flag): - InternodeHttpClientTuning gains h2_tuning_explicit, true when the operator set a window size, a non-default tuning profile, or the keepalive env. - Add pure predicate should_warn_h2_inert(negotiated_is_http2, h2_tuning_explicit, already_warned) and maybe_warn_h2_inert, gated by a process-lifetime AtomicBool so the warn fires at most once. - Call maybe_warn_h2_inert at both record_internode_http_version sites using the actual negotiated resp.version(). - Document the TLS-ALPN requirement at apply_http_client_tuning and near the ENV_INTERNODE_HTTP2_* constants. Tests cover the should_warn_h2_inert truth table and that h2_tuning_explicit reflects explicit windows and a non-default profile.
This commit is contained in:
@@ -194,6 +194,12 @@ pub const ENV_INTERNODE_HTTP_POOL_MAX_IDLE_PER_HOST: &str = "RUSTFS_INTERNODE_HT
|
|||||||
pub const ENV_INTERNODE_HTTP_POOL_IDLE_TIMEOUT_SECS: &str = "RUSTFS_INTERNODE_HTTP_POOL_IDLE_TIMEOUT_SECS";
|
pub const ENV_INTERNODE_HTTP_POOL_IDLE_TIMEOUT_SECS: &str = "RUSTFS_INTERNODE_HTTP_POOL_IDLE_TIMEOUT_SECS";
|
||||||
|
|
||||||
/// Internode HTTP/2 initial stream window size in bytes.
|
/// Internode HTTP/2 initial stream window size in bytes.
|
||||||
|
///
|
||||||
|
/// NOTE (backlog#805-C3): all `RUSTFS_INTERNODE_HTTP2_*` tuning (window sizes,
|
||||||
|
/// adaptive window, keepalive timeout) only takes effect when the internode
|
||||||
|
/// connection actually negotiates HTTP/2, which happens via TLS-ALPN. Over
|
||||||
|
/// plaintext internode transport the connection is HTTP/1.1 and these settings
|
||||||
|
/// are silently inert — enable internode TLS to use them.
|
||||||
pub const ENV_INTERNODE_HTTP2_INITIAL_STREAM_WINDOW_SIZE: &str = "RUSTFS_INTERNODE_HTTP2_INITIAL_STREAM_WINDOW_SIZE";
|
pub const ENV_INTERNODE_HTTP2_INITIAL_STREAM_WINDOW_SIZE: &str = "RUSTFS_INTERNODE_HTTP2_INITIAL_STREAM_WINDOW_SIZE";
|
||||||
|
|
||||||
/// Internode HTTP/2 initial connection window size in bytes.
|
/// Internode HTTP/2 initial connection window size in bytes.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ use std::net::IpAddr;
|
|||||||
use std::ops::Not as _;
|
use std::ops::Not as _;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||||
@@ -278,6 +279,11 @@ struct InternodeHttpClientTuning {
|
|||||||
http2_initial_connection_window_size: Option<u32>,
|
http2_initial_connection_window_size: Option<u32>,
|
||||||
http2_adaptive_window: bool,
|
http2_adaptive_window: bool,
|
||||||
proxy_mode: InternodeHttpProxyMode,
|
proxy_mode: InternodeHttpProxyMode,
|
||||||
|
/// True when the operator explicitly configured any HTTP/2 tuning knob
|
||||||
|
/// (a window size, the keepalive timeout, or a non-default tuning profile).
|
||||||
|
/// Used to warn once when that tuning is inert because the internode
|
||||||
|
/// connection negotiated HTTP/1.1 over plaintext (backlog#805-C3).
|
||||||
|
h2_tuning_explicit: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn internode_http_client_tuning() -> InternodeHttpClientTuning {
|
fn internode_http_client_tuning() -> InternodeHttpClientTuning {
|
||||||
@@ -291,7 +297,7 @@ impl InternodeHttpClientTuning {
|
|||||||
fn from_env() -> Self {
|
fn from_env() -> Self {
|
||||||
let profile =
|
let profile =
|
||||||
parse_internode_http_tuning_profile(get_env_opt_str(rustfs_config::ENV_INTERNODE_HTTP_TUNING_PROFILE).as_deref());
|
parse_internode_http_tuning_profile(get_env_opt_str(rustfs_config::ENV_INTERNODE_HTTP_TUNING_PROFILE).as_deref());
|
||||||
Self::from_values(
|
let mut tuning = Self::from_values(
|
||||||
profile,
|
profile,
|
||||||
get_env_opt_usize(rustfs_config::ENV_INTERNODE_HTTP_POOL_MAX_IDLE_PER_HOST),
|
get_env_opt_usize(rustfs_config::ENV_INTERNODE_HTTP_POOL_MAX_IDLE_PER_HOST),
|
||||||
get_env_opt_u64(rustfs_config::ENV_INTERNODE_HTTP_POOL_IDLE_TIMEOUT_SECS),
|
get_env_opt_u64(rustfs_config::ENV_INTERNODE_HTTP_POOL_IDLE_TIMEOUT_SECS),
|
||||||
@@ -302,7 +308,14 @@ impl InternodeHttpClientTuning {
|
|||||||
profile.default_http2_adaptive_window(),
|
profile.default_http2_adaptive_window(),
|
||||||
),
|
),
|
||||||
get_env_opt_str(rustfs_config::ENV_INTERNODE_HTTP_PROXY).as_deref(),
|
get_env_opt_str(rustfs_config::ENV_INTERNODE_HTTP_PROXY).as_deref(),
|
||||||
)
|
);
|
||||||
|
// The keepalive timeout env is read in build_http_client, not passed to
|
||||||
|
// from_values; fold it into the explicit-tuning signal here so the
|
||||||
|
// inert-h2 warning also fires when only the keepalive knob is set.
|
||||||
|
if get_env_opt_u64(rustfs_config::ENV_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS).is_some() {
|
||||||
|
tuning.h2_tuning_explicit = true;
|
||||||
|
}
|
||||||
|
tuning
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_values(
|
fn from_values(
|
||||||
@@ -314,6 +327,11 @@ impl InternodeHttpClientTuning {
|
|||||||
http2_adaptive_window: bool,
|
http2_adaptive_window: bool,
|
||||||
proxy_mode: Option<&str>,
|
proxy_mode: Option<&str>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
// Explicit h2 tuning = an operator-set window size (raw arg, before the
|
||||||
|
// profile fallback) or a non-default (non-Legacy) tuning profile. The
|
||||||
|
// keepalive-env contribution is OR-ed in by from_env.
|
||||||
|
let h2_tuning_explicit =
|
||||||
|
stream_window_size.is_some() || connection_window_size.is_some() || profile != InternodeHttpTuningProfile::Legacy;
|
||||||
Self {
|
Self {
|
||||||
profile,
|
profile,
|
||||||
pool_max_idle_per_host: pool_max_idle_per_host.or_else(|| profile.default_pool_max_idle_per_host()),
|
pool_max_idle_per_host: pool_max_idle_per_host.or_else(|| profile.default_pool_max_idle_per_host()),
|
||||||
@@ -326,6 +344,7 @@ impl InternodeHttpClientTuning {
|
|||||||
),
|
),
|
||||||
http2_adaptive_window,
|
http2_adaptive_window,
|
||||||
proxy_mode: parse_internode_http_proxy_mode(proxy_mode, profile),
|
proxy_mode: parse_internode_http_proxy_mode(proxy_mode, profile),
|
||||||
|
h2_tuning_explicit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,6 +414,16 @@ fn clamp_http2_window(value: Option<u64>) -> Option<u32> {
|
|||||||
u32::try_from(value).ok()
|
u32::try_from(value).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies internode HTTP client tuning (connection pool + HTTP/2 window
|
||||||
|
/// sizes) to a reqwest client builder.
|
||||||
|
///
|
||||||
|
/// IMPORTANT (backlog#805-C3): the HTTP/2 window and keepalive settings only
|
||||||
|
/// take effect when the internode connection actually negotiates HTTP/2, which
|
||||||
|
/// happens via TLS-ALPN. Over plaintext internode transport the connection is
|
||||||
|
/// HTTP/1.1 and every `http2_*` knob here (and the keepalive settings in
|
||||||
|
/// `build_http_client`) is silently inert. Enable internode TLS to use them.
|
||||||
|
/// `should_warn_h2_inert` emits a one-time warning when explicitly-configured
|
||||||
|
/// h2 tuning is observed to be inert on a live connection.
|
||||||
fn apply_http_client_tuning(mut builder: reqwest::ClientBuilder, tuning: InternodeHttpClientTuning) -> reqwest::ClientBuilder {
|
fn apply_http_client_tuning(mut builder: reqwest::ClientBuilder, tuning: InternodeHttpClientTuning) -> reqwest::ClientBuilder {
|
||||||
if let Some(pool_max_idle_per_host) = tuning.pool_max_idle_per_host {
|
if let Some(pool_max_idle_per_host) = tuning.pool_max_idle_per_host {
|
||||||
builder = builder.pool_max_idle_per_host(pool_max_idle_per_host);
|
builder = builder.pool_max_idle_per_host(pool_max_idle_per_host);
|
||||||
@@ -780,6 +809,7 @@ impl HttpReader {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
record_internode_http_version(track_internode_metrics, internode_operation, http_version_metric_label(resp.version()));
|
record_internode_http_version(track_internode_metrics, internode_operation, http_version_metric_label(resp.version()));
|
||||||
|
maybe_warn_h2_inert(resp.version());
|
||||||
if resp.status().is_success().not() {
|
if resp.status().is_success().not() {
|
||||||
record_internode_operation_duration(track_internode_metrics, internode_operation, request_started.elapsed());
|
record_internode_operation_duration(track_internode_metrics, internode_operation, request_started.elapsed());
|
||||||
record_internode_error(track_internode_metrics, internode_operation);
|
record_internode_error(track_internode_metrics, internode_operation);
|
||||||
@@ -1000,6 +1030,7 @@ impl HttpWriter {
|
|||||||
internode_operation,
|
internode_operation,
|
||||||
http_version_metric_label(resp.version()),
|
http_version_metric_label(resp.version()),
|
||||||
);
|
);
|
||||||
|
maybe_warn_h2_inert(resp.version());
|
||||||
// http_log!("[HttpWriter::spawn] got response: status={}", resp.status());
|
// http_log!("[HttpWriter::spawn] got response: status={}", resp.status());
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
record_internode_error(track_internode_metrics, internode_operation);
|
record_internode_error(track_internode_metrics, internode_operation);
|
||||||
@@ -1162,6 +1193,36 @@ fn record_internode_http_version(track: bool, operation: Option<&'static str>, h
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set once the inert-HTTP/2-tuning warning has been emitted, so it fires at
|
||||||
|
/// most once for the process lifetime (backlog#805-C3).
|
||||||
|
static H2_INERT_WARNED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Pure predicate deciding whether to warn that configured HTTP/2 tuning is
|
||||||
|
/// inert. Warn only when the negotiated protocol is not HTTP/2, the operator
|
||||||
|
/// explicitly configured h2 tuning, and we have not warned before. HTTP/2 for
|
||||||
|
/// internode is negotiated via TLS-ALPN; over plaintext the connection is
|
||||||
|
/// HTTP/1.1 and the h2 window/keepalive knobs are silently ignored.
|
||||||
|
fn should_warn_h2_inert(negotiated_is_http2: bool, h2_tuning_explicit: bool, already_warned: bool) -> bool {
|
||||||
|
!negotiated_is_http2 && h2_tuning_explicit && !already_warned
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit a single process-lifetime warning if explicitly-configured HTTP/2
|
||||||
|
/// tuning is inert because the observed internode connection negotiated a
|
||||||
|
/// non-HTTP/2 protocol (i.e. HTTP/1.1 over plaintext). See backlog#805-C3.
|
||||||
|
fn maybe_warn_h2_inert(negotiated_version: Version) {
|
||||||
|
let negotiated_is_http2 = negotiated_version == Version::HTTP_2;
|
||||||
|
let tuning = internode_http_client_tuning();
|
||||||
|
if should_warn_h2_inert(negotiated_is_http2, tuning.h2_tuning_explicit, H2_INERT_WARNED.load(Ordering::Relaxed))
|
||||||
|
&& H2_INERT_WARNED
|
||||||
|
.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"internode connection negotiated HTTP/1.1 while HTTP/2 tuning is configured; the HTTP/2 window/keepalive settings apply only over internode TLS (h2 is negotiated via ALPN) — enable internode TLS to use them."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn record_internode_stall_timeout(track: bool, operation: Option<&'static str>) {
|
fn record_internode_stall_timeout(track: bool, operation: Option<&'static str>) {
|
||||||
if !track {
|
if !track {
|
||||||
return;
|
return;
|
||||||
@@ -2048,4 +2109,59 @@ mod tests {
|
|||||||
assert!(!should_disable_proxy_for_url("http://127.0.0.1:9000/stream", system_proxy));
|
assert!(!should_disable_proxy_for_url("http://127.0.0.1:9000/stream", system_proxy));
|
||||||
assert!(should_disable_proxy_for_url("http://example.com/stream", no_proxy));
|
assert!(should_disable_proxy_for_url("http://example.com/stream", no_proxy));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// backlog#805-C3: warn once when configured HTTP/2 tuning is inert over
|
||||||
|
// plaintext internode (HTTP/1.1) transport.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_warn_h2_inert_truth_table() {
|
||||||
|
// HTTP/1.1 + explicit tuning + not yet warned -> warn.
|
||||||
|
assert!(should_warn_h2_inert(false, true, false));
|
||||||
|
// Negotiated HTTP/2 -> tuning is actually active, never warn.
|
||||||
|
assert!(!should_warn_h2_inert(true, true, false));
|
||||||
|
// No explicit tuning -> nothing to warn about.
|
||||||
|
assert!(!should_warn_h2_inert(false, false, false));
|
||||||
|
// Already warned -> never warn again (Once semantics).
|
||||||
|
assert!(!should_warn_h2_inert(false, true, true));
|
||||||
|
// HTTP/2 and already warned combinations stay false too.
|
||||||
|
assert!(!should_warn_h2_inert(true, false, false));
|
||||||
|
assert!(!should_warn_h2_inert(true, true, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn h2_tuning_explicit_reflects_windows_and_profile() {
|
||||||
|
// Legacy profile with no explicit windows -> not explicit.
|
||||||
|
let legacy =
|
||||||
|
InternodeHttpClientTuning::from_values(InternodeHttpTuningProfile::Legacy, None, None, None, None, false, None);
|
||||||
|
assert!(!legacy.h2_tuning_explicit);
|
||||||
|
|
||||||
|
// Explicit stream window on the Legacy profile -> explicit.
|
||||||
|
let explicit_stream = InternodeHttpClientTuning::from_values(
|
||||||
|
InternodeHttpTuningProfile::Legacy,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(1024 * 1024),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert!(explicit_stream.h2_tuning_explicit);
|
||||||
|
|
||||||
|
// Explicit connection window on the Legacy profile -> explicit.
|
||||||
|
let explicit_conn = InternodeHttpClientTuning::from_values(
|
||||||
|
InternodeHttpTuningProfile::Legacy,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(4 * 1024 * 1024),
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert!(explicit_conn.h2_tuning_explicit);
|
||||||
|
|
||||||
|
// A non-default (Balanced) profile implies explicit h2 tuning.
|
||||||
|
let balanced =
|
||||||
|
InternodeHttpClientTuning::from_values(InternodeHttpTuningProfile::Balanced, None, None, None, None, false, None);
|
||||||
|
assert!(balanced.h2_tuning_explicit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user