WEB: Lifecycle + Lane ownership + Diag fixes

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-27 09:02:19 +03:00
parent c75cf5cc9d
commit f73f52a033
25 changed files with 2050 additions and 245 deletions
+56 -1
View File
@@ -284,7 +284,7 @@ impl ListenerSlot {
}
pub(super) async fn stop(&mut self) -> Result<(), String> {
self.cancellation.cancel();
self.request_stop();
if let Some(task) = self.task.take() {
task.await.map_err(|error_value| {
format!("listener {} task failed: {error_value}", self.spec.addr)
@@ -305,6 +305,61 @@ impl ListenerSlot {
Ok(())
}
/// Cancels admission synchronously before the shared shutdown deadline starts draining.
pub(super) fn request_stop(&self) {
self.cancellation.cancel();
}
/// Joins this acceptor and its WEB connections by one process shutdown deadline.
pub(super) async fn stop_until(
&mut self,
deadline: tokio::time::Instant,
) -> Result<(), String> {
self.request_stop();
let mut errors = Vec::new();
if let Some(mut task) = self.task.take() {
let joined = if task.is_finished() {
Some(task.await)
} else {
match tokio::time::timeout_at(deadline, &mut task).await {
Ok(result) => Some(result),
Err(_) => {
task.abort();
let _ = task.await;
None
}
}
};
match joined {
Some(Ok(())) => {}
Some(Err(error_value)) => errors.push(format!(
"listener {} task failed: {error_value}",
self.spec.addr
)),
None => errors.push(format!(
"listener {} accept shutdown timed out",
self.spec.addr
)),
}
}
self.connections.close();
if !self.connections.is_empty()
&& tokio::time::timeout_at(deadline, self.connections.wait())
.await
.is_err()
{
errors.push(format!(
"listener {} connection shutdown timed out",
self.spec.addr
));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors.join("; "))
}
}
pub(super) fn restart(&mut self, active_runtime: Arc<ArcSwap<RuntimeGeneration>>) {
self.active_runtime = active_runtime.clone();
self.cancellation = CancellationToken::new();
+61 -8
View File
@@ -1,6 +1,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use arc_swap::ArcSwap;
@@ -13,7 +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::manager::WebProcessRuntime;
use crate::web::manager::{WebProcessRuntime, WebShutdownOutcome};
use crate::web::trace::WebTraceStore;
/// Process-owned listener inventory and accept-task lifecycle controller.
@@ -214,24 +215,76 @@ impl ListenerManager {
);
}
/// Stops and joins every accept task before sockets are released.
/// Stops every accept task and applies one deadline to the complete WEB ingress.
pub(crate) async fn shutdown(&mut self) -> Result<(), String> {
if self.web_runtime.is_none() {
let mut errors = Vec::new();
for slot in self.slots.values_mut() {
if let Err(error_value) = slot.stop().await {
errors.push(error_value);
}
}
#[cfg(unix)]
if let Some(unix) = &mut self.unix
&& let Err(error_value) = unix.stop().await
{
errors.push(error_value);
}
self.slots.clear();
#[cfg(unix)]
{
self.unix = None;
}
return if errors.is_empty() {
Ok(())
} else {
Err(errors.join("; "))
};
}
let timeout_secs = self
.active_runtime
.load()
.config()
.web
.timeouts
.shutdown_secs;
let now = tokio::time::Instant::now();
let deadline = now
.checked_add(Duration::from_secs(timeout_secs))
.unwrap_or(now);
for slot in self.slots.values() {
slot.request_stop();
}
#[cfg(unix)]
if let Some(unix) = &self.unix {
unix.request_stop();
}
let Some(web_runtime) = self.web_runtime.take() else {
return Err("WEB runtime disappeared during shutdown orchestration".to_string());
};
let drain = web_runtime.begin_shutdown();
let mut errors = Vec::new();
for slot in self.slots.values_mut() {
if let Err(error_value) = slot.stop().await {
let slot_waits = futures_util::future::join_all(
self.slots
.values_mut()
.map(|slot| slot.stop_until(deadline)),
);
let (slot_results, web_outcome) = tokio::join!(slot_waits, drain.wait_until(deadline));
for result in slot_results {
if let Err(error_value) = result {
errors.push(error_value);
}
}
#[cfg(unix)]
if let Some(unix) = &mut self.unix
&& let Err(error_value) = unix.stop().await
&& let Err(error_value) = unix.stop_until(deadline).await
{
errors.push(error_value);
}
self.slots.clear();
if let Some(web_runtime) = self.web_runtime.take() {
web_runtime.shutdown().await;
if web_outcome == WebShutdownOutcome::DeadlineExceeded {
errors.push("WEB ingress shutdown deadline exceeded".to_string());
}
self.slots.clear();
#[cfg(unix)]
{
self.unix = None;
+32 -1
View File
@@ -148,11 +148,42 @@ impl UnixAcceptHandle {
}
pub(super) async fn stop(&mut self) -> Result<(), String> {
self.cancellation.cancel();
self.request_stop();
if let Some(task) = self.task.take() {
task.await
.map_err(|error_value| format!("Unix listener task failed: {error_value}"))?;
}
Ok(())
}
/// Cancels Unix admission before process-owned listener waits begin.
pub(super) fn request_stop(&self) {
self.cancellation.cancel();
}
/// Joins the Unix acceptor by the process listener deadline.
pub(super) async fn stop_until(
&mut self,
deadline: tokio::time::Instant,
) -> Result<(), String> {
self.request_stop();
let Some(mut task) = self.task.take() else {
return Ok(());
};
if task.is_finished() {
return task
.await
.map_err(|error_value| format!("Unix listener task failed: {error_value}"));
}
match tokio::time::timeout_at(deadline, &mut task).await {
Ok(result) => {
result.map_err(|error_value| format!("Unix listener task failed: {error_value}"))
}
Err(_) => {
task.abort();
let _ = task.await;
Err("Unix listener shutdown timed out".to_string())
}
}
}
}