mirror of
https://github.com/telemt/telemt.git
synced 2026-10-27 19:17:44 +03:00
cf0cd08387
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
561 lines
19 KiB
Rust
561 lines
19 KiB
Rust
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<u64>,
|
|
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<u64>,
|
|
latest_seq: Option<u64>,
|
|
}
|
|
|
|
/// 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<ManagerStatus>,
|
|
streams: Option<StreamStatus>,
|
|
budget: Option<BudgetStatus>,
|
|
websockets: Option<WebSocketStatus>,
|
|
learning: Option<LearningStatus>,
|
|
debug: Option<DebugStatus>,
|
|
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<u64>,
|
|
/// Exact forwarded client address.
|
|
pub(crate) client_ip: Option<IpAddr>,
|
|
/// Exact canonical virtual host.
|
|
pub(crate) host: Option<String>,
|
|
/// Exact configured user label.
|
|
pub(crate) user: Option<String>,
|
|
/// 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<String>,
|
|
/// Exact current carrier.
|
|
pub(crate) carrier: Option<WebCarrier>,
|
|
/// Exact point-in-time lifecycle token.
|
|
pub(crate) state: Option<String>,
|
|
}
|
|
|
|
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<u64>,
|
|
/// 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<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
user_agent_id: Option<String>,
|
|
#[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<SessionRow>,
|
|
next_cursor: Option<String>,
|
|
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<SessionRow>),
|
|
/// 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<crate::web::session::WebSession>,
|
|
user_agent: Option<Arc<str>>,
|
|
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<u64, SessionRefError> {
|
|
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<tokio::sync::Semaphore>, capacity: usize) -> PermitStatus {
|
|
let available = semaphore.available_permits().min(capacity);
|
|
PermitStatus {
|
|
used: capacity.saturating_sub(available),
|
|
available,
|
|
capacity,
|
|
closed: semaphore.is_closed(),
|
|
}
|
|
}
|