Bridge Request/Buffers/Recovery

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-09-02 18:49:10 +03:00
parent 01ffca5d34
commit 0d8d331c7c
28 changed files with 645 additions and 76 deletions
+65
View File
@@ -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 {
+14 -3
View File
@@ -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)
};
+16 -1
View File
@@ -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
View File
@@ -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)
};
+22 -5
View File
@@ -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
{
+4 -3
View File
@@ -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
);
}
}
+15 -4
View File
@@ -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,
+5 -1
View File
@@ -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);
}