Carrier negotiation + WebSocket lifecycle

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-27 07:38:42 +03:00
parent 80a2737eed
commit c75cf5cc9d
28 changed files with 1411 additions and 1320 deletions
+3
View File
@@ -339,6 +339,9 @@ const WEB_TIMEOUTS_CONFIG_KEYS: &[&str] = &[
"stream_handshake_secs",
"stream_first_byte_secs",
"long_poll_secs",
"bridge_request_secs",
"bridge_retry_secs",
"carrier_probe_coalesce_ms",
"lane_open_wait_secs",
"carrier_health_secs",
"websocket_upgrade_secs",
+12
View File
@@ -33,6 +33,18 @@ pub(super) fn validate(timeouts: &WebTimeoutsConfig) -> Result<()> {
if !(2..=86_400).contains(&timeouts.carrier_learning_secs) {
return config_error("web.timeouts.carrier_learning_secs must be within [2, 86400]");
}
if !(1..=60).contains(&timeouts.bridge_request_secs) {
return config_error("web.timeouts.bridge_request_secs must be within [1, 60]");
}
if !(1..=300).contains(&timeouts.bridge_retry_secs) {
return config_error("web.timeouts.bridge_retry_secs must be within [1, 300]");
}
if timeouts.bridge_request_secs > timeouts.bridge_retry_secs {
return config_error("web.timeouts.bridge_request_secs must not exceed bridge_retry_secs");
}
if timeouts.carrier_probe_coalesce_ms > 10 {
return config_error("web.timeouts.carrier_probe_coalesce_ms must be within [0, 10]");
}
if timeouts.stream_first_byte_secs > 300 {
return config_error("web.timeouts.stream_first_byte_secs must be within [1, 300]");
}
+47 -2
View File
@@ -108,10 +108,10 @@ fn web_carriers_reject_true_empty_and_duplicates() {
}
#[test]
fn web_carrier_deadlines_and_learning_window_are_configurable() {
fn web_carrier_and_bridge_deadlines_are_configurable() {
let configured = WEB_CONFIG.replace(
"[[web.vhosts]]",
"[web.timeouts]\ncarrier_negotiation_deadlines_secs = [1, 2, 4, 9]\ncarrier_learning_secs = 30\n\n[[web.vhosts]]",
"[web.timeouts]\ncarrier_negotiation_deadlines_secs = [1, 2, 4, 9]\ncarrier_learning_secs = 30\nbridge_request_secs = 7\nbridge_retry_secs = 41\ncarrier_probe_coalesce_ms = 4\n\n[[web.vhosts]]",
);
let config = load_config_from_temp_toml(&configured);
assert_eq!(
@@ -119,6 +119,51 @@ fn web_carrier_deadlines_and_learning_window_are_configurable() {
[1, 2, 4, 9]
);
assert_eq!(config.web.timeouts.carrier_learning_secs, 30);
assert_eq!(config.web.timeouts.bridge_request_secs, 7);
assert_eq!(config.web.timeouts.bridge_retry_secs, 41);
assert_eq!(config.web.timeouts.carrier_probe_coalesce_ms, 4);
}
#[test]
fn web_bridge_deadlines_are_known_in_strict_mode() {
let configured = WEB_CONFIG.replace(
"[[web.vhosts]]",
"[web.timeouts]\nbridge_request_secs = 7\nbridge_retry_secs = 41\ncarrier_probe_coalesce_ms = 4\n\n[[web.vhosts]]",
);
let configured = format!("[general]\nconfig_strict = true\n{configured}");
let config = load_config_from_temp_toml(&configured);
assert_eq!(config.web.timeouts.bridge_request_secs, 7);
assert_eq!(config.web.timeouts.bridge_retry_secs, 41);
assert_eq!(config.web.timeouts.carrier_probe_coalesce_ms, 4);
}
#[test]
fn web_bridge_deadlines_are_bounded_and_ordered() {
for (field, value) in [
("bridge_request_secs", "0"),
("bridge_request_secs", "61"),
("bridge_retry_secs", "0"),
("bridge_retry_secs", "301"),
("carrier_probe_coalesce_ms", "11"),
] {
let invalid = WEB_CONFIG.replace(
"[[web.vhosts]]",
&format!("[web.timeouts]\n{field} = {value}\n\n[[web.vhosts]]"),
);
assert!(
load_config_error_from_temp_toml(&invalid).contains(&format!("web.timeouts.{field}"))
);
}
let reversed = WEB_CONFIG.replace(
"[[web.vhosts]]",
"[web.timeouts]\nbridge_request_secs = 20\nbridge_retry_secs = 10\n\n[[web.vhosts]]",
);
assert!(
load_config_error_from_temp_toml(&reversed)
.contains("bridge_request_secs must not exceed bridge_retry_secs")
);
}
#[test]
+12
View File
@@ -288,6 +288,15 @@ pub struct WebTimeoutsConfig {
/// Maximum wait for one empty downlink long poll.
#[serde(default = "default_web_long_poll_timeout_secs")]
pub long_poll_secs: u64,
/// Deadline for one generated-bridge HTTP attempt and response body.
#[serde(default = "default_web_bridge_request_secs")]
pub bridge_request_secs: u64,
/// Absolute generated-bridge budget for one retryable HTTP operation.
#[serde(default = "default_web_bridge_retry_secs")]
pub bridge_retry_secs: u64,
/// Optional delay for coalescing the first OPEN with immediate DATA.
#[serde(default = "default_web_carrier_probe_coalesce_ms")]
pub carrier_probe_coalesce_ms: u64,
/// Grace for a canonical downlink poll that races its lane OPEN.
#[serde(default = "default_web_lane_open_wait_secs")]
pub lane_open_wait_secs: u64,
@@ -340,6 +349,9 @@ impl Default for WebTimeoutsConfig {
stream_handshake_secs: default_web_stream_handshake_timeout_secs(),
stream_first_byte_secs: default_web_stream_first_byte_secs(),
long_poll_secs: default_web_long_poll_timeout_secs(),
bridge_request_secs: default_web_bridge_request_secs(),
bridge_retry_secs: default_web_bridge_retry_secs(),
carrier_probe_coalesce_ms: default_web_carrier_probe_coalesce_ms(),
lane_open_wait_secs: default_web_lane_open_wait_secs(),
carrier_health_secs: default_web_carrier_health_secs(),
websocket_upgrade_secs: default_web_websocket_upgrade_secs(),
+3
View File
@@ -85,6 +85,9 @@ u64_default!(default_web_body_timeout_secs, 30);
u64_default!(default_web_stream_handshake_timeout_secs, 10);
u64_default!(default_web_stream_first_byte_secs, 30);
u64_default!(default_web_long_poll_timeout_secs, 25);
u64_default!(default_web_bridge_request_secs, 10);
u64_default!(default_web_bridge_retry_secs, 90);
u64_default!(default_web_carrier_probe_coalesce_ms, 0);
u64_default!(default_web_lane_open_wait_secs, 2);
u64_default!(default_web_carrier_health_secs, 30);
u64_default!(default_web_websocket_upgrade_secs, 5);