mirror of
https://github.com/telemt/telemt.git
synced 2026-09-11 13:04:08 +03:00
WEB Carriers Safe-matrix Refactored
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -106,6 +106,10 @@ impl WebSession {
|
||||
}
|
||||
let queued = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closing_streams.get(&stream.id) == Some(&stream.instance) {
|
||||
state.closing_streams.remove(&stream.id);
|
||||
self.remember_closed_locked(&mut state, stream.id);
|
||||
}
|
||||
state
|
||||
.streams
|
||||
.get(&stream.id)
|
||||
|
||||
@@ -128,6 +128,9 @@ fn test_runtime_with_dc(
|
||||
1,
|
||||
[9; 32],
|
||||
None,
|
||||
crate::web::manager::CarrierClientClass::Legacy,
|
||||
None,
|
||||
false,
|
||||
limits,
|
||||
timeouts,
|
||||
);
|
||||
|
||||
+21
-114
@@ -16,7 +16,7 @@ impl WebSession {
|
||||
if !self.carrier().is_multiplexed() {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let epoch = {
|
||||
let (epoch, healthy) = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
@@ -35,15 +35,29 @@ impl WebSession {
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let carrier_health_eligible = unacked.carrier_health_eligible;
|
||||
self.release_unacked_locked(&mut state);
|
||||
state.carrier_health_downlink |= carrier_health_eligible;
|
||||
if carrier_health_eligible {
|
||||
state.carrier_health_activity_at = Some(Instant::now());
|
||||
}
|
||||
} else if cursor != state.down_cursor {
|
||||
drop(state);
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
state.down_epoch = state.down_epoch.wrapping_add(1).max(1);
|
||||
state.down_epoch
|
||||
let Some(epoch) = state.down_epoch.checked_add(1) else {
|
||||
drop(state);
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
};
|
||||
state.down_epoch = epoch;
|
||||
let healthy = self.carrier_health_ready_locked(&mut state, Instant::now());
|
||||
(state.down_epoch, healthy)
|
||||
};
|
||||
if healthy {
|
||||
self.finish_carrier_health();
|
||||
}
|
||||
self.down_notify.notify_waiters();
|
||||
|
||||
let deadline = Duration::from_secs(self.timeouts.long_poll_secs);
|
||||
@@ -431,6 +445,8 @@ impl WebSession {
|
||||
data_items,
|
||||
control_bytes,
|
||||
control_items,
|
||||
carrier_health_eligible: state.negotiation_phase
|
||||
== super::SessionNegotiationPhase::Committed,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -450,114 +466,5 @@ impl WebSession {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::{
|
||||
WebCarrier, WebLimitsConfig, WebRuntimeProfile, WebSecretMode, WebTimeoutsConfig,
|
||||
};
|
||||
use crate::web::manager::WebProcessRuntime;
|
||||
|
||||
fn session() -> Arc<WebSession> {
|
||||
let profile = Arc::new(WebRuntimeProfile {
|
||||
host: "proxy.example.com".to_string(),
|
||||
public_addr: SocketAddr::from(([203, 0, 113, 10], 443)),
|
||||
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,
|
||||
max_streams: 1,
|
||||
max_streams_per_session: 1,
|
||||
});
|
||||
WebSession::new(
|
||||
std::sync::Weak::<WebProcessRuntime>::new(),
|
||||
[1; 32],
|
||||
"192.0.2.10".parse().unwrap(),
|
||||
1,
|
||||
profile,
|
||||
[2; 32],
|
||||
WebCarrier::Https,
|
||||
1,
|
||||
[3; 32],
|
||||
None,
|
||||
WebLimitsConfig::default(),
|
||||
WebTimeoutsConfig::default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn queue_close(session: &WebSession) {
|
||||
let encoded = frame::encode(FrameType::Close, 1, &[]);
|
||||
session.state.lock().pending_frames.push_back(QueuedFrame {
|
||||
encoded: BytesMut::from(encoded.as_ref()),
|
||||
frame_type: FrameType::Close,
|
||||
stream_id: 1,
|
||||
control: true,
|
||||
cost: frame::HEADER_BYTES + QUEUE_ITEM_COST,
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn downlink_replays_unacknowledged_batch_byte_for_byte() {
|
||||
let session = session();
|
||||
queue_close(&session);
|
||||
let first = session.poll_down(0).await.unwrap();
|
||||
let replay = session.poll_down(0).await.unwrap();
|
||||
assert_eq!(first.next_cursor, 1);
|
||||
assert_eq!(replay.next_cursor, 1);
|
||||
assert_eq!(first.body, replay.body);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_or_overflowing_cursor_closes_session() {
|
||||
let invalid = session();
|
||||
assert!(matches!(
|
||||
invalid.poll_down(1).await,
|
||||
Err(ManagerError::Protocol)
|
||||
));
|
||||
assert!(invalid.state.lock().closed);
|
||||
|
||||
let overflow = session();
|
||||
{
|
||||
let mut state = overflow.state.lock();
|
||||
state.down_cursor = u64::MAX;
|
||||
}
|
||||
queue_close(&overflow);
|
||||
assert!(matches!(
|
||||
overflow.poll_down(u64::MAX).await,
|
||||
Err(ManagerError::Protocol)
|
||||
));
|
||||
assert!(overflow.state.lock().closed);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn newer_poll_supersedes_older_poll_without_closing_session() {
|
||||
let session = session();
|
||||
let first_session = Arc::clone(&session);
|
||||
let first = tokio::spawn(async move { first_session.poll_down(0).await });
|
||||
while session.state.lock().down_epoch < 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
let second_session = Arc::clone(&session);
|
||||
let second = tokio::spawn(async move { second_session.poll_down(0).await });
|
||||
while session.state.lock().down_epoch < 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
let superseded = tokio::time::timeout(Duration::from_secs(1), first)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(superseded.body.is_empty());
|
||||
assert_eq!(superseded.next_cursor, 0);
|
||||
assert!(!session.state.lock().closed);
|
||||
second.abort();
|
||||
}
|
||||
}
|
||||
#[path = "downlink_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
use super::*;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use crate::config::{
|
||||
ProxyConfig, WebCarrier, WebLimitsConfig, WebRuntimeProfile, WebSecretMode,
|
||||
WebTimeoutsConfig,
|
||||
};
|
||||
use crate::maestro::generation::test_runtime_generation;
|
||||
use crate::web::manager::WebProcessRuntime;
|
||||
|
||||
fn session() -> (Arc<WebSession>, Arc<WebProcessRuntime>) {
|
||||
let generation = test_runtime_generation(1, ProxyConfig::default());
|
||||
let manager = WebProcessRuntime::start(Arc::new(ArcSwap::from(generation)));
|
||||
let profile = Arc::new(WebRuntimeProfile {
|
||||
host: "proxy.example.com".to_string(),
|
||||
public_addr: SocketAddr::from(([203, 0, 113, 10], 443)),
|
||||
user: "alice".to_string(),
|
||||
secret_mode: WebSecretMode::Plain,
|
||||
carrier: WebCarrier::Https,
|
||||
carrier_negotiation_enabled: false,
|
||||
carrier_learning: false,
|
||||
carriers: Arc::from([WebCarrier::Https]),
|
||||
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
|
||||
capability: [0; 32],
|
||||
key_fingerprint: "0000000000000000".to_string(),
|
||||
max_sessions: 1,
|
||||
max_streams: 1,
|
||||
max_streams_per_session: 1,
|
||||
});
|
||||
let mut timeouts = WebTimeoutsConfig::default();
|
||||
timeouts.long_poll_secs = 1;
|
||||
let session = WebSession::new(
|
||||
Arc::downgrade(&manager),
|
||||
[1; 32],
|
||||
"192.0.2.10".parse().unwrap(),
|
||||
1,
|
||||
profile,
|
||||
[2; 32],
|
||||
WebCarrier::Https,
|
||||
1,
|
||||
[3; 32],
|
||||
None,
|
||||
crate::web::manager::CarrierClientClass::Legacy,
|
||||
None,
|
||||
false,
|
||||
WebLimitsConfig::default(),
|
||||
timeouts,
|
||||
);
|
||||
(session, manager)
|
||||
}
|
||||
|
||||
fn queue_close(session: &WebSession) {
|
||||
let mut state = session.state.lock();
|
||||
assert!(session.queue_control_locked(&mut state, FrameType::Close, 1, &[]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn downlink_replays_unacknowledged_batch_byte_for_byte() {
|
||||
let (session, manager) = session();
|
||||
queue_close(&session);
|
||||
let first = session.poll_down(0).await.unwrap();
|
||||
let replay = session.poll_down(0).await.unwrap();
|
||||
assert_eq!(first.next_cursor, 1);
|
||||
assert_eq!(replay.next_cursor, 1);
|
||||
assert_eq!(first.body, replay.body);
|
||||
drop(first);
|
||||
drop(replay);
|
||||
session.close();
|
||||
manager.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn acknowledged_response_stays_resident_until_the_last_body_clone_drops() {
|
||||
let (session, manager) = session();
|
||||
queue_close(&session);
|
||||
let response = session.poll_down(0).await.unwrap();
|
||||
let retained = response.body.clone();
|
||||
{
|
||||
let mut state = session.state.lock();
|
||||
session.release_unacked_locked(&mut state);
|
||||
assert_eq!(state.pending_bytes, 0);
|
||||
}
|
||||
assert!(session.resident.snapshot().bytes() > 0);
|
||||
drop(response);
|
||||
assert!(session.resident.snapshot().bytes() > 0);
|
||||
drop(retained);
|
||||
assert_eq!(session.resident.snapshot().bytes(), 0);
|
||||
session.close();
|
||||
manager.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_or_overflowing_cursor_closes_session() {
|
||||
let (invalid, invalid_manager) = session();
|
||||
assert!(matches!(
|
||||
invalid.poll_down(1).await,
|
||||
Err(ManagerError::Protocol)
|
||||
));
|
||||
assert!(invalid.state.lock().closed);
|
||||
invalid_manager.shutdown().await;
|
||||
|
||||
let (overflow, overflow_manager) = session();
|
||||
{
|
||||
let mut state = overflow.state.lock();
|
||||
state.down_cursor = u64::MAX;
|
||||
}
|
||||
queue_close(&overflow);
|
||||
assert!(matches!(
|
||||
overflow.poll_down(u64::MAX).await,
|
||||
Err(ManagerError::Protocol)
|
||||
));
|
||||
assert!(overflow.state.lock().closed);
|
||||
overflow_manager.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn newer_poll_supersedes_older_poll_without_closing_session() {
|
||||
let (session, manager) = session();
|
||||
let first_session = Arc::clone(&session);
|
||||
let first = tokio::spawn(async move { first_session.poll_down(0).await });
|
||||
while session.state.lock().down_epoch < 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
let second_session = Arc::clone(&session);
|
||||
let second = tokio::spawn(async move { second_session.poll_down(0).await });
|
||||
while session.state.lock().down_epoch < 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
let superseded = tokio::time::timeout(Duration::from_secs(1), first)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(superseded.body.is_empty());
|
||||
assert_eq!(superseded.next_cursor, 0);
|
||||
assert!(!session.state.lock().closed);
|
||||
second.abort();
|
||||
session.close();
|
||||
manager.shutdown().await;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
||||
use super::resident::{OwnedBatchBody, PendingCounts, PendingResponseLease};
|
||||
use super::{CarrierLane, DownBatch, WebSession};
|
||||
use crate::config::WebLimitsConfig;
|
||||
use crate::web::frame::FrameType;
|
||||
use crate::web::manager::ManagerError;
|
||||
|
||||
/// Stages one bounded lane batch and transfers its accounting into response ownership.
|
||||
pub(super) fn take_lane_down_batch(
|
||||
session: &WebSession,
|
||||
limits: &WebLimitsConfig,
|
||||
lane: &mut CarrierLane,
|
||||
cursor: u64,
|
||||
carrier_health_eligible: bool,
|
||||
) -> Result<DownBatch, ManagerError> {
|
||||
let next_cursor = lane
|
||||
.down_cursor
|
||||
.checked_add(1)
|
||||
.ok_or(ManagerError::Protocol)?;
|
||||
let mut count = 0usize;
|
||||
let mut body_len = 0usize;
|
||||
for queued in &lane.pending_frames {
|
||||
if count >= limits.max_frames_per_body
|
||||
|| (count != 0
|
||||
&& body_len.saturating_add(queued.encoded.len()) > limits.carrier_batch_bytes)
|
||||
{
|
||||
break;
|
||||
}
|
||||
body_len += queued.encoded.len();
|
||||
count += 1;
|
||||
}
|
||||
let Some(manager) = session.manager.upgrade() else {
|
||||
return Err(ManagerError::Closed);
|
||||
};
|
||||
let Some(_staging) = manager.try_downlink_staging_budget(body_len) else {
|
||||
return Err(ManagerError::Backpressure);
|
||||
};
|
||||
let mut body = BytesMut::with_capacity(body_len);
|
||||
let mut data_bytes = 0usize;
|
||||
let mut data_items = 0usize;
|
||||
let mut control_bytes = 0usize;
|
||||
let mut control_items = 0usize;
|
||||
for index in 0..count {
|
||||
let Some(queued) = lane.pending_frames.get(index) else {
|
||||
break;
|
||||
};
|
||||
if queued.frame_type == FrameType::Window
|
||||
&& lane.pending_windows.get(&queued.stream_id) == Some(&index)
|
||||
{
|
||||
lane.pending_windows.remove(&queued.stream_id);
|
||||
}
|
||||
}
|
||||
for _ in 0..count {
|
||||
let Some(queued) = lane.pending_frames.pop_front() else {
|
||||
break;
|
||||
};
|
||||
body.extend_from_slice(&queued.encoded);
|
||||
if queued.control {
|
||||
control_bytes += queued.cost;
|
||||
control_items += 1;
|
||||
} else {
|
||||
data_bytes += queued.cost;
|
||||
data_items += 1;
|
||||
}
|
||||
}
|
||||
for index in lane.pending_windows.values_mut() {
|
||||
*index = index.saturating_sub(count);
|
||||
}
|
||||
lane.down_cursor = next_cursor;
|
||||
let counts = PendingCounts {
|
||||
data_bytes,
|
||||
data_items,
|
||||
control_bytes,
|
||||
control_items,
|
||||
};
|
||||
let lease = PendingResponseLease::new(session, counts, Some(Arc::clone(&lane.resident)));
|
||||
let body = Bytes::from_owner(OwnedBatchBody::new(body.freeze(), Arc::clone(&lease)));
|
||||
Ok(DownBatch {
|
||||
body,
|
||||
lease,
|
||||
base_cursor: cursor,
|
||||
next_cursor,
|
||||
data_bytes,
|
||||
data_items,
|
||||
control_bytes,
|
||||
control_items,
|
||||
carrier_health_eligible,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
use super::uplink::{AppliedProgress, inbound_reservation, validate_batch};
|
||||
use super::{PendingClass, WebSession, insert_carrier_lane};
|
||||
use crate::config::WebCarrier;
|
||||
use crate::web::frame::{self, Frame, FrameType};
|
||||
use crate::web::manager::{ManagerError, TokenHash};
|
||||
|
||||
impl WebSession {
|
||||
/// Applies one exactly-once uplink batch to an independent HTTPS lane.
|
||||
pub(crate) fn process_up_lane(
|
||||
self: &Arc<Self>,
|
||||
lane_id: u32,
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
) -> Result<u64, ManagerError> {
|
||||
if self.carrier() != WebCarrier::HttpsLanes || lane_id > frame::MAX_STREAM_ID {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let frames = match frame::parse_all(body, &self.limits) {
|
||||
Ok(frames) => frames,
|
||||
Err(_) => {
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
};
|
||||
if frames
|
||||
.iter()
|
||||
.copied()
|
||||
.any(|value| value.stream_id != lane_id || frame::validate_client_shape(value).is_err())
|
||||
{
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let digest: TokenHash = Sha256::digest(body).into();
|
||||
let mut opened = Vec::new();
|
||||
let mut committed = false;
|
||||
let mut healthy = 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();
|
||||
let new_lane = !state.carrier_lanes.contains_key(&lane_id);
|
||||
if new_lane {
|
||||
if lane_id != 0
|
||||
&& frames
|
||||
.first()
|
||||
.is_some_and(|value| value.frame_type != FrameType::Open)
|
||||
&& only_late_frames(&frames)
|
||||
{
|
||||
return Ok(sequence);
|
||||
}
|
||||
if lane_id == 0
|
||||
|| frames
|
||||
.first()
|
||||
.is_none_or(|value| value.frame_type != FrameType::Open)
|
||||
{
|
||||
drop(state);
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let lane_limit = self
|
||||
.profile
|
||||
.max_streams_per_session
|
||||
.saturating_add(self.limits.max_tombstones_per_session)
|
||||
.saturating_add(1);
|
||||
if state.carrier_lanes.len() >= lane_limit {
|
||||
return Err(ManagerError::Limit);
|
||||
}
|
||||
}
|
||||
let (last_sequence, last_digest, up_active) = state
|
||||
.carrier_lanes
|
||||
.get(&lane_id)
|
||||
.map_or((0, [0; 32], false), |lane| {
|
||||
(lane.last_up_sequence, lane.last_up_digest, lane.up_active)
|
||||
});
|
||||
if sequence == last_sequence && sequence != 0 {
|
||||
return if bool::from(last_digest.ct_eq(&digest)) {
|
||||
Ok(sequence)
|
||||
} else {
|
||||
drop(state);
|
||||
self.close();
|
||||
Err(ManagerError::Protocol)
|
||||
};
|
||||
}
|
||||
if sequence == 0 || sequence != last_sequence.saturating_add(1) {
|
||||
drop(state);
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
if up_active {
|
||||
return Err(ManagerError::Concurrent);
|
||||
}
|
||||
if !validate_batch(&state, &frames) {
|
||||
drop(state);
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let (reserve_bytes, reserve_items) = inbound_reservation(&state, &frames);
|
||||
if !self.reserve_locked(
|
||||
&mut state,
|
||||
reserve_bytes,
|
||||
reserve_items,
|
||||
PendingClass::Uplink,
|
||||
) {
|
||||
if let Some(lane) = state.carrier_lanes.get_mut(&lane_id) {
|
||||
lane.up_active = false;
|
||||
}
|
||||
return Err(ManagerError::Backpressure);
|
||||
}
|
||||
if new_lane && insert_carrier_lane(&mut state, lane_id).is_none() {
|
||||
self.release_locked(&mut state, reserve_bytes, reserve_items, false);
|
||||
drop(state);
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let Some(lane) = state.carrier_lanes.get_mut(&lane_id) else {
|
||||
self.release_locked(&mut state, reserve_bytes, reserve_items, false);
|
||||
return Err(ManagerError::Closed);
|
||||
};
|
||||
lane.up_active = true;
|
||||
let mut unused_bytes = reserve_bytes;
|
||||
let mut unused_items = reserve_items;
|
||||
let mut progress = AppliedProgress::default();
|
||||
let applied = self.apply_batch_locked(
|
||||
&mut state,
|
||||
&frames,
|
||||
&mut opened,
|
||||
&mut None,
|
||||
&mut unused_bytes,
|
||||
&mut unused_items,
|
||||
&mut progress,
|
||||
);
|
||||
self.release_locked(&mut state, unused_bytes, unused_items, false);
|
||||
if let Some(lane) = state.carrier_lanes.get_mut(&lane_id) {
|
||||
lane.up_active = false;
|
||||
if applied {
|
||||
lane.last_up_sequence = sequence;
|
||||
lane.last_up_digest = digest;
|
||||
}
|
||||
}
|
||||
if applied {
|
||||
(committed, healthy) =
|
||||
self.record_uplink_progress_locked(&mut state, progress);
|
||||
}
|
||||
applied.then_some(sequence).ok_or(ManagerError::Closed)
|
||||
};
|
||||
if matches!(result, Err(ManagerError::Backpressure)) {
|
||||
return result;
|
||||
}
|
||||
if result.is_err() {
|
||||
self.close();
|
||||
drop(opened);
|
||||
return result;
|
||||
}
|
||||
if committed {
|
||||
self.finish_carrier_commit();
|
||||
}
|
||||
if healthy {
|
||||
self.finish_carrier_health();
|
||||
}
|
||||
self.lane_open_notify.notify_waiters();
|
||||
for completion in opened {
|
||||
self.spawn_stream(completion, false);
|
||||
}
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
manager.record_up(body.len());
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn only_late_frames(frames: &[Frame<'_>]) -> bool {
|
||||
frames.iter().all(|value| {
|
||||
matches!(
|
||||
value.frame_type,
|
||||
FrameType::Data | FrameType::Window | FrameType::Close
|
||||
)
|
||||
})
|
||||
}
|
||||
+101
-299
@@ -2,184 +2,16 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use sha2::{Digest, Sha256};
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
use super::uplink::{inbound_reservation, validate_batch};
|
||||
use super::resident::{OwnedBatchBody, PendingCounts, PendingResponseLease};
|
||||
use tokio::sync::OwnedSemaphorePermit;
|
||||
use super::lane_downlink::take_lane_down_batch;
|
||||
use super::{
|
||||
CarrierLane, DownBatch, PendingClass, PollResult, QUEUE_ITEM_COST, QueuedFrame, SessionState,
|
||||
WebSession, insert_carrier_lane, remember_closed,
|
||||
PendingClass, PollResult, QUEUE_ITEM_COST, QueuedFrame, SessionState, WebSession,
|
||||
remember_closed,
|
||||
};
|
||||
use crate::config::{WebCarrier, WebLimitsConfig};
|
||||
use crate::web::frame::{self, Frame, FrameType};
|
||||
use crate::web::manager::{ManagerError, TokenHash};
|
||||
use crate::web::frame::{self, FrameType};
|
||||
use crate::web::manager::ManagerError;
|
||||
|
||||
impl WebSession {
|
||||
/// Classifies control and pre-OPEN polls for their reserved handler pool.
|
||||
pub(crate) fn lane_poll_is_auxiliary(&self, lane_id: u32) -> bool {
|
||||
let state = self.state.lock();
|
||||
lane_id == 0 || !state.carrier_lanes.contains_key(&lane_id)
|
||||
}
|
||||
|
||||
/// Applies one exactly-once uplink batch to an independent HTTPS lane.
|
||||
pub(crate) fn process_up_lane(
|
||||
self: &Arc<Self>,
|
||||
lane_id: u32,
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
) -> Result<u64, ManagerError> {
|
||||
if self.carrier() != WebCarrier::HttpsLanes || lane_id > frame::MAX_STREAM_ID {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let frames = match frame::parse_all(body, &self.limits) {
|
||||
Ok(frames) => frames,
|
||||
Err(_) => {
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
};
|
||||
if frames
|
||||
.iter()
|
||||
.copied()
|
||||
.any(|value| value.stream_id != lane_id || frame::validate_client_shape(value).is_err())
|
||||
{
|
||||
self.close();
|
||||
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();
|
||||
let new_lane = !state.carrier_lanes.contains_key(&lane_id);
|
||||
if new_lane {
|
||||
if lane_id != 0
|
||||
&& frames
|
||||
.first()
|
||||
.is_some_and(|value| value.frame_type != FrameType::Open)
|
||||
&& only_late_frames(&frames)
|
||||
{
|
||||
return Ok(sequence);
|
||||
}
|
||||
if lane_id == 0
|
||||
|| frames
|
||||
.first()
|
||||
.is_none_or(|value| value.frame_type != FrameType::Open)
|
||||
{
|
||||
drop(state);
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
if state.carrier_lanes.len()
|
||||
>= self.profile.max_streams_per_session.saturating_add(1)
|
||||
{
|
||||
return Err(ManagerError::Limit);
|
||||
}
|
||||
}
|
||||
let (last_sequence, last_digest, up_active) = state
|
||||
.carrier_lanes
|
||||
.get(&lane_id)
|
||||
.map_or((0, [0; 32], false), |lane| {
|
||||
(lane.last_up_sequence, lane.last_up_digest, lane.up_active)
|
||||
});
|
||||
if sequence == last_sequence && sequence != 0 {
|
||||
return if bool::from(last_digest.ct_eq(&digest)) {
|
||||
Ok(sequence)
|
||||
} else {
|
||||
drop(state);
|
||||
self.close();
|
||||
Err(ManagerError::Protocol)
|
||||
};
|
||||
}
|
||||
if sequence == 0 || sequence != last_sequence.saturating_add(1) {
|
||||
drop(state);
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
if up_active {
|
||||
return Err(ManagerError::Concurrent);
|
||||
}
|
||||
if !validate_batch(&state, &frames) {
|
||||
drop(state);
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let (reserve_bytes, reserve_items) = inbound_reservation(&state, &frames);
|
||||
if !self.reserve_locked(
|
||||
&mut state,
|
||||
reserve_bytes,
|
||||
reserve_items,
|
||||
PendingClass::Uplink,
|
||||
) {
|
||||
if let Some(lane) = state.carrier_lanes.get_mut(&lane_id) {
|
||||
lane.up_active = false;
|
||||
}
|
||||
return Err(ManagerError::Backpressure);
|
||||
}
|
||||
if new_lane && insert_carrier_lane(&mut state, lane_id).is_none() {
|
||||
self.release_locked(&mut state, reserve_bytes, reserve_items, false);
|
||||
drop(state);
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
let Some(lane) = state.carrier_lanes.get_mut(&lane_id) else {
|
||||
self.release_locked(&mut state, reserve_bytes, reserve_items, false);
|
||||
return Err(ManagerError::Closed);
|
||||
};
|
||||
lane.up_active = true;
|
||||
let mut unused_bytes = reserve_bytes;
|
||||
let mut unused_items = reserve_items;
|
||||
let applied = self.apply_batch_locked(
|
||||
&mut state,
|
||||
&frames,
|
||||
&mut opened,
|
||||
&mut None,
|
||||
&mut unused_bytes,
|
||||
&mut unused_items,
|
||||
);
|
||||
self.release_locked(&mut state, unused_bytes, unused_items, false);
|
||||
if let Some(lane) = state.carrier_lanes.get_mut(&lane_id) {
|
||||
lane.up_active = false;
|
||||
if applied {
|
||||
lane.last_up_sequence = sequence;
|
||||
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)) {
|
||||
return result;
|
||||
}
|
||||
if result.is_err() {
|
||||
self.close();
|
||||
drop(opened);
|
||||
return result;
|
||||
}
|
||||
if committed {
|
||||
self.finish_carrier_commit();
|
||||
}
|
||||
self.lane_open_notify.notify_waiters();
|
||||
for completion in opened {
|
||||
self.spawn_stream(completion, false);
|
||||
}
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
manager.record_up(body.len());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Polls one lane with independent cursor replay and newest-poll-wins semantics.
|
||||
pub(crate) async fn poll_down_lane(
|
||||
&self,
|
||||
@@ -196,7 +28,7 @@ impl WebSession {
|
||||
lane_closed: false,
|
||||
});
|
||||
}
|
||||
let (instance, epoch, notify) = {
|
||||
let (instance, epoch, notify, healthy) = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
@@ -235,12 +67,8 @@ impl WebSession {
|
||||
};
|
||||
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.saturating_add(batch.control_bytes));
|
||||
lane.pending_items = lane
|
||||
.pending_items
|
||||
.saturating_sub(batch.data_items.saturating_add(batch.control_items));
|
||||
lane.pending_bytes = lane.pending_bytes.saturating_sub(batch.data_bytes);
|
||||
lane.pending_items = lane.pending_items.saturating_sub(batch.data_items);
|
||||
}
|
||||
batch.lease.detach();
|
||||
self.release_local_locked(&mut state, batch.data_bytes, batch.data_items, false);
|
||||
@@ -250,6 +78,10 @@ impl WebSession {
|
||||
batch.control_items,
|
||||
true,
|
||||
);
|
||||
state.carrier_health_downlink |= batch.carrier_health_eligible;
|
||||
if batch.carrier_health_eligible {
|
||||
state.carrier_health_activity_at = Some(Instant::now());
|
||||
}
|
||||
if let Some(stream) = state.streams.get_mut(&lane_id)
|
||||
&& let Some(waker) = stream.write_waker.take()
|
||||
{
|
||||
@@ -260,9 +92,20 @@ impl WebSession {
|
||||
.carrier_lanes
|
||||
.get_mut(&lane_id)
|
||||
.ok_or(ManagerError::Protocol)?;
|
||||
lane.down_epoch = lane.down_epoch.wrapping_add(1).max(1);
|
||||
(lane.instance, lane.down_epoch, Arc::clone(&lane.notify))
|
||||
let Some(epoch) = lane.down_epoch.checked_add(1) else {
|
||||
drop(state);
|
||||
self.close();
|
||||
return Err(ManagerError::Protocol);
|
||||
};
|
||||
lane.down_epoch = epoch;
|
||||
let instance = lane.instance;
|
||||
let notify = Arc::clone(&lane.notify);
|
||||
let healthy = self.carrier_health_ready_locked(&mut state, Instant::now());
|
||||
(instance, epoch, notify, healthy)
|
||||
};
|
||||
if healthy {
|
||||
self.finish_carrier_health();
|
||||
}
|
||||
notify.notify_waiters();
|
||||
|
||||
let deadline = Duration::from_secs(self.timeouts.long_poll_secs);
|
||||
@@ -274,6 +117,9 @@ impl WebSession {
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
let carrier_health_eligible = lane_id != 0
|
||||
&& state.negotiation_phase
|
||||
== super::SessionNegotiationPhase::Committed;
|
||||
let Some(lane) = state.carrier_lanes.get_mut(&lane_id) else {
|
||||
return Ok(PollResult {
|
||||
body: Bytes::new(),
|
||||
@@ -281,7 +127,14 @@ impl WebSession {
|
||||
lane_closed: true,
|
||||
});
|
||||
};
|
||||
if lane.instance != instance || lane.down_epoch != epoch {
|
||||
if lane.instance != instance {
|
||||
return Ok(PollResult {
|
||||
body: Bytes::new(),
|
||||
next_cursor: cursor,
|
||||
lane_closed: true,
|
||||
});
|
||||
}
|
||||
if lane.down_epoch != epoch {
|
||||
return Ok(PollResult {
|
||||
body: Bytes::new(),
|
||||
next_cursor: cursor,
|
||||
@@ -289,7 +142,13 @@ impl WebSession {
|
||||
});
|
||||
}
|
||||
if !lane.pending_frames.is_empty() {
|
||||
let batch = match take_lane_down_batch(self, &self.limits, lane, cursor) {
|
||||
let batch = match take_lane_down_batch(
|
||||
self,
|
||||
&self.limits,
|
||||
lane,
|
||||
cursor,
|
||||
carrier_health_eligible,
|
||||
) {
|
||||
Ok(batch) => batch,
|
||||
Err(ManagerError::Backpressure) => {
|
||||
return Err(ManagerError::Backpressure);
|
||||
@@ -350,12 +209,17 @@ impl WebSession {
|
||||
lane_closed: true,
|
||||
});
|
||||
}
|
||||
if state
|
||||
.carrier_lanes
|
||||
.get(&lane_id)
|
||||
.is_some_and(|lane| lane.instance == instance && lane.down_epoch == epoch)
|
||||
{
|
||||
state.last_activity = Instant::now();
|
||||
if let Some(lane) = state.carrier_lanes.get(&lane_id) {
|
||||
if lane.instance != instance {
|
||||
return Ok(PollResult {
|
||||
body: Bytes::new(),
|
||||
next_cursor: cursor,
|
||||
lane_closed: true,
|
||||
});
|
||||
}
|
||||
if lane.down_epoch == epoch {
|
||||
state.last_activity = Instant::now();
|
||||
}
|
||||
}
|
||||
Ok(PollResult {
|
||||
body: Bytes::new(),
|
||||
@@ -392,8 +256,17 @@ impl WebSession {
|
||||
if state.lane_open_waits >= self.limits.max_lane_open_waits_per_session {
|
||||
return Err(ManagerError::Limit);
|
||||
}
|
||||
let Some(manager) = self.manager.upgrade() else {
|
||||
return Err(ManagerError::Closed);
|
||||
};
|
||||
let Some(auxiliary) = manager.try_lane_poll(true) else {
|
||||
return Err(ManagerError::Limit);
|
||||
};
|
||||
state.lane_open_waits += 1;
|
||||
LaneOpenWaitGuard { session: self }
|
||||
LaneOpenWaitGuard {
|
||||
session: self,
|
||||
_auxiliary: auxiliary,
|
||||
}
|
||||
};
|
||||
let deadline = Duration::from_secs(self.timeouts.lane_open_wait_secs);
|
||||
let opened = tokio::time::timeout(deadline, async {
|
||||
@@ -401,23 +274,34 @@ impl WebSession {
|
||||
let notified = self.lane_open_notify.notified();
|
||||
{
|
||||
let state = self.state.lock();
|
||||
if state.closed
|
||||
|| state.carrier_lanes.contains_key(&lane_id)
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
if state.carrier_lanes.contains_key(&lane_id)
|
||||
|| state.closed_streams.contains(&lane_id)
|
||||
|| state.closing_streams.contains_key(&lane_id)
|
||||
{
|
||||
return state.carrier_lanes.contains_key(&lane_id)
|
||||
|| state.closed_streams.contains(&lane_id)
|
||||
|| state.closing_streams.contains_key(&lane_id);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
notified.await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
.await;
|
||||
drop(wait);
|
||||
Ok(opened)
|
||||
match opened {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
let state = self.state.lock();
|
||||
if state.closed {
|
||||
Err(ManagerError::Closed)
|
||||
} else {
|
||||
Ok(state.carrier_lanes.contains_key(&lane_id)
|
||||
|| state.closed_streams.contains(&lane_id)
|
||||
|| state.closing_streams.contains_key(&lane_id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn queue_lane_frame_locked(
|
||||
@@ -468,7 +352,8 @@ impl WebSession {
|
||||
if can_coalesce {
|
||||
if state.carrier_lanes.get(&stream_id).is_none_or(|lane| {
|
||||
let resident = lane.resident.snapshot();
|
||||
lane.pending_bytes.saturating_add(resident.bytes())
|
||||
payload.len() > self.limits.pending_bytes_per_lane
|
||||
|| lane.pending_bytes.saturating_add(resident.data_bytes)
|
||||
> self
|
||||
.limits
|
||||
.pending_bytes_per_lane
|
||||
@@ -501,13 +386,16 @@ impl WebSession {
|
||||
} else {
|
||||
PendingClass::Downlink
|
||||
};
|
||||
if state.carrier_lanes.get(&stream_id).is_none_or(|lane| {
|
||||
let resident = lane.resident.snapshot();
|
||||
lane.pending_bytes.saturating_add(resident.bytes())
|
||||
> self.limits.pending_bytes_per_lane.saturating_sub(cost)
|
||||
|| lane.pending_items.saturating_add(resident.items())
|
||||
>= self.limits.pending_items_per_lane
|
||||
}) {
|
||||
if !control
|
||||
&& state.carrier_lanes.get(&stream_id).is_none_or(|lane| {
|
||||
let resident = lane.resident.snapshot();
|
||||
cost > self.limits.pending_bytes_per_lane
|
||||
|| lane.pending_bytes.saturating_add(resident.data_bytes)
|
||||
> self.limits.pending_bytes_per_lane.saturating_sub(cost)
|
||||
|| lane.pending_items.saturating_add(resident.data_items)
|
||||
>= self.limits.pending_items_per_lane
|
||||
})
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if !self.reserve_locked(state, cost, 1, class) {
|
||||
@@ -532,8 +420,10 @@ impl WebSession {
|
||||
control,
|
||||
cost,
|
||||
});
|
||||
lane.pending_bytes += cost;
|
||||
lane.pending_items += 1;
|
||||
if !control {
|
||||
lane.pending_bytes += cost;
|
||||
lane.pending_items += 1;
|
||||
}
|
||||
if frame_type == FrameType::Window {
|
||||
lane.pending_windows.insert(stream_id, index);
|
||||
}
|
||||
@@ -585,6 +475,7 @@ impl WebSession {
|
||||
|
||||
struct LaneOpenWaitGuard<'a> {
|
||||
session: &'a WebSession,
|
||||
_auxiliary: OwnedSemaphorePermit,
|
||||
}
|
||||
|
||||
impl Drop for LaneOpenWaitGuard<'_> {
|
||||
@@ -594,95 +485,6 @@ impl Drop for LaneOpenWaitGuard<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn only_late_frames(frames: &[Frame<'_>]) -> bool {
|
||||
frames.iter().all(|value| {
|
||||
matches!(
|
||||
value.frame_type,
|
||||
FrameType::Data | FrameType::Window | FrameType::Close
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn take_lane_down_batch(
|
||||
session: &WebSession,
|
||||
limits: &WebLimitsConfig,
|
||||
lane: &mut CarrierLane,
|
||||
cursor: u64,
|
||||
) -> Result<DownBatch, ManagerError> {
|
||||
let next_cursor = lane
|
||||
.down_cursor
|
||||
.checked_add(1)
|
||||
.ok_or(ManagerError::Protocol)?;
|
||||
let mut count = 0usize;
|
||||
let mut body_len = 0usize;
|
||||
for queued in &lane.pending_frames {
|
||||
if count >= limits.max_frames_per_body
|
||||
|| (count != 0
|
||||
&& body_len.saturating_add(queued.encoded.len()) > limits.carrier_batch_bytes)
|
||||
{
|
||||
break;
|
||||
}
|
||||
body_len += queued.encoded.len();
|
||||
count += 1;
|
||||
}
|
||||
let Some(manager) = session.manager.upgrade() else {
|
||||
return Err(ManagerError::Closed);
|
||||
};
|
||||
let Some(_staging) = manager.try_downlink_staging_budget(body_len) else {
|
||||
return Err(ManagerError::Backpressure);
|
||||
};
|
||||
let mut body = BytesMut::with_capacity(body_len);
|
||||
let mut data_bytes = 0usize;
|
||||
let mut data_items = 0usize;
|
||||
let mut control_bytes = 0usize;
|
||||
let mut control_items = 0usize;
|
||||
for index in 0..count {
|
||||
let Some(queued) = lane.pending_frames.get(index) else {
|
||||
break;
|
||||
};
|
||||
if queued.frame_type == FrameType::Window
|
||||
&& lane.pending_windows.get(&queued.stream_id) == Some(&index)
|
||||
{
|
||||
lane.pending_windows.remove(&queued.stream_id);
|
||||
}
|
||||
}
|
||||
for _ in 0..count {
|
||||
let Some(queued) = lane.pending_frames.pop_front() else {
|
||||
break;
|
||||
};
|
||||
body.extend_from_slice(&queued.encoded);
|
||||
if queued.control {
|
||||
control_bytes += queued.cost;
|
||||
control_items += 1;
|
||||
} else {
|
||||
data_bytes += queued.cost;
|
||||
data_items += 1;
|
||||
}
|
||||
}
|
||||
for index in lane.pending_windows.values_mut() {
|
||||
*index = index.saturating_sub(count);
|
||||
}
|
||||
lane.down_cursor = next_cursor;
|
||||
let counts = PendingCounts {
|
||||
data_bytes,
|
||||
data_items,
|
||||
control_bytes,
|
||||
control_items,
|
||||
};
|
||||
let lease = PendingResponseLease::new(session, counts, Some(Arc::clone(&lane.resident)));
|
||||
let body = Bytes::from_owner(OwnedBatchBody::new(body.freeze(), Arc::clone(&lease)));
|
||||
Ok(DownBatch {
|
||||
body,
|
||||
lease,
|
||||
base_cursor: cursor,
|
||||
next_cursor,
|
||||
data_bytes,
|
||||
data_items,
|
||||
control_bytes,
|
||||
control_items,
|
||||
})
|
||||
}
|
||||
|
||||
// Lane-specific protocol, replay, and lifecycle tests.
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
+137
-16
@@ -1,13 +1,26 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use bytes::BytesMut;
|
||||
|
||||
use super::*;
|
||||
use crate::config::{WebRuntimeProfile, WebSecretMode, WebTimeoutsConfig};
|
||||
use crate::config::{
|
||||
ProxyConfig, WebCarrier, WebLimitsConfig, WebRuntimeProfile, WebSecretMode,
|
||||
WebTimeoutsConfig,
|
||||
};
|
||||
use crate::maestro::generation::test_runtime_generation;
|
||||
use crate::web::manager::WebProcessRuntime;
|
||||
use crate::web::session::{CarrierLane, insert_carrier_lane};
|
||||
|
||||
fn session_with_limits(limits: WebLimitsConfig) -> Arc<WebSession> {
|
||||
new_session(limits, std::sync::Weak::new())
|
||||
}
|
||||
|
||||
fn new_session(
|
||||
limits: WebLimitsConfig,
|
||||
manager: std::sync::Weak<WebProcessRuntime>,
|
||||
) -> Arc<WebSession> {
|
||||
let profile = Arc::new(WebRuntimeProfile {
|
||||
host: "proxy.example.com".to_string(),
|
||||
public_addr: SocketAddr::from(([203, 0, 113, 10], 443)),
|
||||
@@ -25,7 +38,7 @@ fn session_with_limits(limits: WebLimitsConfig) -> Arc<WebSession> {
|
||||
max_streams_per_session: 2,
|
||||
});
|
||||
WebSession::new(
|
||||
std::sync::Weak::<WebProcessRuntime>::new(),
|
||||
manager,
|
||||
[1; 32],
|
||||
"192.0.2.10".parse().unwrap(),
|
||||
1,
|
||||
@@ -35,22 +48,134 @@ fn session_with_limits(limits: WebLimitsConfig) -> Arc<WebSession> {
|
||||
1,
|
||||
[3; 32],
|
||||
None,
|
||||
crate::web::manager::CarrierClientClass::Legacy,
|
||||
None,
|
||||
false,
|
||||
limits,
|
||||
WebTimeoutsConfig::default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn session_with_manager() -> (Arc<WebSession>, Arc<WebProcessRuntime>) {
|
||||
session_with_manager_limits(WebLimitsConfig::default())
|
||||
}
|
||||
|
||||
fn session_with_manager_limits(
|
||||
limits: WebLimitsConfig,
|
||||
) -> (Arc<WebSession>, Arc<WebProcessRuntime>) {
|
||||
let generation = test_runtime_generation(1, ProxyConfig::default());
|
||||
let manager = WebProcessRuntime::start(Arc::new(ArcSwap::from(generation)));
|
||||
let session = new_session(limits, Arc::downgrade(&manager));
|
||||
(session, manager)
|
||||
}
|
||||
|
||||
fn session() -> Arc<WebSession> {
|
||||
session_with_limits(WebLimitsConfig::default())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn early_down_waits_without_creating_a_provisional_lane() {
|
||||
let (session, manager) = session_with_manager();
|
||||
let polling = Arc::clone(&session);
|
||||
let poll = tokio::spawn(async move { polling.poll_down_lane(7, 0).await });
|
||||
while session.state.lock().lane_open_waits == 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
assert!(!session.state.lock().carrier_lanes.contains_key(&7));
|
||||
{
|
||||
let mut state = session.state.lock();
|
||||
assert!(insert_carrier_lane(&mut state, 7).is_some());
|
||||
state.closed_streams.insert(7);
|
||||
assert!(session.queue_control_locked(&mut state, FrameType::Close, 7, &[]));
|
||||
}
|
||||
session.lane_open_notify.notify_waiters();
|
||||
let result = tokio::time::timeout(Duration::from_secs(1), poll)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(!result.body.is_empty());
|
||||
assert_eq!(session.state.lock().lane_open_waits, 0);
|
||||
drop(result);
|
||||
session.close();
|
||||
manager.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn early_down_timeout_is_empty_and_releases_its_session_slot() {
|
||||
let (session, manager) = session_with_manager();
|
||||
let polling = Arc::clone(&session);
|
||||
let poll = tokio::spawn(async move { polling.poll_down_lane(7, 0).await });
|
||||
while session.state.lock().lane_open_waits == 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
tokio::time::advance(Duration::from_secs(3)).await;
|
||||
let result = poll.await.unwrap().unwrap();
|
||||
assert!(result.body.is_empty());
|
||||
assert_eq!(result.next_cursor, 0);
|
||||
assert!(!result.lane_closed);
|
||||
assert_eq!(session.state.lock().lane_open_waits, 0);
|
||||
session.close();
|
||||
manager.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn early_down_admission_is_bounded_and_cancellation_safe() {
|
||||
let limits = WebLimitsConfig {
|
||||
max_lane_open_waits_per_session: 2,
|
||||
..WebLimitsConfig::default()
|
||||
};
|
||||
let (session, manager) = session_with_manager_limits(limits);
|
||||
let mut waits = Vec::new();
|
||||
for lane_id in [7, 8] {
|
||||
let polling = Arc::clone(&session);
|
||||
waits.push(tokio::spawn(async move {
|
||||
polling.poll_down_lane(lane_id, 0).await
|
||||
}));
|
||||
}
|
||||
while session.state.lock().lane_open_waits < 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
assert!(matches!(
|
||||
session.poll_down_lane(9, 0).await,
|
||||
Err(ManagerError::Limit)
|
||||
));
|
||||
for wait in waits {
|
||||
wait.abort();
|
||||
let _ = wait.await;
|
||||
}
|
||||
assert_eq!(session.state.lock().lane_open_waits, 0);
|
||||
session.close();
|
||||
manager.shutdown().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_close_wakes_early_down_with_closed_state() {
|
||||
let (session, manager) = session_with_manager();
|
||||
let polling = Arc::clone(&session);
|
||||
let poll = tokio::spawn(async move { polling.poll_down_lane(7, 0).await });
|
||||
while session.state.lock().lane_open_waits == 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
session.close();
|
||||
assert!(matches!(
|
||||
tokio::time::timeout(Duration::from_secs(1), poll)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
Err(ManagerError::Closed)
|
||||
));
|
||||
assert_eq!(session.state.lock().lane_open_waits, 0);
|
||||
manager.shutdown().await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lane_uplink_sequences_are_independent_and_exactly_once() {
|
||||
let session = session();
|
||||
{
|
||||
let mut state = session.state.lock();
|
||||
for lane_id in [51, 52] {
|
||||
state.carrier_lanes.insert(lane_id, CarrierLane::new());
|
||||
state.carrier_lanes.insert(lane_id, CarrierLane::new(u64::from(lane_id)));
|
||||
state.closed_streams.insert(lane_id);
|
||||
}
|
||||
}
|
||||
@@ -75,20 +200,12 @@ fn cross_lane_frame_is_fatal_to_https_lane_session() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn drained_closed_lane_replays_then_signals_completion() {
|
||||
let session = session();
|
||||
let (session, manager) = session_with_manager();
|
||||
{
|
||||
let mut state = session.state.lock();
|
||||
state.carrier_lanes.insert(7, CarrierLane::new());
|
||||
state.carrier_lanes.insert(7, CarrierLane::new(7));
|
||||
state.closed_streams.insert(7);
|
||||
let lane = state.carrier_lanes.get_mut(&7).unwrap();
|
||||
let encoded = frame::encode(FrameType::Close, 7, &[]);
|
||||
lane.pending_frames.push_back(QueuedFrame {
|
||||
encoded: BytesMut::from(encoded.as_ref()),
|
||||
frame_type: FrameType::Close,
|
||||
stream_id: 7,
|
||||
control: true,
|
||||
cost: frame::HEADER_BYTES + QUEUE_ITEM_COST,
|
||||
});
|
||||
assert!(session.queue_control_locked(&mut state, FrameType::Close, 7, &[]));
|
||||
}
|
||||
let first = session.poll_down_lane(7, 0).await.unwrap();
|
||||
let replay = session.poll_down_lane(7, 0).await.unwrap();
|
||||
@@ -97,6 +214,10 @@ async fn drained_closed_lane_replays_then_signals_completion() {
|
||||
let finished = session.poll_down_lane(7, 1).await.unwrap();
|
||||
assert!(finished.body.is_empty());
|
||||
assert!(finished.lane_closed);
|
||||
drop(first);
|
||||
drop(replay);
|
||||
session.close();
|
||||
manager.shutdown().await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -108,7 +229,7 @@ fn tombstone_eviction_releases_lane_budget_and_accepts_late_frames() {
|
||||
let session = session_with_limits(limits);
|
||||
{
|
||||
let mut state = session.state.lock();
|
||||
state.carrier_lanes.insert(7, CarrierLane::new());
|
||||
state.carrier_lanes.insert(7, CarrierLane::new(7));
|
||||
let encoded = frame::encode(FrameType::Close, 7, &[]);
|
||||
let cost = encoded.len() + QUEUE_ITEM_COST;
|
||||
state
|
||||
@@ -128,7 +249,7 @@ fn tombstone_eviction_releases_lane_budget_and_accepts_late_frames() {
|
||||
state.pending_control_bytes = cost;
|
||||
state.pending_control_items = 1;
|
||||
session.remember_closed_locked(&mut state, 7);
|
||||
state.carrier_lanes.insert(8, CarrierLane::new());
|
||||
state.carrier_lanes.insert(8, CarrierLane::new(8));
|
||||
session.remember_closed_locked(&mut state, 8);
|
||||
assert!(!state.carrier_lanes.contains_key(&7));
|
||||
assert_eq!(state.pending_bytes, 0);
|
||||
|
||||
@@ -10,6 +10,20 @@ struct ReleasedQueues {
|
||||
control_items: usize,
|
||||
}
|
||||
|
||||
/// Deferred queue release after manager publication linearizes a supersede.
|
||||
#[must_use]
|
||||
pub(crate) struct CarrierSupersedeCompletion<'a> {
|
||||
session: &'a WebSession,
|
||||
released: ReleasedQueues,
|
||||
}
|
||||
|
||||
impl CarrierSupersedeCompletion<'_> {
|
||||
/// Releases process budgets and signals cancellation after manager locks are dropped.
|
||||
pub(crate) fn finish(self) {
|
||||
self.session.finish_close(self.released, true);
|
||||
}
|
||||
}
|
||||
|
||||
impl WebSession {
|
||||
/// Closes carrier state while relay tasks retain their admission until exit.
|
||||
pub(crate) fn close(&self) {
|
||||
@@ -50,14 +64,13 @@ impl WebSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes manager-owned replacement without unregistering the old session twice.
|
||||
pub(crate) fn finish_carrier_supersede(&self) -> bool {
|
||||
let close_requested = self.state.lock().close_requested;
|
||||
let Some(released) = self.begin_close(true, None) else {
|
||||
return close_requested;
|
||||
};
|
||||
self.finish_close(released, true);
|
||||
close_requested
|
||||
/// Linearizes manager publication against close requests on the old token.
|
||||
pub(crate) fn prepare_carrier_supersede(&self) -> Option<CarrierSupersedeCompletion<'_>> {
|
||||
let released = self.begin_close(true, None)?;
|
||||
Some(CarrierSupersedeCompletion {
|
||||
session: self,
|
||||
released,
|
||||
})
|
||||
}
|
||||
|
||||
/// Waits for all logical-stream tasks after admission has closed.
|
||||
@@ -73,6 +86,13 @@ impl WebSession {
|
||||
|
||||
/// Atomically closes a session only when reconnect grace is still due.
|
||||
pub(crate) fn close_if_due(&self, now: Instant) -> bool {
|
||||
let healthy = {
|
||||
let mut state = self.state.lock();
|
||||
self.carrier_health_ready_locked(&mut state, now)
|
||||
};
|
||||
if healthy {
|
||||
self.finish_carrier_health();
|
||||
}
|
||||
let Some(released) = self.begin_close(false, Some(now)) else {
|
||||
return false;
|
||||
};
|
||||
@@ -82,7 +102,11 @@ impl WebSession {
|
||||
|
||||
fn begin_close(&self, superseded: bool, idle_now: Option<Instant>) -> Option<ReleasedQueues> {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed || (superseded && state.negotiation_phase != SessionNegotiationPhase::Replacing) {
|
||||
if state.closed
|
||||
|| (superseded
|
||||
&& (state.negotiation_phase != SessionNegotiationPhase::Replacing
|
||||
|| state.close_requested))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if let Some(now) = idle_now
|
||||
@@ -111,10 +135,37 @@ impl WebSession {
|
||||
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();
|
||||
if let Some(batch) = state.unacked.take() {
|
||||
batch.lease.detach();
|
||||
self.release_local_locked(&mut state, batch.data_bytes, batch.data_items, false);
|
||||
self.release_local_locked(
|
||||
&mut state,
|
||||
batch.control_bytes,
|
||||
batch.control_items,
|
||||
true,
|
||||
);
|
||||
}
|
||||
let mut lane_data_bytes = 0usize;
|
||||
let mut lane_data_items = 0usize;
|
||||
let mut lane_control_bytes = 0usize;
|
||||
let mut lane_control_items = 0usize;
|
||||
for lane in state.carrier_lanes.values_mut() {
|
||||
lane.notify.notify_waiters();
|
||||
if let Some(batch) = lane.unacked.take() {
|
||||
batch.lease.detach();
|
||||
lane_data_bytes = lane_data_bytes.saturating_add(batch.data_bytes);
|
||||
lane_data_items = lane_data_items.saturating_add(batch.data_items);
|
||||
lane_control_bytes = lane_control_bytes.saturating_add(batch.control_bytes);
|
||||
lane_control_items = lane_control_items.saturating_add(batch.control_items);
|
||||
}
|
||||
}
|
||||
self.release_local_locked(&mut state, lane_data_bytes, lane_data_items, false);
|
||||
self.release_local_locked(
|
||||
&mut state,
|
||||
lane_control_bytes,
|
||||
lane_control_items,
|
||||
true,
|
||||
);
|
||||
state.carrier_lanes.clear();
|
||||
let control_bytes = state.pending_control_bytes;
|
||||
let control_items = state.pending_control_items;
|
||||
@@ -137,6 +188,9 @@ impl WebSession {
|
||||
if self.carrier().is_multiplexed() {
|
||||
self.down_notify.notify_waiters();
|
||||
}
|
||||
if self.carrier().uses_lanes() {
|
||||
self.lane_open_notify.notify_waiters();
|
||||
}
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
manager.release_pending(
|
||||
self.profile_key,
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::uplink::AppliedProgress;
|
||||
use super::{SessionNegotiationPhase, SessionState, WebSession};
|
||||
|
||||
impl WebSession {
|
||||
/// Returns whether accepted carrier progress made this attempt immutable.
|
||||
pub(crate) fn is_carrier_committed(&self) -> bool {
|
||||
self.state.lock().negotiation_phase == SessionNegotiationPhase::Committed
|
||||
}
|
||||
|
||||
/// Rejects mutation after replacement, supersede, or the final chain deadline.
|
||||
pub(super) fn ensure_carrier_active_locked(
|
||||
&self,
|
||||
state: &SessionState,
|
||||
) -> Result<(), crate::web::manager::ManagerError> {
|
||||
if state.negotiation_phase == SessionNegotiationPhase::Uncommitted
|
||||
&& self
|
||||
.carrier_deadline_at
|
||||
.is_some_and(|deadline| Instant::now() >= deadline)
|
||||
{
|
||||
return Err(crate::web::manager::ManagerError::Closed);
|
||||
}
|
||||
match state.negotiation_phase {
|
||||
SessionNegotiationPhase::Uncommitted | SessionNegotiationPhase::Committed => Ok(()),
|
||||
SessionNegotiationPhase::Replacing | SessionNegotiationPhase::Superseded => {
|
||||
Err(crate::web::manager::ManagerError::Closed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Publishes the already-linearized session commit to process state.
|
||||
pub(super) fn finish_carrier_commit(&self) {
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
manager.carrier_committed(
|
||||
self.bootstrap_hash,
|
||||
self.token_hash,
|
||||
self.carrier_attempt,
|
||||
self.selected_carrier,
|
||||
self.carrier_class,
|
||||
self.client_ip,
|
||||
self.trace_identity(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Publishes complete transport-specific health evidence to process state.
|
||||
pub(super) fn finish_carrier_health(&self) {
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
manager.carrier_became_healthy(
|
||||
self.bootstrap_hash,
|
||||
self.token_hash,
|
||||
self.carrier_attempt,
|
||||
self.selected_carrier,
|
||||
self.carrier_class,
|
||||
self.learning_context,
|
||||
self.client_ip,
|
||||
self.trace_identity(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Records accepted OPEN or DATA progress and returns commit and health transitions.
|
||||
pub(super) fn record_uplink_progress_locked(
|
||||
&self,
|
||||
state: &mut SessionState,
|
||||
progress: AppliedProgress,
|
||||
) -> (bool, bool) {
|
||||
if !self.automatic_carrier || !progress.any() {
|
||||
return (false, false);
|
||||
}
|
||||
if self.selected_carrier.uses_websocket() {
|
||||
state.websocket_carrier_active = true;
|
||||
state.websocket_commit_ack_pending = true;
|
||||
} else if progress.accepted_data {
|
||||
state.carrier_health_uplink = true;
|
||||
}
|
||||
let now = Instant::now();
|
||||
let committed = if state.negotiation_phase == SessionNegotiationPhase::Uncommitted {
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_health_due_at = Some(
|
||||
now + Duration::from_secs(self.timeouts.carrier_health_secs),
|
||||
);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
state.carrier_health_activity_at = Some(now);
|
||||
let healthy = self.carrier_health_ready_locked(state, now);
|
||||
(committed, healthy)
|
||||
}
|
||||
|
||||
/// Consumes complete post-commit health evidence at most once.
|
||||
pub(super) fn carrier_health_ready_locked(
|
||||
&self,
|
||||
state: &mut SessionState,
|
||||
now: Instant,
|
||||
) -> bool {
|
||||
if !self.automatic_carrier
|
||||
|| state.negotiation_phase != SessionNegotiationPhase::Committed
|
||||
|| state.carrier_health_reported
|
||||
|| state.carrier_health_due_at.is_none_or(|due| now < due)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let evidence = if state.websocket_carrier_active {
|
||||
state.websocket_probe_claimed
|
||||
&& state.websocket_commit_ack_owner.is_some()
|
||||
&& state.websocket_commit_ack_written
|
||||
&& state.carrier_health_uplink
|
||||
} else {
|
||||
state.carrier_health_uplink
|
||||
&& state.carrier_health_downlink
|
||||
&& state
|
||||
.carrier_health_activity_at
|
||||
.zip(state.carrier_health_due_at)
|
||||
.is_some_and(|(activity, due)| activity >= due)
|
||||
};
|
||||
if evidence {
|
||||
state.carrier_health_reported = true;
|
||||
}
|
||||
evidence
|
||||
}
|
||||
|
||||
/// Returns whether the exact automatic WebSocket owner must receive a commit acknowledgement.
|
||||
pub(crate) fn needs_websocket_commit_ack(&self, owner: u64) -> bool {
|
||||
let state = self.state.lock();
|
||||
!state.closed
|
||||
&& state.websocket_commit_ack_owner == Some(owner)
|
||||
&& state.websocket_commit_ack_pending
|
||||
&& !state.websocket_commit_ack_written
|
||||
}
|
||||
|
||||
/// Records that the commit acknowledgement reached its exact WebSocket owner.
|
||||
pub(crate) fn websocket_commit_ack_written(&self, owner: u64) -> bool {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed
|
||||
|| state.websocket_commit_ack_owner != Some(owner)
|
||||
|| !state.websocket_commit_ack_pending
|
||||
{
|
||||
return false;
|
||||
}
|
||||
state.websocket_commit_ack_written = true;
|
||||
true
|
||||
}
|
||||
|
||||
/// Records validated binary peer progress after the exact WebSocket acknowledgement.
|
||||
pub(crate) fn websocket_peer_after_commit_ack(&self, owner: u64) -> bool {
|
||||
let healthy = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed
|
||||
|| state.websocket_commit_ack_owner != Some(owner)
|
||||
|| !state.websocket_commit_ack_written
|
||||
{
|
||||
return false;
|
||||
}
|
||||
state.carrier_health_uplink = true;
|
||||
let now = Instant::now();
|
||||
state.carrier_health_activity_at = Some(now);
|
||||
self.carrier_health_ready_locked(&mut state, now)
|
||||
};
|
||||
if healthy {
|
||||
self.finish_carrier_health();
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::config::{
|
||||
WebCarrier, WebLimitsConfig, WebRuntimeProfile, WebSecretMode, WebTimeoutsConfig,
|
||||
};
|
||||
use crate::web::manager::{CarrierClientClass, WebProcessRuntime};
|
||||
|
||||
fn session(carrier: WebCarrier, deadline: Instant) -> Arc<WebSession> {
|
||||
let profile = Arc::new(WebRuntimeProfile {
|
||||
host: "proxy.example.com".to_string(),
|
||||
public_addr: SocketAddr::from(([203, 0, 113, 10], 443)),
|
||||
user: "alice".to_string(),
|
||||
secret_mode: WebSecretMode::Plain,
|
||||
carrier,
|
||||
carrier_negotiation_enabled: true,
|
||||
carrier_learning: false,
|
||||
carriers: Arc::from([carrier]),
|
||||
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
|
||||
capability: [0; 32],
|
||||
key_fingerprint: "0000000000000000".to_string(),
|
||||
max_sessions: 1,
|
||||
max_streams: 1,
|
||||
max_streams_per_session: 1,
|
||||
});
|
||||
WebSession::new(
|
||||
std::sync::Weak::<WebProcessRuntime>::new(),
|
||||
[1; 32],
|
||||
"192.0.2.10".parse().unwrap(),
|
||||
1,
|
||||
profile,
|
||||
[2; 32],
|
||||
carrier,
|
||||
1,
|
||||
[3; 32],
|
||||
Some(deadline),
|
||||
CarrierClientClass::Bridge,
|
||||
None,
|
||||
true,
|
||||
WebLimitsConfig::default(),
|
||||
WebTimeoutsConfig::default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_deadline_refuses_uncommitted_progress() {
|
||||
let session = session(WebCarrier::Https, Instant::now() - Duration::from_secs(1));
|
||||
let state = session.state.lock();
|
||||
assert_eq!(
|
||||
session.ensure_carrier_active_locked(&state),
|
||||
Err(crate::web::manager::ManagerError::Closed)
|
||||
);
|
||||
assert!(matches!(
|
||||
state.negotiation_phase,
|
||||
SessionNegotiationPhase::Uncommitted
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_health_requires_authenticated_activity_after_the_window() {
|
||||
let session = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
let now = Instant::now();
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.carrier_health_uplink = true;
|
||||
state.carrier_health_downlink = true;
|
||||
state.carrier_health_activity_at = Some(now - Duration::from_secs(2));
|
||||
assert!(!session.carrier_health_ready_locked(&mut state, now));
|
||||
state.carrier_health_activity_at = Some(now);
|
||||
assert!(session.carrier_health_ready_locked(&mut state, now));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn websocket_health_requires_the_exact_live_probe_owner() {
|
||||
let session = session(
|
||||
WebCarrier::Websocket,
|
||||
Instant::now() + Duration::from_secs(60),
|
||||
);
|
||||
let now = Instant::now();
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.websocket_carrier_active = true;
|
||||
state.websocket_commit_ack_owner = Some(7);
|
||||
state.websocket_commit_ack_written = true;
|
||||
state.carrier_health_uplink = true;
|
||||
assert!(!session.carrier_health_ready_locked(&mut state, now));
|
||||
state.websocket_probe_claimed = true;
|
||||
assert!(session.carrier_health_ready_locked(&mut state, now));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_and_supersede_have_one_session_lock_winner() {
|
||||
let committed = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
{
|
||||
let mut state = committed.state.lock();
|
||||
assert!(
|
||||
committed
|
||||
.record_uplink_progress_locked(
|
||||
&mut state,
|
||||
AppliedProgress {
|
||||
accepted_open: true,
|
||||
accepted_data: true,
|
||||
},
|
||||
)
|
||||
.0
|
||||
);
|
||||
}
|
||||
assert!(!committed.begin_carrier_supersede());
|
||||
|
||||
let replacing = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
assert!(replacing.begin_carrier_supersede());
|
||||
assert_eq!(
|
||||
replacing.ensure_carrier_active_locked(&replacing.state.lock()),
|
||||
Err(crate::web::manager::ManagerError::Closed)
|
||||
);
|
||||
replacing.cancel_carrier_supersede();
|
||||
assert!(
|
||||
replacing
|
||||
.ensure_carrier_active_locked(&replacing.state.lock())
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,18 @@ use super::{
|
||||
use crate::web::frame::{self, Frame, FrameType};
|
||||
use crate::web::manager::{ManagerError, TokenHash};
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(super) struct AppliedProgress {
|
||||
pub(super) accepted_open: bool,
|
||||
pub(super) accepted_data: bool,
|
||||
}
|
||||
|
||||
impl AppliedProgress {
|
||||
pub(super) fn any(self) -> bool {
|
||||
self.accepted_open || self.accepted_data
|
||||
}
|
||||
}
|
||||
|
||||
impl WebSession {
|
||||
/// Applies one exactly-once uplink batch.
|
||||
pub(crate) fn process_up(
|
||||
@@ -22,6 +34,25 @@ impl WebSession {
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
) -> Result<u64, ManagerError> {
|
||||
self.process_up_inner(sequence, body)
|
||||
.map(|(acknowledged, _)| acknowledged)
|
||||
}
|
||||
|
||||
/// Applies one WebSocket uplink batch and reports actual carrier progress.
|
||||
pub(crate) fn process_websocket_multiplex(
|
||||
self: &Arc<Self>,
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
) -> Result<bool, ManagerError> {
|
||||
self.process_up_inner(sequence, body)
|
||||
.map(|(_, progress)| progress)
|
||||
}
|
||||
|
||||
fn process_up_inner(
|
||||
self: &Arc<Self>,
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
) -> Result<(u64, bool), ManagerError> {
|
||||
if !self.carrier().is_multiplexed() {
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
@@ -49,11 +80,9 @@ 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 mut healthy = false;
|
||||
let result = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed {
|
||||
@@ -63,7 +92,7 @@ impl WebSession {
|
||||
state.last_activity = Instant::now();
|
||||
if sequence == state.last_up_sequence && sequence != 0 {
|
||||
return if bool::from(state.last_up_digest.ct_eq(&digest)) {
|
||||
Ok(sequence)
|
||||
Ok((sequence, false))
|
||||
} else {
|
||||
drop(state);
|
||||
self.close();
|
||||
@@ -91,6 +120,7 @@ impl WebSession {
|
||||
}
|
||||
let mut unused_bytes = reserve_bytes;
|
||||
let mut unused_items = reserve_items;
|
||||
let mut progress = AppliedProgress::default();
|
||||
let applied = self.apply_batch_locked(
|
||||
&mut state,
|
||||
&frames,
|
||||
@@ -98,6 +128,7 @@ impl WebSession {
|
||||
&mut None,
|
||||
&mut unused_bytes,
|
||||
&mut unused_items,
|
||||
&mut progress,
|
||||
);
|
||||
self.release_locked(&mut state, unused_bytes, unused_items, false);
|
||||
if !applied {
|
||||
@@ -105,8 +136,9 @@ impl WebSession {
|
||||
} else {
|
||||
state.last_up_sequence = sequence;
|
||||
state.last_up_digest = digest;
|
||||
committed = self.commit_carrier_locked(&mut state, progress);
|
||||
Ok(sequence)
|
||||
(committed, healthy) =
|
||||
self.record_uplink_progress_locked(&mut state, progress);
|
||||
Ok((sequence, progress.any()))
|
||||
}
|
||||
};
|
||||
if matches!(result, Err(ManagerError::Backpressure)) {
|
||||
@@ -120,6 +152,9 @@ impl WebSession {
|
||||
if committed {
|
||||
self.finish_carrier_commit();
|
||||
}
|
||||
if healthy {
|
||||
self.finish_carrier_health();
|
||||
}
|
||||
for completion in opened {
|
||||
self.spawn_stream(completion, false);
|
||||
}
|
||||
@@ -137,6 +172,7 @@ impl WebSession {
|
||||
reserved_open: &mut Option<(u32, u16)>,
|
||||
unused_bytes: &mut usize,
|
||||
unused_items: &mut usize,
|
||||
progress: &mut AppliedProgress,
|
||||
) -> bool {
|
||||
for value in frames {
|
||||
if value.stream_id == 0 {
|
||||
@@ -186,6 +222,7 @@ impl WebSession {
|
||||
write_waker: None,
|
||||
},
|
||||
);
|
||||
progress.accepted_open = true;
|
||||
opened.push(self.own_stream_task(stream, peer_port));
|
||||
}
|
||||
FrameType::Data if !was_closed => {
|
||||
@@ -197,6 +234,7 @@ impl WebSession {
|
||||
bytes: Bytes::copy_from_slice(value.payload),
|
||||
offset: 0,
|
||||
});
|
||||
progress.accepted_data = true;
|
||||
*unused_bytes =
|
||||
unused_bytes.saturating_sub(value.payload.len() + QUEUE_ITEM_COST);
|
||||
*unused_items = unused_items.saturating_sub(1);
|
||||
@@ -409,6 +447,9 @@ mod tests {
|
||||
1,
|
||||
[3; 32],
|
||||
None,
|
||||
crate::web::manager::CarrierClientClass::Legacy,
|
||||
None,
|
||||
false,
|
||||
WebLimitsConfig::default(),
|
||||
WebTimeoutsConfig::default(),
|
||||
)
|
||||
@@ -445,6 +486,7 @@ mod tests {
|
||||
state.streams.insert(
|
||||
1,
|
||||
StreamState {
|
||||
instance: 1,
|
||||
inbound: VecDeque::new(),
|
||||
receive_window: frame::INITIAL_STREAM_WINDOW,
|
||||
send_credit: u64::from(frame::INITIAL_STREAM_WINDOW),
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::time::Instant;
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use super::uplink::{inbound_reservation, validate_batch};
|
||||
use super::uplink::{AppliedProgress, inbound_reservation, validate_batch};
|
||||
use super::{PendingClass, WebSession, inbound_queue_cost, insert_carrier_lane};
|
||||
use crate::config::WebCarrier;
|
||||
use crate::web::frame;
|
||||
@@ -17,6 +17,43 @@ pub(crate) struct WebSocketLaneReservation {
|
||||
transferred: bool,
|
||||
}
|
||||
|
||||
/// Session-wide ownership of the only automatic WebSocket carrier probe.
|
||||
pub(crate) struct WebSocketProbeReservation {
|
||||
session: Arc<WebSession>,
|
||||
owner: Option<u64>,
|
||||
}
|
||||
|
||||
impl WebSocketProbeReservation {
|
||||
/// Binds the admitted process connection to the future commit acknowledgement.
|
||||
pub(crate) fn bind(&mut self, owner: u64) -> Result<(), ManagerError> {
|
||||
let mut state = self.session.state.lock();
|
||||
if state.closed
|
||||
|| !state.websocket_probe_claimed
|
||||
|| state.websocket_commit_ack_owner.is_some()
|
||||
{
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
state.websocket_commit_ack_owner = Some(owner);
|
||||
self.owner = Some(owner);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WebSocketProbeReservation {
|
||||
fn drop(&mut self) {
|
||||
let mut state = self.session.state.lock();
|
||||
state.websocket_probe_claimed = false;
|
||||
if state.websocket_commit_ack_owner == self.owner {
|
||||
state.websocket_commit_ack_owner = None;
|
||||
if !state.carrier_health_reported {
|
||||
state.websocket_commit_ack_written = false;
|
||||
state.carrier_health_uplink = false;
|
||||
state.carrier_health_activity_at = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebSocketLaneReservation {
|
||||
/// Returns the logical stream owned by this connection.
|
||||
pub(crate) fn lane_id(&self) -> u32 {
|
||||
@@ -46,6 +83,42 @@ impl Drop for WebSocketLaneReservation {
|
||||
}
|
||||
|
||||
impl WebSession {
|
||||
/// Reserves the only automatic WebSocket probe before any HTTP 101 response.
|
||||
pub(crate) fn reserve_websocket_probe(
|
||||
self: &Arc<Self>,
|
||||
acknowledge_commit: bool,
|
||||
) -> Result<Option<WebSocketProbeReservation>, ManagerError> {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
self.ensure_carrier_active_locked(&state)?;
|
||||
if !self.automatic_carrier {
|
||||
return if acknowledge_commit {
|
||||
Err(ManagerError::Protocol)
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
}
|
||||
match state.negotiation_phase {
|
||||
super::SessionNegotiationPhase::Uncommitted if acknowledge_commit => {
|
||||
if state.websocket_probe_claimed || state.websocket_commit_ack_owner.is_some() {
|
||||
return Err(ManagerError::Concurrent);
|
||||
}
|
||||
state.websocket_probe_claimed = true;
|
||||
Ok(Some(WebSocketProbeReservation {
|
||||
session: Arc::clone(self),
|
||||
owner: None,
|
||||
}))
|
||||
}
|
||||
super::SessionNegotiationPhase::Committed if !acknowledge_commit => Ok(None),
|
||||
super::SessionNegotiationPhase::Committed => Err(ManagerError::Committed),
|
||||
super::SessionNegotiationPhase::Uncommitted => Err(ManagerError::Protocol),
|
||||
super::SessionNegotiationPhase::Replacing
|
||||
| super::SessionNegotiationPhase::Superseded => Err(ManagerError::Closed),
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquires stream quota and tuple ownership before a lane returns HTTP 101.
|
||||
pub(crate) fn reserve_websocket_lane(
|
||||
self: &Arc<Self>,
|
||||
@@ -63,6 +136,7 @@ impl WebSession {
|
||||
}
|
||||
if state.active_peer_ports.len() >= self.profile.max_streams_per_session
|
||||
|| state.streams.contains_key(&lane_id)
|
||||
|| state.closing_streams.contains_key(&lane_id)
|
||||
|| state.closed_streams.contains(&lane_id)
|
||||
|| state.websocket_lane_reservations.contains_key(&lane_id)
|
||||
{
|
||||
@@ -116,7 +190,7 @@ impl WebSession {
|
||||
reservation: &mut WebSocketLaneReservation,
|
||||
sequence: u64,
|
||||
body: &[u8],
|
||||
) -> Result<(), ManagerError> {
|
||||
) -> Result<bool, ManagerError> {
|
||||
if !Arc::ptr_eq(self, &reservation.session)
|
||||
|| reservation.lane_id == 0
|
||||
|| reservation.lane_id > frame::MAX_STREAM_ID
|
||||
@@ -133,11 +207,9 @@ 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 mut healthy = false;
|
||||
let result = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed {
|
||||
@@ -179,6 +251,7 @@ impl WebSession {
|
||||
}
|
||||
let mut unused_bytes = reserve_bytes;
|
||||
let mut unused_items = reserve_items;
|
||||
let mut progress = AppliedProgress::default();
|
||||
let mut reserved_open =
|
||||
(!reservation.transferred).then_some((lane_id, reservation.peer_port));
|
||||
let applied = self.apply_batch_locked(
|
||||
@@ -188,6 +261,7 @@ impl WebSession {
|
||||
&mut reserved_open,
|
||||
&mut unused_bytes,
|
||||
&mut unused_items,
|
||||
&mut progress,
|
||||
);
|
||||
self.release_locked(&mut state, unused_bytes, unused_items, false);
|
||||
if let Some(lane) = state.carrier_lanes.get_mut(&lane_id) {
|
||||
@@ -199,14 +273,20 @@ impl WebSession {
|
||||
}
|
||||
state.last_activity = Instant::now();
|
||||
if applied {
|
||||
committed = self.commit_carrier_locked(&mut state, progress);
|
||||
(committed, healthy) =
|
||||
self.record_uplink_progress_locked(&mut state, progress);
|
||||
}
|
||||
applied.then_some(()).ok_or(ManagerError::Protocol)
|
||||
applied
|
||||
.then_some(progress.any())
|
||||
.ok_or(ManagerError::Protocol)
|
||||
};
|
||||
result?;
|
||||
let progressed = result?;
|
||||
if committed {
|
||||
self.finish_carrier_commit();
|
||||
}
|
||||
if healthy {
|
||||
self.finish_carrier_health();
|
||||
}
|
||||
for completion in opened {
|
||||
if completion.stream.id != lane_id || completion.peer_port != reservation.peer_port {
|
||||
return Err(ManagerError::Protocol);
|
||||
@@ -222,7 +302,7 @@ impl WebSession {
|
||||
if let Some(manager) = self.manager.upgrade() {
|
||||
manager.record_up(body.len());
|
||||
}
|
||||
Ok(())
|
||||
Ok(progressed)
|
||||
}
|
||||
|
||||
/// Ends one failed or disconnected lane without closing its parent session.
|
||||
|
||||
@@ -68,6 +68,9 @@ fn runtime(admission: bool) -> TestRuntime {
|
||||
1,
|
||||
[9; 32],
|
||||
None,
|
||||
crate::web::manager::CarrierClientClass::Legacy,
|
||||
None,
|
||||
false,
|
||||
limits,
|
||||
timeouts,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user