mirror of
https://github.com/telemt/telemt.git
synced 2026-10-29 20:17:16 +03:00
WEB Knobs in API
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,560 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user