mirror of
https://github.com/telemt/telemt.git
synced 2026-09-13 05:54:09 +03:00
Bridge Request/Buffers/Recovery
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -1,11 +1,76 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::{SessionState, WebSession};
|
||||
use crate::web::telemetry::WebSessionLifecycleObservation;
|
||||
|
||||
/// Session-local activity clocks with distinct lease and diagnostic authority.
|
||||
pub(super) struct SessionActivity {
|
||||
last_peer: Instant,
|
||||
last_progress: Instant,
|
||||
}
|
||||
|
||||
impl WebSession {
|
||||
/// Refreshes the authenticated peer lease and records only threshold-crossing gaps.
|
||||
pub(super) fn touch_peer_locked(
|
||||
&self,
|
||||
state: &mut SessionState,
|
||||
now: Instant,
|
||||
observation: WebSessionLifecycleObservation,
|
||||
) {
|
||||
let gap = state.activity.touch_peer(now);
|
||||
if gap >= Duration::from_secs(self.timeouts.reconnect_grace_secs)
|
||||
&& let Some(manager) = self.manager.upgrade()
|
||||
{
|
||||
manager
|
||||
.telemetry()
|
||||
.record_session_observation(self.carrier(), observation);
|
||||
}
|
||||
}
|
||||
|
||||
/// Records one valid WebSocket control message as authenticated peer activity.
|
||||
pub(crate) fn record_websocket_peer_activity(&self) -> bool {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed {
|
||||
return false;
|
||||
}
|
||||
self.touch_peer_locked(
|
||||
&mut state,
|
||||
Instant::now(),
|
||||
WebSessionLifecycleObservation::WebSocketActivityAfterGap,
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn progress_does_not_extend_the_authenticated_peer_lease() {
|
||||
let started = Instant::now();
|
||||
let mut activity = SessionActivity::new(started);
|
||||
activity.touch_progress(started + Duration::from_secs(4));
|
||||
|
||||
assert_eq!(
|
||||
activity.peer_idle(started + Duration::from_secs(9)),
|
||||
Duration::from_secs(9)
|
||||
);
|
||||
assert_eq!(
|
||||
activity.progress_idle(started + Duration::from_secs(9)),
|
||||
Duration::from_secs(5)
|
||||
);
|
||||
assert_eq!(
|
||||
activity.touch_peer(started + Duration::from_secs(9)),
|
||||
Duration::from_secs(9)
|
||||
);
|
||||
assert_eq!(
|
||||
activity.peer_idle(started + Duration::from_secs(10)),
|
||||
Duration::from_secs(1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionActivity {
|
||||
/// Starts both activity clocks at the same session creation instant.
|
||||
pub(super) fn new(now: Instant) -> Self {
|
||||
|
||||
@@ -10,6 +10,7 @@ use super::{
|
||||
};
|
||||
use crate::web::frame::{self, FrameType};
|
||||
use crate::web::manager::ManagerError;
|
||||
use crate::web::telemetry::WebSessionLifecycleObservation;
|
||||
|
||||
impl WebSession {
|
||||
/// Polls pending downlink frames with cursor replay and newest-poll-wins semantics.
|
||||
@@ -22,14 +23,19 @@ impl WebSession {
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
state.activity.touch_peer(Instant::now());
|
||||
if let Some(unacked) = &state.unacked {
|
||||
if cursor == unacked.base_cursor {
|
||||
return Ok(PollResult {
|
||||
let result = PollResult {
|
||||
body: unacked.body.clone(),
|
||||
next_cursor: unacked.next_cursor,
|
||||
lane_closed: false,
|
||||
});
|
||||
};
|
||||
self.touch_peer_locked(
|
||||
&mut state,
|
||||
Instant::now(),
|
||||
WebSessionLifecycleObservation::HttpActivityAfterGap,
|
||||
);
|
||||
return Ok(result);
|
||||
}
|
||||
if cursor != unacked.next_cursor {
|
||||
drop(state);
|
||||
@@ -53,6 +59,11 @@ impl WebSession {
|
||||
return Err(ManagerError::Protocol);
|
||||
};
|
||||
state.down_epoch = epoch;
|
||||
self.touch_peer_locked(
|
||||
&mut state,
|
||||
Instant::now(),
|
||||
WebSessionLifecycleObservation::HttpActivityAfterGap,
|
||||
);
|
||||
let healthy = self.carrier_health_ready_locked(&mut state, Instant::now());
|
||||
(state.down_epoch, healthy)
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ use super::{PendingClass, SessionCloseReason, WebSession, insert_carrier_lane};
|
||||
use crate::config::WebCarrier;
|
||||
use crate::web::frame::{self, Frame, FrameType};
|
||||
use crate::web::manager::{ManagerError, TokenHash};
|
||||
use crate::web::telemetry::WebSessionLifecycleObservation;
|
||||
|
||||
impl WebSession {
|
||||
/// Applies one exactly-once uplink batch to an independent HTTPS lane.
|
||||
@@ -46,7 +47,6 @@ impl WebSession {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
self.ensure_carrier_active_locked(&state)?;
|
||||
state.activity.touch_peer(Instant::now());
|
||||
let new_lane = !state.carrier_lanes.contains_key(&lane_id);
|
||||
if new_lane {
|
||||
if lane_id != 0
|
||||
@@ -55,6 +55,11 @@ impl WebSession {
|
||||
.is_some_and(|value| value.frame_type != FrameType::Open)
|
||||
&& only_late_frames(&frames)
|
||||
{
|
||||
self.touch_peer_locked(
|
||||
&mut state,
|
||||
Instant::now(),
|
||||
WebSessionLifecycleObservation::HttpActivityAfterGap,
|
||||
);
|
||||
return if self.automatic_carrier
|
||||
&& state.negotiation_phase != super::SessionNegotiationPhase::Committed
|
||||
{
|
||||
@@ -89,6 +94,11 @@ impl WebSession {
|
||||
});
|
||||
if sequence == last_sequence && sequence != 0 {
|
||||
return if bool::from(last_digest.ct_eq(&digest)) {
|
||||
self.touch_peer_locked(
|
||||
&mut state,
|
||||
Instant::now(),
|
||||
WebSessionLifecycleObservation::HttpActivityAfterGap,
|
||||
);
|
||||
Ok(sequence)
|
||||
} else {
|
||||
drop(state);
|
||||
@@ -109,6 +119,11 @@ impl WebSession {
|
||||
self.close(SessionCloseReason::Protocol);
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
self.touch_peer_locked(
|
||||
&mut state,
|
||||
Instant::now(),
|
||||
WebSessionLifecycleObservation::HttpActivityAfterGap,
|
||||
);
|
||||
let (reserve_bytes, reserve_items) = inbound_reservation(&state, &frames);
|
||||
if !self.reserve_locked(
|
||||
&mut state,
|
||||
|
||||
+31
-11
@@ -11,6 +11,7 @@ use super::{
|
||||
};
|
||||
use crate::web::frame::{self, FrameType};
|
||||
use crate::web::manager::ManagerError;
|
||||
use crate::web::telemetry::WebSessionLifecycleObservation;
|
||||
|
||||
impl WebSession {
|
||||
/// Polls one lane with independent cursor replay and newest-poll-wins semantics.
|
||||
@@ -65,8 +66,7 @@ impl WebSession {
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
state.activity.touch_peer(Instant::now());
|
||||
let acknowledged = {
|
||||
let (acknowledged, replay) = {
|
||||
let Some(lane) = state.carrier_lanes.get_mut(&lane_id) else {
|
||||
return Ok(PollResult {
|
||||
body: Bytes::new(),
|
||||
@@ -83,27 +83,40 @@ impl WebSession {
|
||||
}
|
||||
if let Some(unacked) = &lane.unacked {
|
||||
if cursor == unacked.base_cursor {
|
||||
return Ok(PollResult {
|
||||
body: unacked.body.clone(),
|
||||
next_cursor: unacked.next_cursor,
|
||||
lane_closed: false,
|
||||
});
|
||||
}
|
||||
if cursor != unacked.next_cursor {
|
||||
(
|
||||
None,
|
||||
Some(PollResult {
|
||||
body: unacked.body.clone(),
|
||||
next_cursor: unacked.next_cursor,
|
||||
lane_closed: false,
|
||||
}),
|
||||
)
|
||||
} else if cursor != unacked.next_cursor {
|
||||
drop(state);
|
||||
self.close(SessionCloseReason::Protocol);
|
||||
return Err(ManagerError::Protocol);
|
||||
} else {
|
||||
(lane.unacked.take(), None)
|
||||
}
|
||||
lane.unacked.take()
|
||||
} else {
|
||||
if cursor != lane.down_cursor {
|
||||
drop(state);
|
||||
self.close(SessionCloseReason::Protocol);
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
None
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
if let Some(result) = replay {
|
||||
if expected_instance.is_none() {
|
||||
self.touch_peer_locked(
|
||||
&mut state,
|
||||
Instant::now(),
|
||||
WebSessionLifecycleObservation::HttpActivityAfterGap,
|
||||
);
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
if let Some(batch) = acknowledged {
|
||||
if let Some(lane) = state.carrier_lanes.get_mut(&lane_id) {
|
||||
lane.pending_bytes = lane.pending_bytes.saturating_sub(batch.data_bytes);
|
||||
@@ -139,6 +152,13 @@ impl WebSession {
|
||||
lane.down_epoch = epoch;
|
||||
let instance = lane.instance;
|
||||
let notify = Arc::clone(&lane.notify);
|
||||
if expected_instance.is_none() {
|
||||
self.touch_peer_locked(
|
||||
&mut state,
|
||||
Instant::now(),
|
||||
WebSessionLifecycleObservation::HttpActivityAfterGap,
|
||||
);
|
||||
}
|
||||
let healthy = self.carrier_health_ready_locked(&mut state, Instant::now());
|
||||
(instance, epoch, notify, healthy)
|
||||
};
|
||||
|
||||
@@ -98,6 +98,7 @@ struct ReleasedQueues {
|
||||
control_items: usize,
|
||||
closed_before_health: bool,
|
||||
reason: SessionCloseReason,
|
||||
peer_gap: Duration,
|
||||
}
|
||||
|
||||
/// Deferred queue release after manager publication linearizes a supersede.
|
||||
@@ -248,6 +249,7 @@ impl WebSession {
|
||||
state: &mut super::SessionState,
|
||||
reason: SessionCloseReason,
|
||||
) -> ReleasedQueues {
|
||||
let peer_gap = state.activity.peer_idle(Instant::now());
|
||||
let closed_before_health = self.automatic_carrier
|
||||
&& state.negotiation_phase == SessionNegotiationPhase::Committed
|
||||
&& self.reject_carrier_health_on_close();
|
||||
@@ -304,6 +306,7 @@ impl WebSession {
|
||||
control_items,
|
||||
closed_before_health,
|
||||
reason,
|
||||
peer_gap,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,11 +340,25 @@ impl WebSession {
|
||||
);
|
||||
}
|
||||
if !self.finished.swap(true, Ordering::AcqRel) {
|
||||
self.trace_lifecycle(
|
||||
crate::web::trace::TraceLifecycleEvent::SessionClosed,
|
||||
None,
|
||||
Some(released.reason.as_str()),
|
||||
);
|
||||
if let Some(manager) = &manager {
|
||||
manager.trace().record_lifecycle_with_context(
|
||||
None,
|
||||
Some(self.client_ip),
|
||||
self.trace_identity(),
|
||||
crate::web::trace::TraceLifecycleEvent::SessionClosed,
|
||||
None,
|
||||
Some(released.reason.as_str()),
|
||||
crate::web::trace::TraceLifecycleContext {
|
||||
peer_gap_ms: Some(
|
||||
released
|
||||
.peer_gap
|
||||
.as_millis()
|
||||
.min(u128::from(u64::MAX)) as u64,
|
||||
),
|
||||
predecessor_session_id: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
if released.reason != SessionCloseReason::CarrierSuperseded
|
||||
&& let Some(manager) = manager
|
||||
{
|
||||
|
||||
@@ -298,6 +298,7 @@ mod tests {
|
||||
WebCarrier, WebLimitsConfig, WebRuntimeProfile, WebSecretMode, WebTimeoutsConfig,
|
||||
};
|
||||
use crate::web::manager::{CarrierClientClass, WebProcessRuntime};
|
||||
use crate::web::session::{SessionCloseOutcome, SessionCloseReason};
|
||||
|
||||
fn session(carrier: WebCarrier, deadline: Instant) -> Arc<WebSession> {
|
||||
let profile = Arc::new(WebRuntimeProfile {
|
||||
@@ -460,7 +461,7 @@ mod tests {
|
||||
let close = std::thread::spawn(move || {
|
||||
close_barrier.wait();
|
||||
std::thread::yield_now();
|
||||
close_session.close(super::SessionCloseReason::ApiClose);
|
||||
close_session.close(SessionCloseReason::ApiClose);
|
||||
});
|
||||
barrier.wait();
|
||||
health.join().unwrap();
|
||||
@@ -473,8 +474,8 @@ mod tests {
|
||||
));
|
||||
assert!(!session.publish_carrier_health());
|
||||
assert_eq!(
|
||||
session.close(super::SessionCloseReason::ApiClose),
|
||||
super::SessionCloseOutcome::AlreadyClosing
|
||||
session.close(SessionCloseReason::ApiClose),
|
||||
SessionCloseOutcome::AlreadyClosing
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use super::{
|
||||
};
|
||||
use crate::web::frame::{self, Frame, FrameType};
|
||||
use crate::web::manager::{ManagerError, TokenHash};
|
||||
use crate::web::telemetry::WebSessionLifecycleObservation;
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(super) struct AppliedProgress {
|
||||
@@ -34,7 +35,11 @@ impl WebSession {
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
) -> Result<u64, ManagerError> {
|
||||
let (acknowledged, progressed) = self.process_up_inner(sequence, body)?;
|
||||
let (acknowledged, progressed) = self.process_up_inner(
|
||||
sequence,
|
||||
body,
|
||||
WebSessionLifecycleObservation::HttpActivityAfterGap,
|
||||
)?;
|
||||
if self.automatic_carrier && !progressed && !self.is_carrier_committed() {
|
||||
return Err(ManagerError::Backpressure);
|
||||
}
|
||||
@@ -47,14 +52,19 @@ impl WebSession {
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
) -> Result<bool, ManagerError> {
|
||||
self.process_up_inner(sequence, body)
|
||||
.map(|(_, progress)| progress)
|
||||
self.process_up_inner(
|
||||
sequence,
|
||||
body,
|
||||
WebSessionLifecycleObservation::WebSocketActivityAfterGap,
|
||||
)
|
||||
.map(|(_, progress)| progress)
|
||||
}
|
||||
|
||||
fn process_up_inner(
|
||||
self: &Arc<Self>,
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
observation: WebSessionLifecycleObservation,
|
||||
) -> Result<(u64, bool), ManagerError> {
|
||||
if !self.carrier().is_multiplexed() {
|
||||
return Err(ManagerError::Protocol);
|
||||
@@ -92,9 +102,9 @@ impl WebSession {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
self.ensure_carrier_active_locked(&state)?;
|
||||
state.activity.touch_peer(Instant::now());
|
||||
if sequence == state.last_up_sequence && sequence != 0 {
|
||||
return if bool::from(state.last_up_digest.ct_eq(&digest)) {
|
||||
self.touch_peer_locked(&mut state, Instant::now(), observation);
|
||||
Ok((sequence, false))
|
||||
} else {
|
||||
drop(state);
|
||||
@@ -112,6 +122,7 @@ impl WebSession {
|
||||
self.close(SessionCloseReason::Protocol);
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
self.touch_peer_locked(&mut state, Instant::now(), observation);
|
||||
let (reserve_bytes, reserve_items) = inbound_reservation(&state, &frames);
|
||||
if !self.reserve_locked(
|
||||
&mut state,
|
||||
|
||||
@@ -391,7 +391,11 @@ impl WebSession {
|
||||
lane.last_up_digest = digest;
|
||||
}
|
||||
}
|
||||
state.activity.touch_peer(Instant::now());
|
||||
self.touch_peer_locked(
|
||||
&mut state,
|
||||
Instant::now(),
|
||||
crate::web::telemetry::WebSessionLifecycleObservation::WebSocketActivityAfterGap,
|
||||
);
|
||||
if applied {
|
||||
(committed, healthy) = self.record_uplink_progress_locked(&mut state, progress);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user