use std::net::IpAddr; use std::ops::Bound::{Excluded, Unbounded}; use std::sync::Arc; use std::sync::atomic::Ordering; use std::time::Instant; use serde::Serialize; use super::WebProcessRuntime; use crate::config::{ WebCarrier, WebCarrierNegotiationAggressiveness, WebDebugConfig, WebLimitsConfig, }; use crate::web::session::WebSessionStatus; const SESSION_REF_VERSION: &str = "ws1"; const MAX_SESSION_SCAN: usize = 1000; /// Current usage of one process-owned semaphore. #[derive(Clone, Serialize)] struct PermitStatus { used: usize, available: usize, capacity: usize, closed: bool, } /// Short-lock manager registry counts. #[derive(Clone, Serialize)] struct ManagerStatus { issuance_enabled: bool, issuance_generation: u64, shutdown: bool, bootstraps: usize, sessions: usize, closed_tokens: usize, closed_sessions: usize, client_ips: usize, profiles: usize, } /// Logical-stream admission counters. #[derive(Clone, Serialize)] struct StreamStatus { live: usize, profiles: usize, closed: bool, } /// Shared queue and WebSocket byte-budget counters. #[derive(Clone, Serialize)] struct BudgetStatus { queue_bytes: usize, queue_items: usize, control_bytes: usize, control_items: usize, websocket_bytes: usize, high_water_bytes: usize, owners: usize, closed: bool, } /// Process WebSocket registry counters. #[derive(Clone, Serialize)] struct WebSocketStatus { entries: usize, claims: usize, evictions_in_flight: usize, closed: bool, } /// Process-local carrier-learning summary. #[derive(Clone, Serialize)] struct LearningStatus { enabled: bool, aggressiveness: WebCarrierNegotiationAggressiveness, epoch: Option, entries: usize, capacity: usize, lifetime_secs: u64, age_ms: u64, } /// Effective trace policy and bounded ring counters. #[derive(Clone, Serialize)] struct DebugStatus { policy: WebDebugConfig, policy_generation: u64, epoch: u64, records: usize, records_capacity: usize, used_bytes: usize, bytes_capacity: usize, contention_drops: u64, evictions: u64, byte_truncations: u64, earliest_seq: Option, latest_seq: Option, } /// One non-blocking multi-plane WEB runtime snapshot. #[derive(Clone, Serialize)] pub(crate) struct WebRuntimeStatus { runtime_instance: String, generation_id: u64, limits: WebLimitsConfig, manager: Option, streams: Option, budget: Option, websockets: Option, learning: Option, debug: Option, permits: Vec<(&'static str, PermitStatus)>, auxiliary_tasks: usize, session_incarnations_created: u64, session_incarnations_closed: u64, streams_opened: u64, streams_rejected: u64, bytes_up: u64, bytes_down: u64, limit_hits: u64, partial: Vec<&'static str>, } /// Strict bounded filters for session enumeration and bulk close. #[derive(Clone, Default)] pub(crate) struct SessionFilter { /// Exact process-local logical session identifier. pub(crate) trace_session_id: Option, /// Exact forwarded client address. pub(crate) client_ip: Option, /// Exact canonical virtual host. pub(crate) host: Option, /// Exact configured user label. pub(crate) user: Option, /// Exact non-secret User-Agent identifier. pub(crate) user_agent_id: Option<[u8; 16]>, /// Exact non-secret profile-key fingerprint. pub(crate) key_id: Option, /// Exact current carrier. pub(crate) carrier: Option, /// Exact point-in-time lifecycle token. pub(crate) state: Option, } impl SessionFilter { /// Returns whether the selector would match every live session. pub(crate) fn is_empty(&self) -> bool { self.trace_session_id.is_none() && self.client_ip.is_none() && self.host.is_none() && self.user.is_none() && self.user_agent_id.is_none() && self.key_id.is_none() && self.carrier.is_none() && self.state.is_none() } } /// Validated bounded list request. pub(crate) struct SessionListRequest { /// Maximum returned rows. pub(crate) limit: usize, /// Exclusive ordered logical-session cursor. pub(crate) cursor: Option, /// Exact bounded filters. pub(crate) filter: SessionFilter, } /// One API-safe session row with optional User-Agent metadata. #[derive(Clone, Serialize)] pub(crate) struct SessionRow { /// Opaque process-fenced logical-session reference. pub(crate) session_ref: String, /// Bounded sanitized User-Agent display value. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) user_agent: Option, #[serde(skip_serializing_if = "Option::is_none")] user_agent_id: Option, #[serde(flatten)] status: WebSessionStatus, } /// Bounded session page and continuation metadata. #[derive(Clone, Serialize)] pub(crate) struct SessionPage { /// Ordered live-session rows captured without blocking. pub(crate) sessions: Vec, next_cursor: Option, scanned: usize, scan_truncated: bool, partial_sessions: usize, partial: Vec<&'static str>, } /// Exact detail lookup outcome. pub(crate) enum SessionDetail { /// One exact live-session snapshot. Active(Box), /// One bounded retained closed-session tombstone. Gone { attempt: u8 }, /// A required short lock was contended. Busy, /// Neither a live session nor a retained tombstone exists. NotFound, } struct Candidate { trace_session_id: u64, session: Arc, user_agent: Option>, user_agent_id: Option<[u8; 16]>, } impl WebProcessRuntime { /// Captures every independent plane without blocking on a contended lock. pub(crate) fn try_status(&self) -> WebRuntimeStatus { let generation_id = self.active_generation().id; let mut partial = Vec::new(); let manager = self.state.try_lock().map(|state| ManagerStatus { issuance_enabled: state.issuance_enabled, issuance_generation: state.issuance_generation, shutdown: state.closed, bootstraps: state.bootstraps.len(), sessions: state.sessions.len(), closed_tokens: state.closed_tokens.len(), closed_sessions: state.closed_sessions.len(), client_ips: state.sessions_per_ip.len(), profiles: state.sessions_per_profile.len(), }); if manager.is_none() { partial.push("manager"); } let streams = self.stream_admission.try_lock().map(|state| StreamStatus { live: state.streams_live, profiles: state.streams_per_profile.len(), closed: state.closed, }); if streams.is_none() { partial.push("streams"); } let budget = self.data_budget.try_snapshot().map(|status| BudgetStatus { queue_bytes: status.queue_bytes, queue_items: status.queue_items, control_bytes: status.queue_control_bytes, control_items: status.queue_control_items, websocket_bytes: status.websocket_bytes, high_water_bytes: status.high_water_bytes, owners: status.owners, closed: status.closed, }); if budget.is_none() { partial.push("budget"); } let websockets = self.websockets.try_lock().map(|registry| { let status = registry.status(); WebSocketStatus { entries: status.entries, claims: status.claims, evictions_in_flight: status.evictions_in_flight, closed: status.closed, } }); if websockets.is_none() { partial.push("websockets"); } let learning = self .try_carrier_learning_status() .map(|status| LearningStatus { enabled: status.enabled, aggressiveness: status.aggressiveness, epoch: status.epoch, entries: status.entries, capacity: status.capacity, lifetime_secs: status.lifetime_secs, age_ms: status.age_ms, }); if learning.is_none() { partial.push("learning"); } let debug = self.trace.try_status().map(|status| DebugStatus { policy: status.policy.as_ref().clone(), policy_generation: status.policy_generation, epoch: status.epoch, records: status.records, records_capacity: status.records_capacity, used_bytes: status.used_bytes, bytes_capacity: status.bytes_capacity, contention_drops: status.contention_drops, evictions: status.evictions, byte_truncations: status.byte_truncations, earliest_seq: status.earliest_seq, latest_seq: status.latest_seq, }); if debug.is_none() { partial.push("debug"); } let websocket_capacity = self .limits .max_http_connections .saturating_sub(self.limits.websocket_http_connection_reserve); WebRuntimeStatus { runtime_instance: self.runtime_instance().to_string(), generation_id, limits: self.limits.clone(), manager, streams, budget, websockets, learning, debug, permits: vec![ ( "http_connections", permits(&self.http_connections, self.limits.max_http_connections), ), ( "http_handlers", permits(&self.http_handlers, self.limits.max_http_handlers), ), ( "lane_polls", permits(&self.lane_polls, self.limits.max_http_handlers / 2), ), ( "lane_aux_polls", permits( &self.lane_aux_polls, (self.limits.max_http_handlers / 4).max(1), ), ), ( "body_readers", permits(&self.body_readers, self.limits.max_body_readers), ), ( "body_bytes", permits(&self.body_bytes, self.limits.max_body_bytes_global), ), ( "stream_handshakes", permits(&self.stream_handshakes, self.limits.max_stream_handshakes), ), ( "websocket_connections", permits(&self.websocket_connections, websocket_capacity), ), ], auxiliary_tasks: self.tasks.len(), session_incarnations_created: self.sessions_created.load(Ordering::Relaxed), session_incarnations_closed: self.sessions_closed.load(Ordering::Relaxed), streams_opened: self.streams_opened.load(Ordering::Relaxed), streams_rejected: self.streams_rejected.load(Ordering::Relaxed), bytes_up: self.bytes_up.load(Ordering::Relaxed), bytes_down: self.bytes_down.load(Ordering::Relaxed), limit_hits: self.limit_hits.load(Ordering::Relaxed), partial, } } /// Formats an opaque process-fenced session reference. pub(crate) fn session_ref(&self, trace_session_id: u64) -> String { format!( "{SESSION_REF_VERSION}.{}.{trace_session_id:016x}", self.runtime_instance() ) } /// Parses an exact reference and distinguishes stale process instances. pub(crate) fn parse_session_ref(&self, value: &str) -> Result { let mut parts = value.split('.'); let version = parts.next(); let instance = parts.next(); let id = parts.next(); if version != Some(SESSION_REF_VERSION) || parts.next().is_some() { return Err(SessionRefError::Invalid); } if !instance.is_some_and(|instance| { instance.len() == 32 && instance .bytes() .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) }) { return Err(SessionRefError::Invalid); } let id = id .filter(|id| { id.len() == 16 && id .bytes() .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) }) .and_then(|id| u64::from_str_radix(id, 16).ok()) .filter(|id| *id != 0) .ok_or(SessionRefError::Invalid)?; if instance != Some(self.runtime_instance()) { return Err(SessionRefError::StaleInstance); } Ok(id) } /// Lists live sessions under an ordered bounded scan. pub(crate) fn list_sessions(&self, request: SessionListRequest) -> SessionPage { let Some(state) = self.state.try_lock() else { return SessionPage { sessions: Vec::new(), next_cursor: request.cursor.map(|id| self.session_ref(id)), scanned: 0, scan_truncated: false, partial_sessions: 0, partial: vec!["manager"], }; }; let mut candidates = Vec::with_capacity(request.limit); let mut scanned = 0usize; let mut last_scanned = request.cursor; for (&trace_session_id, index) in state .session_index .range((request.cursor.map_or(Unbounded, Excluded), Unbounded)) { if scanned >= MAX_SESSION_SCAN || candidates.len() >= request.limit { break; } scanned += 1; last_scanned = Some(trace_session_id); let Some(session) = state.sessions.get(&index.session_hash).cloned() else { continue; }; if !immutable_matches(&session, index, &request.filter) { continue; } candidates.push(Candidate { trace_session_id, session, user_agent: index.user_agent.clone(), user_agent_id: index.user_agent_id, }); } drop(state); let mut rows = Vec::with_capacity(candidates.len()); let mut partial_sessions = 0usize; let now = Instant::now(); for candidate in candidates { let Some(status) = candidate.session.try_status(now) else { partial_sessions += 1; continue; }; if request .filter .state .as_deref() .is_some_and(|expected| expected != status.state) { continue; } rows.push(self.row(candidate, status)); } let scan_truncated = scanned >= MAX_SESSION_SCAN; SessionPage { sessions: rows, next_cursor: (scan_truncated || scanned >= request.limit) .then(|| last_scanned.map(|id| self.session_ref(id))) .flatten(), scanned, scan_truncated, partial_sessions, partial: Vec::new(), } } /// Resolves one active or recently closed logical session. pub(crate) fn session_detail(&self, trace_session_id: u64) -> SessionDetail { let Some(state) = self.state.try_lock() else { return SessionDetail::Busy; }; if let Some(index) = state.session_index.get(&trace_session_id) { let Some(session) = state.sessions.get(&index.session_hash).cloned() else { return SessionDetail::Busy; }; let candidate = Candidate { trace_session_id, session, user_agent: index.user_agent.clone(), user_agent_id: index.user_agent_id, }; drop(state); return candidate .session .try_status(Instant::now()) .map(|status| SessionDetail::Active(Box::new(self.row(candidate, status)))) .unwrap_or(SessionDetail::Busy); } let closed = state .closed_sessions .get(&trace_session_id) .map(|closed| closed.attempt); closed.map_or(SessionDetail::NotFound, |attempt| SessionDetail::Gone { attempt, }) } fn row(&self, candidate: Candidate, status: WebSessionStatus) -> SessionRow { SessionRow { session_ref: self.session_ref(candidate.trace_session_id), user_agent: candidate.user_agent.map(|value| value.to_string()), user_agent_id: candidate.user_agent_id.map(hex::encode), status, } } } /// Opaque session-reference validation failure. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum SessionRefError { /// The reference does not use the canonical versioned shape. Invalid, /// The reference belongs to another process runtime. StaleInstance, } /// Tests immutable candidate fields before any optional state-lock read. pub(super) fn immutable_matches( session: &crate::web::session::WebSession, index: &super::state::LiveSessionIndex, filter: &SessionFilter, ) -> bool { filter .trace_session_id .is_none_or(|value| value == session.trace_session_id()) && filter .client_ip .is_none_or(|value| value == session.client_ip()) && filter .host .as_deref() .is_none_or(|value| value == session.profile_host()) && filter .user .as_deref() .is_none_or(|value| value == session.profile_user()) && filter .key_id .as_deref() .is_none_or(|value| session.key_id() == value) && filter .carrier .is_none_or(|value| value == session.carrier()) && filter .user_agent_id .is_none_or(|value| index.user_agent_id == Some(value)) } fn permits(semaphore: &Arc, capacity: usize) -> PermitStatus { let available = semaphore.available_permits().min(capacity); PermitStatus { used: capacity.saturating_sub(available), available, capacity, closed: semaphore.is_closed(), } }