WEB Carrier: https-lanes

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-23 09:04:53 +03:00
parent 79ad4cb541
commit 90c0d65e1b
18 changed files with 1063 additions and 74 deletions
+5 -7
View File
@@ -7,7 +7,7 @@ use crate::web::frame::FrameType;
use crate::web::stream::WebLogicalStream;
use crate::proxy::shared_state::ConntrackClosePolicy;
use super::{WebSession, inbound_queue_cost, remember_closed};
use super::{WebSession, inbound_queue_cost};
impl WebSession {
/// Starts one owned inner handshake and relay task for an admitted stream.
@@ -65,11 +65,7 @@ impl WebSession {
let queued = state.streams.remove(&stream_id).map(|stream| {
let (bytes, items) = inbound_queue_cost(&stream.inbound);
self.release_locked(&mut state, bytes, items, false);
remember_closed(
&mut state,
stream_id,
self.limits.max_tombstones_per_session,
);
self.remember_closed_locked(&mut state, stream_id);
self.queue_control_locked(&mut state, FrameType::Close, stream_id, &[])
});
(queued, reserved)
@@ -88,7 +84,9 @@ impl WebSession {
if !queued {
self.close();
}
self.down_notify.notify_waiters();
if self.carrier() == crate::config::WebCarrier::Https {
self.down_notify.notify_waiters();
}
}
}
}
+29
View File
@@ -6,12 +6,16 @@ use super::{
DownBatch, PendingClass, PollResult, QUEUE_ITEM_COST, QueuedFrame, SessionState,
WebSession,
};
use crate::config::WebCarrier;
use crate::web::frame::{self, FrameType};
use crate::web::manager::ManagerError;
impl WebSession {
/// Polls pending downlink frames with cursor replay and newest-poll-wins semantics.
pub(crate) async fn poll_down(&self, cursor: u64) -> Result<PollResult, ManagerError> {
if self.carrier() != WebCarrier::Https {
return Err(ManagerError::Protocol);
}
let epoch = {
let mut state = self.state.lock();
if state.closed {
@@ -23,6 +27,7 @@ impl WebSession {
return Ok(PollResult {
body: unacked.body.clone(),
next_cursor: unacked.next_cursor,
lane_closed: false,
});
}
if cursor != unacked.next_cursor {
@@ -51,6 +56,7 @@ impl WebSession {
return Ok(PollResult {
body: Bytes::new(),
next_cursor: cursor,
lane_closed: false,
});
}
if !state.pending_frames.is_empty() {
@@ -65,6 +71,7 @@ impl WebSession {
let result = PollResult {
body: batch.body.clone(),
next_cursor: batch.next_cursor,
lane_closed: false,
};
if let Some(manager) = self.manager.upgrade() {
manager.record_down(result.body.len());
@@ -89,6 +96,7 @@ impl WebSession {
Ok(PollResult {
body: Bytes::new(),
next_cursor: cursor,
lane_closed: false,
})
}
}
@@ -210,6 +218,14 @@ impl WebSession {
if amount == 0 {
return true;
}
if self.carrier() == WebCarrier::HttpsLanes {
return self.queue_control_locked(
state,
FrameType::Window,
stream_id,
&frame::window_payload(amount),
);
}
if let Some(index) = state.pending_windows.get(&stream_id).copied()
&& let Some(queued) = state.pending_frames.get_mut(index)
{
@@ -251,6 +267,9 @@ impl WebSession {
stream_id: u32,
payload: &[u8],
) -> bool {
if self.carrier() == WebCarrier::HttpsLanes {
return self.queue_frame_locked(state, FrameType::Data, stream_id, payload, false);
}
let can_coalesce = state.pending_frames.back().is_some_and(|last| {
last.frame_type == FrameType::Data
&& last.stream_id == stream_id
@@ -281,6 +300,15 @@ impl WebSession {
payload: &[u8],
control: bool,
) -> bool {
if self.carrier() == WebCarrier::HttpsLanes {
return self.queue_lane_frame_locked(
state,
frame_type,
stream_id,
payload,
control,
);
}
let cost = frame::HEADER_BYTES + payload.len() + QUEUE_ITEM_COST;
let class = if control {
PendingClass::Control
@@ -408,6 +436,7 @@ mod tests {
public_addr: SocketAddr::from(([203, 0, 113, 10], 443)),
user: "alice".to_string(),
secret_mode: WebSecretMode::Plain,
carrier: WebCarrier::Https,
capability: [0; 32],
max_sessions: 1,
max_streams: 1,
+525
View File
@@ -0,0 +1,525 @@
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::{
CarrierLane, DownBatch, 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};
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 result = {
let mut state = self.state.lock();
if state.closed {
return Err(ManagerError::Closed);
}
state.last_activity = Instant::now();
if !state.carrier_lanes.contains_key(&lane_id) {
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);
}
state.carrier_lanes.insert(lane_id, CarrierLane::new());
}
let lane = state
.carrier_lanes
.get_mut(&lane_id)
.ok_or(ManagerError::Protocol)?;
if sequence == lane.last_up_sequence && sequence != 0 {
return if bool::from(lane.last_up_digest.ct_eq(&digest)) {
Ok(sequence)
} else {
drop(state);
self.close();
Err(ManagerError::Protocol)
};
}
if sequence == 0 || sequence != lane.last_up_sequence.saturating_add(1) {
drop(state);
self.close();
return Err(ManagerError::Protocol);
}
if lane.up_active {
return Err(ManagerError::Concurrent);
}
lane.up_active = true;
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);
}
let mut unused_bytes = reserve_bytes;
let mut unused_items = reserve_items;
let applied = self.apply_batch_locked(
&mut state,
&frames,
&mut opened,
&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;
}
}
applied.then_some(sequence).ok_or(ManagerError::Closed)
};
if matches!(result, Err(ManagerError::Backpressure)) {
return result;
}
if result.is_err() {
self.close();
for (_, peer_port) in opened {
self.release_stream_reservation(peer_port);
}
return result;
}
for (stream_id, peer_port) in opened {
self.spawn_stream(stream_id, peer_port);
}
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,
lane_id: u32,
cursor: u64,
) -> Result<PollResult, ManagerError> {
if self.carrier() != WebCarrier::HttpsLanes || lane_id > frame::MAX_STREAM_ID {
return Err(ManagerError::Protocol);
}
let (epoch, notify) = {
let mut state = self.state.lock();
if state.closed {
return Err(ManagerError::Closed);
}
let acknowledged = {
let Some(lane) = state.carrier_lanes.get_mut(&lane_id) else {
return Err(ManagerError::Protocol);
};
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 {
drop(state);
self.close();
return Err(ManagerError::Protocol);
}
lane.unacked.take()
} else {
if cursor != lane.down_cursor {
drop(state);
self.close();
return Err(ManagerError::Protocol);
}
None
}
};
if let Some(batch) = acknowledged {
self.release_locked(&mut state, batch.data_bytes, batch.data_items, false);
self.release_locked(
&mut state,
batch.control_bytes,
batch.control_items,
true,
);
if let Some(stream) = state.streams.get_mut(&lane_id)
&& let Some(waker) = stream.write_waker.take()
{
waker.wake();
}
}
state.last_activity = Instant::now();
let lane = state
.carrier_lanes
.get_mut(&lane_id)
.ok_or(ManagerError::Protocol)?;
lane.down_epoch = lane.down_epoch.wrapping_add(1).max(1);
(lane.down_epoch, Arc::clone(&lane.notify))
};
notify.notify_waiters();
let deadline = Duration::from_secs(self.timeouts.long_poll_secs);
let poll = async {
loop {
let notified = notify.notified();
{
let mut state = self.state.lock();
if state.closed {
return Err(ManagerError::Closed);
}
let Some(lane) = state.carrier_lanes.get_mut(&lane_id) else {
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,
lane_closed: false,
});
}
if !lane.pending_frames.is_empty() {
let batch = match take_lane_down_batch(&self.limits, lane, cursor) {
Ok(batch) => batch,
Err(error) => {
drop(state);
self.close();
return Err(error);
}
};
let result = PollResult {
body: batch.body.clone(),
next_cursor: batch.next_cursor,
lane_closed: false,
};
lane.unacked = Some(batch);
drop(state);
if let Some(manager) = self.manager.upgrade() {
manager.record_down(result.body.len());
}
return Ok(result);
}
if lane_id != 0
&& !state.streams.contains_key(&lane_id)
&& state.closed_streams.contains(&lane_id)
{
return Ok(PollResult {
body: Bytes::new(),
next_cursor: cursor,
lane_closed: true,
});
}
}
notified.await;
}
};
match tokio::time::timeout(deadline, poll).await {
Ok(result) => result,
Err(_) => {
let mut state = self.state.lock();
if state.closed {
return Err(ManagerError::Closed);
}
if !state.carrier_lanes.contains_key(&lane_id) {
return Ok(PollResult {
body: Bytes::new(),
next_cursor: cursor,
lane_closed: true,
});
}
if lane_id != 0
&& !state.streams.contains_key(&lane_id)
&& state.closed_streams.contains(&lane_id)
{
return Ok(PollResult {
body: Bytes::new(),
next_cursor: cursor,
lane_closed: true,
});
}
if state
.carrier_lanes
.get(&lane_id)
.is_some_and(|lane| lane.down_epoch == epoch)
{
state.last_activity = Instant::now();
}
Ok(PollResult {
body: Bytes::new(),
next_cursor: cursor,
lane_closed: false,
})
}
}
}
pub(super) fn queue_lane_frame_locked(
&self,
state: &mut SessionState,
frame_type: FrameType,
stream_id: u32,
payload: &[u8],
control: bool,
) -> bool {
if !state.carrier_lanes.contains_key(&stream_id) {
return false;
}
if frame_type == FrameType::Window {
let coalesced = state.carrier_lanes.get(&stream_id).and_then(|lane| {
let index = lane.pending_windows.get(&stream_id).copied()?;
let queued = lane.pending_frames.get(index)?;
let previous = u32::from_be_bytes(
queued.encoded[frame::HEADER_BYTES..frame::HEADER_BYTES + 4]
.try_into()
.unwrap_or([0; 4]),
);
previous
.checked_add(frame::window_amount(payload).unwrap_or(0))
.map(|total| (index, total))
});
if let Some((index, total)) = coalesced
&& let Some(lane) = state.carrier_lanes.get_mut(&stream_id)
&& let Some(queued) = lane.pending_frames.get_mut(index)
{
queued.encoded[frame::HEADER_BYTES..frame::HEADER_BYTES + 4]
.copy_from_slice(&total.to_be_bytes());
lane.notify.notify_waiters();
return true;
}
}
let can_coalesce = frame_type == FrameType::Data
&& state
.carrier_lanes
.get(&stream_id)
.and_then(|lane| lane.pending_frames.back())
.is_some_and(|last| {
last.frame_type == FrameType::Data
&& last.stream_id == stream_id
&& last.encoded.len() - frame::HEADER_BYTES + payload.len()
<= self.limits.max_frame_payload_bytes
});
if can_coalesce {
if !self.reserve_locked(state, payload.len(), 0, PendingClass::Downlink) {
return false;
}
let Some(lane) = state.carrier_lanes.get_mut(&stream_id) else {
self.release_locked(state, payload.len(), 0, false);
return false;
};
let Some(last) = lane.pending_frames.back_mut() else {
self.release_locked(state, payload.len(), 0, false);
return false;
};
last.encoded.extend_from_slice(payload);
last.cost += payload.len();
let payload_len = (last.encoded.len() - frame::HEADER_BYTES) as u32;
last.encoded[4..8].copy_from_slice(&payload_len.to_be_bytes());
lane.notify.notify_waiters();
return true;
}
let cost = frame::HEADER_BYTES + payload.len() + QUEUE_ITEM_COST;
let class = if control {
PendingClass::Control
} else {
PendingClass::Downlink
};
if !self.reserve_locked(state, cost, 1, class) {
return false;
}
let mut encoded = BytesMut::with_capacity(frame::HEADER_BYTES + payload.len());
encoded.put_u8(frame_type as u8);
encoded.put_u8((stream_id >> 16) as u8);
encoded.put_u8((stream_id >> 8) as u8);
encoded.put_u8(stream_id as u8);
encoded.put_u32(payload.len() as u32);
encoded.extend_from_slice(payload);
let Some(lane) = state.carrier_lanes.get_mut(&stream_id) else {
self.release_locked(state, cost, 1, control);
return false;
};
let index = lane.pending_frames.len();
lane.pending_frames.push_back(QueuedFrame {
encoded,
frame_type,
stream_id,
control,
cost,
});
if frame_type == FrameType::Window {
lane.pending_windows.insert(stream_id, index);
}
lane.notify.notify_waiters();
true
}
pub(super) fn remember_closed_locked(&self, state: &mut SessionState, stream_id: u32) {
let evicted = remember_closed(
state,
stream_id,
self.limits.max_tombstones_per_session,
);
if self.carrier() != WebCarrier::HttpsLanes {
return;
}
if let Some(evicted) = evicted {
self.release_lane_locked(state, evicted);
}
if let Some(lane) = state.carrier_lanes.get(&stream_id) {
lane.notify.notify_waiters();
}
}
fn release_lane_locked(&self, state: &mut SessionState, lane_id: u32) {
let Some(mut lane) = state.carrier_lanes.remove(&lane_id) else {
return;
};
lane.notify.notify_waiters();
let mut data_bytes = 0usize;
let mut data_items = 0usize;
let mut control_bytes = 0usize;
let mut control_items = 0usize;
for queued in lane.pending_frames.drain(..) {
if queued.control {
control_bytes = control_bytes.saturating_add(queued.cost);
control_items = control_items.saturating_add(1);
} else {
data_bytes = data_bytes.saturating_add(queued.cost);
data_items = data_items.saturating_add(1);
}
}
if let Some(batch) = lane.unacked.take() {
data_bytes = data_bytes.saturating_add(batch.data_bytes);
data_items = data_items.saturating_add(batch.data_items);
control_bytes = control_bytes.saturating_add(batch.control_bytes);
control_items = control_items.saturating_add(batch.control_items);
}
self.release_locked(state, data_bytes, data_items, false);
self.release_locked(state, control_bytes, control_items, true);
}
}
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(
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 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;
Ok(DownBatch {
body: body.freeze(),
base_cursor: cursor,
next_cursor,
data_bytes,
data_items,
control_bytes,
control_items,
})
}
// Lane-specific protocol, replay, and lifecycle tests.
#[cfg(test)]
mod tests;
+129
View File
@@ -0,0 +1,129 @@
use std::net::SocketAddr;
use std::sync::Arc;
use bytes::BytesMut;
use super::*;
use crate::config::{
WebRuntimeProfile, WebSecretMode, WebTimeoutsConfig,
};
use crate::web::manager::WebProcessRuntime;
fn session_with_limits(limits: WebLimitsConfig) -> 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::HttpsLanes,
capability: [0; 32],
max_sessions: 1,
max_streams: 2,
max_streams_per_session: 2,
});
WebSession::new(
std::sync::Weak::<WebProcessRuntime>::new(),
[1; 32],
"192.0.2.10".parse().unwrap(),
profile,
[2; 32],
limits,
WebTimeoutsConfig::default(),
)
}
fn session() -> Arc<WebSession> {
session_with_limits(WebLimitsConfig::default())
}
#[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.closed_streams.insert(lane_id);
}
}
let first = frame::encode(FrameType::Data, 51, b"first");
let second = frame::encode(FrameType::Data, 52, b"second");
assert_eq!(session.process_up_lane(51, 1, &first), Ok(1));
assert_eq!(session.process_up_lane(52, 1, &second), Ok(1));
assert_eq!(session.process_up_lane(51, 1, &first), Ok(1));
assert_eq!(session.state.lock().carrier_lanes[&52].last_up_sequence, 1);
}
#[test]
fn cross_lane_frame_is_fatal_to_https_lane_session() {
let session = session();
let body = frame::encode(FrameType::Data, 52, b"wrong lane");
assert_eq!(
session.process_up_lane(51, 1, &body),
Err(ManagerError::Protocol)
);
assert!(session.state.lock().closed);
}
#[tokio::test]
async fn drained_closed_lane_replays_then_signals_completion() {
let session = session();
{
let mut state = session.state.lock();
state.carrier_lanes.insert(7, CarrierLane::new());
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,
});
}
let first = session.poll_down_lane(7, 0).await.unwrap();
let replay = session.poll_down_lane(7, 0).await.unwrap();
assert_eq!(first.body, replay.body);
assert!(!replay.lane_closed);
let finished = session.poll_down_lane(7, 1).await.unwrap();
assert!(finished.body.is_empty());
assert!(finished.lane_closed);
}
#[test]
fn tombstone_eviction_releases_lane_budget_and_accepts_late_frames() {
let limits = WebLimitsConfig {
max_tombstones_per_session: 1,
..WebLimitsConfig::default()
};
let session = session_with_limits(limits);
{
let mut state = session.state.lock();
state.carrier_lanes.insert(7, CarrierLane::new());
let encoded = frame::encode(FrameType::Close, 7, &[]);
let cost = encoded.len() + QUEUE_ITEM_COST;
state.carrier_lanes.get_mut(&7).unwrap().pending_frames.push_back(
QueuedFrame {
encoded: BytesMut::from(encoded.as_ref()),
frame_type: FrameType::Close,
stream_id: 7,
control: true,
cost,
},
);
state.pending_bytes = cost;
state.pending_items = 1;
state.pending_control_bytes = cost;
state.pending_control_items = 1;
session.remember_closed_locked(&mut state, 7);
state.carrier_lanes.insert(8, CarrierLane::new());
session.remember_closed_locked(&mut state, 8);
assert!(!state.carrier_lanes.contains_key(&7));
assert_eq!(state.pending_bytes, 0);
assert_eq!(state.pending_items, 0);
}
let late = frame::encode(FrameType::Data, 7, b"late");
assert_eq!(session.process_up_lane(7, 7, &late), Ok(7));
assert!(!session.state.lock().closed);
}
+14 -14
View File
@@ -9,8 +9,9 @@ use subtle::ConstantTimeEq;
use super::{
InboundChunk, PendingClass, QUEUE_ITEM_COST, SessionState, StreamState, WebSession,
inbound_queue_cost, remember_closed,
inbound_queue_cost,
};
use crate::config::WebCarrier;
use crate::web::frame::{self, Frame, FrameType};
use crate::web::manager::{ManagerError, TokenHash};
@@ -21,6 +22,9 @@ impl WebSession {
sequence: u64,
body: &[u8],
) -> Result<u64, ManagerError> {
if self.carrier() != WebCarrier::Https {
return Err(ManagerError::Protocol);
}
if self
.up_active
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
@@ -117,7 +121,7 @@ impl WebSession {
result
}
fn apply_batch_locked(
pub(super) fn apply_batch_locked(
&self,
state: &mut SessionState,
frames: &[Frame<'_>],
@@ -133,11 +137,7 @@ impl WebSession {
match value.frame_type {
FrameType::Open => {
let Some(peer_port) = self.reserve_stream_locked(state) else {
remember_closed(
state,
value.stream_id,
self.limits.max_tombstones_per_session,
);
self.remember_closed_locked(state, value.stream_id);
if !self.queue_control_locked(
state,
FrameType::Close,
@@ -195,11 +195,7 @@ impl WebSession {
};
let (bytes, items) = inbound_queue_cost(&stream.inbound);
self.release_locked(state, bytes, items, false);
remember_closed(
state,
value.stream_id,
self.limits.max_tombstones_per_session,
);
self.remember_closed_locked(state, value.stream_id);
if let Some(waker) = stream.read_waker {
waker.wake();
}
@@ -246,7 +242,7 @@ impl Drop for UplinkGuard<'_> {
}
}
fn validate_batch(state: &SessionState, frames: &[Frame<'_>]) -> bool {
pub(super) fn validate_batch(state: &SessionState, frames: &[Frame<'_>]) -> bool {
let mut live = state
.streams
.iter()
@@ -312,7 +308,10 @@ fn validate_batch(state: &SessionState, frames: &[Frame<'_>]) -> bool {
true
}
fn inbound_reservation(state: &SessionState, frames: &[Frame<'_>]) -> (usize, usize) {
pub(super) fn inbound_reservation(
state: &SessionState,
frames: &[Frame<'_>],
) -> (usize, usize) {
let mut live = state.streams.keys().copied().collect::<HashSet<_>>();
let mut bytes = 0usize;
let mut items = 0usize;
@@ -350,6 +349,7 @@ mod tests {
public_addr: SocketAddr::from(([203, 0, 113, 10], 443)),
user: "alice".to_string(),
secret_mode: WebSecretMode::Plain,
carrier: WebCarrier::Https,
capability: [0; 32],
max_sessions: 1,
max_streams: 1,