mirror of
https://github.com/telemt/telemt.git
synced 2026-09-14 06:24:09 +03:00
Bounded Debugging + Websocket Carriers + Carriers Negotiation
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -70,6 +70,10 @@ fn test_runtime_with_dc(
|
||||
user: "default".to_string(),
|
||||
secret_mode: WebSecretMode::Plain,
|
||||
carrier,
|
||||
carrier_negotiation_enabled: false,
|
||||
carrier_learning: true,
|
||||
carriers: Arc::from([carrier]),
|
||||
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
|
||||
capability: [7; 32],
|
||||
key_fingerprint: "0000000000000000".to_string(),
|
||||
max_sessions: 4,
|
||||
@@ -120,6 +124,10 @@ fn test_runtime_with_dc(
|
||||
1,
|
||||
profile,
|
||||
[7; 32],
|
||||
carrier,
|
||||
1,
|
||||
[9; 32],
|
||||
None,
|
||||
limits,
|
||||
timeouts,
|
||||
);
|
||||
@@ -323,10 +331,11 @@ async fn silent_streams_do_not_consume_active_handshake_capacity() {
|
||||
assert_eq!(runtime.process_frame(2, 4, FrameType::Data, &[2]), Ok(4));
|
||||
settle_tasks().await;
|
||||
|
||||
let state = runtime.session.state.lock();
|
||||
assert!(state.streams.contains_key(&1));
|
||||
assert!(!state.streams.contains_key(&2));
|
||||
drop(state);
|
||||
{
|
||||
let state = runtime.session.state.lock();
|
||||
assert!(state.streams.contains_key(&1));
|
||||
assert!(!state.streams.contains_key(&2));
|
||||
}
|
||||
runtime.shutdown().await;
|
||||
}
|
||||
|
||||
|
||||
@@ -426,6 +426,10 @@ mod tests {
|
||||
user: "alice".to_string(),
|
||||
secret_mode: WebSecretMode::Plain,
|
||||
carrier: WebCarrier::Https,
|
||||
carrier_negotiation_enabled: false,
|
||||
carrier_learning: true,
|
||||
carriers: Arc::from([WebCarrier::Https]),
|
||||
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
|
||||
capability: [0; 32],
|
||||
key_fingerprint: "0000000000000000".to_string(),
|
||||
max_sessions: 1,
|
||||
@@ -439,6 +443,10 @@ mod tests {
|
||||
1,
|
||||
profile,
|
||||
[2; 32],
|
||||
WebCarrier::Https,
|
||||
1,
|
||||
[3; 32],
|
||||
None,
|
||||
WebLimitsConfig::default(),
|
||||
WebTimeoutsConfig::default(),
|
||||
)
|
||||
|
||||
@@ -41,12 +41,17 @@ impl WebSession {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let digest: TokenHash = Sha256::digest(body).into();
|
||||
let progress = frames
|
||||
.iter()
|
||||
.any(|frame| matches!(frame.frame_type, FrameType::Open | FrameType::Data));
|
||||
let mut opened = Vec::new();
|
||||
let mut committed = false;
|
||||
let result = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
self.ensure_carrier_active_locked(&state)?;
|
||||
state.last_activity = Instant::now();
|
||||
if !state.carrier_lanes.contains_key(&lane_id) {
|
||||
if lane_id != 0
|
||||
@@ -125,6 +130,9 @@ impl WebSession {
|
||||
lane.last_up_digest = digest;
|
||||
}
|
||||
}
|
||||
if applied {
|
||||
committed = self.commit_carrier_locked(&mut state, progress);
|
||||
}
|
||||
applied.then_some(sequence).ok_or(ManagerError::Closed)
|
||||
};
|
||||
if matches!(result, Err(ManagerError::Backpressure)) {
|
||||
@@ -137,6 +145,9 @@ impl WebSession {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if committed {
|
||||
self.finish_carrier_commit();
|
||||
}
|
||||
for (stream_id, peer_port) in opened {
|
||||
self.spawn_stream(stream_id, peer_port, false);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ fn session_with_limits(limits: WebLimitsConfig) -> Arc<WebSession> {
|
||||
user: "alice".to_string(),
|
||||
secret_mode: WebSecretMode::Plain,
|
||||
carrier: WebCarrier::HttpsLanes,
|
||||
carrier_negotiation_enabled: false,
|
||||
carrier_learning: true,
|
||||
carriers: Arc::from([WebCarrier::HttpsLanes]),
|
||||
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
|
||||
capability: [0; 32],
|
||||
key_fingerprint: "0000000000000000".to_string(),
|
||||
max_sessions: 1,
|
||||
@@ -27,6 +31,10 @@ fn session_with_limits(limits: WebLimitsConfig) -> Arc<WebSession> {
|
||||
1,
|
||||
profile,
|
||||
[2; 32],
|
||||
WebCarrier::HttpsLanes,
|
||||
1,
|
||||
[3; 32],
|
||||
None,
|
||||
limits,
|
||||
WebTimeoutsConfig::default(),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::{SessionNegotiationPhase, WebSession};
|
||||
|
||||
struct ReleasedQueues {
|
||||
data_bytes: usize,
|
||||
data_items: usize,
|
||||
control_bytes: usize,
|
||||
control_items: usize,
|
||||
}
|
||||
|
||||
impl WebSession {
|
||||
/// Closes carrier state while relay tasks retain their admission until exit.
|
||||
pub(crate) fn close(&self) {
|
||||
let Some(released) = self.begin_close(false) else {
|
||||
return;
|
||||
};
|
||||
self.finish_close(released, false);
|
||||
}
|
||||
|
||||
/// Atomically prevents first-frame commit while one successor is prepared.
|
||||
pub(crate) fn begin_carrier_supersede(&self) -> bool {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed || state.close_requested {
|
||||
return false;
|
||||
}
|
||||
match state.negotiation_phase {
|
||||
SessionNegotiationPhase::Uncommitted => {
|
||||
state.negotiation_phase = SessionNegotiationPhase::Replacing;
|
||||
true
|
||||
}
|
||||
SessionNegotiationPhase::Replacing
|
||||
| SessionNegotiationPhase::Committed
|
||||
| SessionNegotiationPhase::Superseded => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Restores an uncommitted attempt after successor admission failed.
|
||||
pub(crate) fn cancel_carrier_supersede(&self) {
|
||||
let close_requested = {
|
||||
let mut state = self.state.lock();
|
||||
if !state.closed && state.negotiation_phase == SessionNegotiationPhase::Replacing {
|
||||
state.negotiation_phase = SessionNegotiationPhase::Uncommitted;
|
||||
}
|
||||
state.close_requested
|
||||
};
|
||||
if close_requested {
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes manager-owned replacement without unregistering the old session twice.
|
||||
pub(crate) fn finish_carrier_supersede(&self) {
|
||||
let Some(released) = self.begin_close(true) else {
|
||||
return;
|
||||
};
|
||||
self.finish_close(released, true);
|
||||
}
|
||||
|
||||
/// Waits for all logical-stream tasks after admission has closed.
|
||||
pub(crate) async fn wait(&self) {
|
||||
loop {
|
||||
let notified = self.tasks_done.notified();
|
||||
if self.tasks_live.load(Ordering::Acquire) == 0 {
|
||||
return;
|
||||
}
|
||||
notified.await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether reconnect grace elapsed without activity.
|
||||
pub(crate) fn is_idle(&self, now: Instant) -> bool {
|
||||
let state = self.state.lock();
|
||||
!state.closed
|
||||
&& state.negotiation_phase != SessionNegotiationPhase::Replacing
|
||||
&& now.saturating_duration_since(state.last_activity)
|
||||
>= Duration::from_secs(self.timeouts.reconnect_grace_secs)
|
||||
}
|
||||
|
||||
fn begin_close(&self, superseded: bool) -> Option<ReleasedQueues> {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed || (superseded && state.negotiation_phase != SessionNegotiationPhase::Replacing) {
|
||||
return None;
|
||||
}
|
||||
if !superseded && state.negotiation_phase == SessionNegotiationPhase::Replacing {
|
||||
state.close_requested = true;
|
||||
return None;
|
||||
}
|
||||
state.closed = true;
|
||||
if superseded {
|
||||
state.negotiation_phase = SessionNegotiationPhase::Superseded;
|
||||
}
|
||||
for stream in state.streams.values_mut() {
|
||||
if let Some(waker) = stream.read_waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
if let Some(waker) = stream.write_waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
state.streams.clear();
|
||||
state.pending_frames.clear();
|
||||
state.pending_windows.clear();
|
||||
state.unacked = None;
|
||||
for lane in state.carrier_lanes.values() {
|
||||
lane.notify.notify_waiters();
|
||||
}
|
||||
state.carrier_lanes.clear();
|
||||
let control_bytes = state.pending_control_bytes;
|
||||
let control_items = state.pending_control_items;
|
||||
let data_bytes = state.pending_bytes.saturating_sub(control_bytes);
|
||||
let data_items = state.pending_items.saturating_sub(control_items);
|
||||
state.pending_bytes = 0;
|
||||
state.pending_items = 0;
|
||||
state.pending_control_bytes = 0;
|
||||
state.pending_control_items = 0;
|
||||
Some(ReleasedQueues {
|
||||
data_bytes,
|
||||
data_items,
|
||||
control_bytes,
|
||||
control_items,
|
||||
})
|
||||
}
|
||||
|
||||
fn finish_close(&self, released: ReleasedQueues, superseded: bool) {
|
||||
self.cancel.cancel();
|
||||
if self.carrier().is_multiplexed() {
|
||||
self.down_notify.notify_waiters();
|
||||
}
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
manager.release_pending(
|
||||
self.profile_key,
|
||||
released.data_bytes,
|
||||
released.data_items,
|
||||
false,
|
||||
);
|
||||
manager.release_pending(
|
||||
self.profile_key,
|
||||
released.control_bytes,
|
||||
released.control_items,
|
||||
true,
|
||||
);
|
||||
if !self.finished.swap(true, Ordering::AcqRel) {
|
||||
self.trace_lifecycle(
|
||||
crate::web::trace::TraceLifecycleEvent::SessionClosed,
|
||||
None,
|
||||
Some(if superseded { "superseded" } else { "closed" }),
|
||||
);
|
||||
if !superseded {
|
||||
manager.session_finished(
|
||||
self.token_hash,
|
||||
self.client_ip,
|
||||
self.profile_key,
|
||||
&self.profile.host,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,12 +48,17 @@ impl WebSession {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let digest: TokenHash = Sha256::digest(body).into();
|
||||
let progress = frames
|
||||
.iter()
|
||||
.any(|frame| matches!(frame.frame_type, FrameType::Open | FrameType::Data));
|
||||
let mut opened = Vec::new();
|
||||
let mut committed = false;
|
||||
let result = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
self.ensure_carrier_active_locked(&state)?;
|
||||
state.last_activity = Instant::now();
|
||||
if sequence == state.last_up_sequence && sequence != 0 {
|
||||
return if bool::from(state.last_up_digest.ct_eq(&digest)) {
|
||||
@@ -99,6 +104,7 @@ impl WebSession {
|
||||
} else {
|
||||
state.last_up_sequence = sequence;
|
||||
state.last_up_digest = digest;
|
||||
committed = self.commit_carrier_locked(&mut state, progress);
|
||||
Ok(sequence)
|
||||
}
|
||||
};
|
||||
@@ -112,6 +118,9 @@ impl WebSession {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if committed {
|
||||
self.finish_carrier_commit();
|
||||
}
|
||||
for (stream_id, peer_port) in opened {
|
||||
self.spawn_stream(stream_id, peer_port, false);
|
||||
}
|
||||
@@ -362,6 +371,10 @@ mod tests {
|
||||
user: "alice".to_string(),
|
||||
secret_mode: WebSecretMode::Plain,
|
||||
carrier: WebCarrier::Https,
|
||||
carrier_negotiation_enabled: false,
|
||||
carrier_learning: true,
|
||||
carriers: Arc::from([WebCarrier::Https]),
|
||||
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
|
||||
capability: [0; 32],
|
||||
key_fingerprint: "0000000000000000".to_string(),
|
||||
max_sessions: 1,
|
||||
@@ -375,6 +388,10 @@ mod tests {
|
||||
1,
|
||||
profile,
|
||||
[2; 32],
|
||||
WebCarrier::Https,
|
||||
1,
|
||||
[3; 32],
|
||||
None,
|
||||
WebLimitsConfig::default(),
|
||||
WebTimeoutsConfig::default(),
|
||||
)
|
||||
|
||||
@@ -121,12 +121,17 @@ impl WebSession {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let digest = Sha256::digest(body).into();
|
||||
let progress = frames
|
||||
.iter()
|
||||
.any(|frame| matches!(frame.frame_type, frame::FrameType::Open | frame::FrameType::Data));
|
||||
let mut opened = Vec::new();
|
||||
let mut committed = false;
|
||||
let result = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
self.ensure_carrier_active_locked(&state)?;
|
||||
if !reservation.transferred
|
||||
&& state.websocket_lane_reservations.get(&lane_id) != Some(&reservation.peer_port)
|
||||
{
|
||||
@@ -181,9 +186,15 @@ impl WebSession {
|
||||
}
|
||||
}
|
||||
state.last_activity = Instant::now();
|
||||
if applied {
|
||||
committed = self.commit_carrier_locked(&mut state, progress);
|
||||
}
|
||||
applied.then_some(()).ok_or(ManagerError::Protocol)
|
||||
};
|
||||
result?;
|
||||
if committed {
|
||||
self.finish_carrier_commit();
|
||||
}
|
||||
for (stream_id, peer_port) in opened {
|
||||
if stream_id != lane_id || peer_port != reservation.peer_port {
|
||||
return Err(ManagerError::Protocol);
|
||||
|
||||
@@ -33,6 +33,10 @@ fn runtime(admission: bool) -> TestRuntime {
|
||||
user: "default".to_string(),
|
||||
secret_mode: WebSecretMode::Plain,
|
||||
carrier: WebCarrier::WebsocketLanes,
|
||||
carrier_negotiation_enabled: false,
|
||||
carrier_learning: true,
|
||||
carriers: Arc::from([WebCarrier::WebsocketLanes]),
|
||||
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
|
||||
capability: [7; 32],
|
||||
key_fingerprint: "0000000000000000".to_string(),
|
||||
max_sessions: 2,
|
||||
@@ -60,6 +64,10 @@ fn runtime(admission: bool) -> TestRuntime {
|
||||
1,
|
||||
profile,
|
||||
[7; 32],
|
||||
WebCarrier::WebsocketLanes,
|
||||
1,
|
||||
[9; 32],
|
||||
None,
|
||||
limits,
|
||||
timeouts,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user