WEB Knobs in API

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-27 21:42:14 +03:00
parent d41a8c3220
commit cf0cd08387
43 changed files with 3399 additions and 196 deletions
+55 -4
View File
@@ -14,6 +14,7 @@ use super::bind::{BoundListeners, BoundTcpListener, PreparedTcpListener, prepare
use super::plan::{ListenerBindSpec, listener_bind_plan};
#[cfg(unix)]
use super::unix::UnixAcceptHandle;
use crate::web::control::{WebRuntimeControl, WebRuntimeLifecycle};
use crate::web::manager::{WebProcessRuntime, WebShutdownOutcome};
use crate::web::trace::WebTraceStore;
@@ -22,6 +23,8 @@ pub(crate) struct ListenerManager {
active_runtime: Arc<ArcSwap<RuntimeGeneration>>,
slots: BTreeMap<SocketAddr, ListenerSlot>,
web_runtime: Option<Arc<WebProcessRuntime>>,
web_control: WebRuntimeControl,
web_listeners: Arc<[SocketAddr]>,
#[cfg(unix)]
unix: Option<UnixAcceptHandle>,
}
@@ -46,11 +49,15 @@ impl ListenerManager {
bound: BoundListeners,
active_runtime: Arc<ArcSwap<RuntimeGeneration>>,
trace: Arc<WebTraceStore>,
web_control: WebRuntimeControl,
) -> Self {
let has_web = bound
let web_listeners: Arc<[SocketAddr]> = bound
.listeners
.iter()
.any(|listener| listener.spec.transport == ListenerTransport::Web);
.filter(|listener| listener.spec.transport == ListenerTransport::Web)
.map(|listener| listener.spec.addr)
.collect();
let has_web = !web_listeners.is_empty();
let web_runtime =
has_web.then(|| WebProcessRuntime::start_with_trace(active_runtime.clone(), trace));
let mut slots = BTreeMap::new();
@@ -65,10 +72,23 @@ impl ListenerManager {
let unix = bound
.unix_listener
.map(|listener| UnixAcceptHandle::start(listener, active_runtime.clone()));
web_control.publish(
if has_web {
WebRuntimeLifecycle::Running
} else {
WebRuntimeLifecycle::NoWebListener
},
Arc::clone(&web_listeners),
web_runtime
.as_ref()
.map_or_else(std::sync::Weak::new, Arc::downgrade),
);
Self {
active_runtime,
slots,
web_runtime,
web_control,
web_listeners,
#[cfg(unix)]
unix,
}
@@ -76,10 +96,18 @@ impl ListenerManager {
#[cfg(test)]
pub(crate) fn empty(active_runtime: Arc<ArcSwap<RuntimeGeneration>>) -> Self {
let web_control = WebRuntimeControl::new();
web_control.publish(
WebRuntimeLifecycle::NoWebListener,
Arc::from([]),
std::sync::Weak::new(),
);
Self {
active_runtime,
slots: BTreeMap::new(),
web_runtime: None,
web_control,
web_listeners: Arc::from([]),
#[cfg(unix)]
unix: None,
}
@@ -217,6 +245,13 @@ impl ListenerManager {
/// Stops every accept task and applies one deadline to the complete WEB ingress.
pub(crate) async fn shutdown(&mut self) -> Result<(), String> {
self.web_control.publish(
WebRuntimeLifecycle::Draining,
Arc::clone(&self.web_listeners),
self.web_runtime
.as_ref()
.map_or_else(std::sync::Weak::new, Arc::downgrade),
);
if self.web_runtime.is_none() {
let mut errors = Vec::new();
for slot in self.slots.values_mut() {
@@ -235,6 +270,11 @@ impl ListenerManager {
{
self.unix = None;
}
self.web_control.publish(
WebRuntimeLifecycle::Drained,
Arc::clone(&self.web_listeners),
std::sync::Weak::new(),
);
return if errors.is_empty() {
Ok(())
} else {
@@ -289,6 +329,15 @@ impl ListenerManager {
{
self.unix = None;
}
self.web_control.publish(
if web_outcome == WebShutdownOutcome::DeadlineExceeded {
WebRuntimeLifecycle::DeadlineExceeded
} else {
WebRuntimeLifecycle::Drained
},
Arc::clone(&self.web_listeners),
std::sync::Weak::new(),
);
if errors.is_empty() {
Ok(())
} else {
@@ -367,7 +416,8 @@ mod tests {
runtime.config().web.debug.clone(),
&runtime.config().web.limits,
);
let mut manager = ListenerManager::start(bound, active_runtime, trace);
let mut manager =
ListenerManager::start(bound, active_runtime, trace, WebRuntimeControl::new());
let blocker = TcpListener::bind("127.0.0.1:0").await.unwrap();
let blocked_addr = blocker.local_addr().unwrap();
let mut desired = ProxyConfig::default();
@@ -394,7 +444,8 @@ mod tests {
runtime.config().web.debug.clone(),
&runtime.config().web.limits,
);
let mut manager = ListenerManager::start(bound, active_runtime, trace);
let mut manager =
ListenerManager::start(bound, active_runtime, trace, WebRuntimeControl::new());
let reservation = TcpListener::bind("127.0.0.1:0").await.unwrap();
let new_addr = reservation.local_addr().unwrap();
drop(reservation);
+10 -2
View File
@@ -17,6 +17,7 @@ use crate::stats::{QuotaStore, Stats};
use crate::synlimit_control;
use crate::transport::UpstreamManager;
use crate::transport::middle_proxy::MePool;
use crate::web::control::WebRuntimeControl;
use crate::web::trace::WebTraceStore;
use super::{
@@ -106,6 +107,7 @@ pub(super) async fn run_telemt_core(
config.access.cidr_rate_limits.clone(),
);
let web_trace = WebTraceStore::new(config.web.debug.clone(), &config.web.limits);
let web_runtime_control = WebRuntimeControl::new();
let (detected_ips_tx, detected_ips_rx) = watch::channel((None::<IpAddr>, None::<IpAddr>));
let initial_direct_first = config.general.use_middle_proxy && config.general.me2dc_fallback;
@@ -157,6 +159,7 @@ pub(super) async fn run_telemt_core(
let active_runtime_rx_api = active_runtime_rx.clone();
let runtime_watch_rx_api = runtime_watch_rx.clone();
let web_trace_api = web_trace.clone();
let web_runtime_rx_api = web_runtime_control.subscribe();
tokio::spawn(async move {
api::serve(
listen,
@@ -175,6 +178,7 @@ pub(super) async fn run_telemt_core(
active_runtime_rx_api,
runtime_watch_rx_api,
web_trace_api,
web_runtime_rx_api,
)
.await;
});
@@ -318,8 +322,12 @@ pub(super) async fn run_telemt_core(
active_runtime_tx.send_replace(Some(active_runtime.clone()));
runtime_tasks::mark_runtime_ready(&startup_tracker).await;
let listener_manager =
listeners::ListenerManager::start(bound, active_runtime.clone(), web_trace.clone());
let listener_manager = listeners::ListenerManager::start(
bound,
active_runtime.clone(),
web_trace.clone(),
web_runtime_control,
);
let reload_supervisor = reload_supervisor::ReloadSupervisor::spawn(
active_runtime.clone(),
reload_control,
+9 -3
View File
@@ -278,7 +278,11 @@ impl ReloadSupervisor {
self.control
.mark_phase(command.reload_id, ReloadPhase::Activating)
.await;
let new_runtime = prepared.generation;
let PreparedRuntime {
generation: new_runtime,
detected_ips,
config_watcher_activation,
} = prepared;
if let Err(error) = install_dns(&new_runtime.config().network.dns_overrides) {
let message = format!("runtime DNS activation failed: {}", error);
if command.request.failure_policy == ReloadFailurePolicy::Rollback {
@@ -313,14 +317,16 @@ impl ReloadSupervisor {
};
old_runtime.stop_accepting_sessions();
let replaced = self.active_runtime.swap(new_runtime.clone());
self.web_trace.apply_policy(&new_runtime.config().web.debug);
self.web_trace
.apply_policy(new_runtime.id, &new_runtime.config().web.debug);
config_watcher_activation.send_replace(true);
if let Some(pending) = pending_listener_transition {
self.listener_manager
.lock()
.await
.finish_transition(pending);
}
self.detected_ips_tx.send_replace(prepared.detected_ips);
self.detected_ips_tx.send_replace(detected_ips);
self.runtime_log_filter
.apply_reload(&new_runtime.config().general.log_level);
self.runtime_watch_tx
+13 -16
View File
@@ -21,6 +21,15 @@ fn runtime_log_filter() -> RuntimeLogFilter {
RuntimeLogFilter::new(handle)
}
fn prepared_runtime(generation: Arc<RuntimeGeneration>) -> PreparedRuntime {
let (config_watcher_activation, _activation_rx) = watch::channel(false);
PreparedRuntime {
generation,
detected_ips: (None, None),
config_watcher_activation,
}
}
async fn fixture(request: ReloadRequest) -> ReloadFixture {
let old_runtime = test_runtime_generation(1, ProxyConfig::default());
let new_config = Arc::new(ProxyConfig::default());
@@ -120,10 +129,7 @@ async fn revision_rollback_keeps_old_generation_and_cleans_candidate() {
.activate_prepared(
fixture.command,
fixture.old_runtime.clone(),
PreparedRuntime {
generation: fixture.new_runtime,
detected_ips: (None, None),
},
prepared_runtime(fixture.new_runtime),
RevisionGateAction::Rollback("revision changed".to_string()),
|_| -> Result<(), String> { panic!("DNS activation must not run on rollback") },
)
@@ -161,10 +167,7 @@ async fn dns_failure_policy_controls_rollback_or_keep_new() {
.activate_prepared(
fixture.command,
fixture.old_runtime.clone(),
PreparedRuntime {
generation: fixture.new_runtime.clone(),
detected_ips: (None, None),
},
prepared_runtime(fixture.new_runtime.clone()),
RevisionGateAction::Proceed,
|_| Err("invalid DNS entry".to_string()),
)
@@ -215,10 +218,7 @@ async fn drain_publishes_new_generation_before_old_sessions_finish() {
.activate_prepared(
fixture.command,
old_runtime,
PreparedRuntime {
generation: new_runtime,
detected_ips: (None, None),
},
prepared_runtime(new_runtime),
RevisionGateAction::Proceed,
|_| Ok(()),
)
@@ -271,10 +271,7 @@ async fn drain_timeout_cancels_old_sessions_and_records_one_warning() {
.activate_prepared(
fixture.command,
old_runtime,
PreparedRuntime {
generation: new_runtime,
detected_ips: (None, None),
},
prepared_runtime(new_runtime),
RevisionGateAction::Proceed,
|_| Ok(()),
)
+8
View File
@@ -28,9 +28,14 @@ use super::listeners::listener_rebind_supported;
use super::runtime_tasks::RuntimeLogFilter;
use super::{me_startup, runtime_tasks, tls_bootstrap};
/// Fully prepared candidate runtime and its activation-gated config watcher.
pub(crate) struct PreparedRuntime {
/// Candidate generation ready for publication.
pub(crate) generation: Arc<RuntimeGeneration>,
/// Detected public addresses associated with the candidate.
pub(crate) detected_ips: (Option<IpAddr>, Option<IpAddr>),
/// Gate opened only after the candidate becomes the active generation.
pub(crate) config_watcher_activation: watch::Sender<bool>,
}
pub(crate) async fn prepare_runtime(
@@ -171,6 +176,7 @@ pub(crate) async fn prepare_runtime(
config.server.max_connections as usize
};
let max_connections = Arc::new(Semaphore::new(max_connections_limit));
let (config_watcher_activation, config_watcher_activation_rx) = watch::channel(false);
let watches = runtime_tasks::spawn_runtime_tasks(
&config,
config_path,
@@ -190,6 +196,7 @@ pub(crate) async fn prepare_runtime(
proxy_shared.clone(),
me_ready_tx.clone(),
task_scope.clone(),
Some(config_watcher_activation_rx),
)
.await;
let config_rx = watches.config_rx;
@@ -295,6 +302,7 @@ pub(crate) async fn prepare_runtime(
Ok(PreparedRuntime {
generation,
config_watcher_activation,
detected_ips: (
probe.detected_ipv4.map(IpAddr::V4),
probe.detected_ipv6.map(IpAddr::V6),
+1
View File
@@ -247,6 +247,7 @@ pub(super) async fn prepare_runtime(
shared_state.clone(),
me_ready_tx.clone(),
runtime_task_scope.clone(),
None,
)
.await;
let config_rx = runtime_watches.config_rx;
+10 -8
View File
@@ -107,6 +107,7 @@ pub(crate) async fn spawn_runtime_tasks(
shared_state: Arc<ProxySharedState>,
me_ready_tx: watch::Sender<u64>,
task_scope: RuntimeTaskScope,
config_watcher_activation: Option<watch::Receiver<bool>>,
) -> RuntimeWatches {
let um_clone = upstream_manager.clone();
let dc_overrides_for_health = config.dc_overrides.clone();
@@ -151,14 +152,15 @@ pub(crate) async fn spawn_runtime_tasks(
Some("spawn config hot-reload watcher".to_string()),
)
.await;
let (config_rx, log_level_rx): (watch::Receiver<Arc<ProxyConfig>>, watch::Receiver<LogLevel>) =
spawn_config_watcher(
config_path.to_path_buf(),
config.clone(),
detected_ip_v4,
detected_ip_v6,
task_scope.cancellation_token(),
);
let (config_rx, log_level_rx, config_watcher_task) = spawn_config_watcher(
config_path.to_path_buf(),
config.clone(),
detected_ip_v4,
detected_ip_v6,
task_scope.cancellation_token(),
config_watcher_activation,
);
task_scope.spawn(config_watcher_task);
startup_tracker
.complete_component(
COMPONENT_CONFIG_WATCHER_START,