mirror of
https://github.com/telemt/telemt.git
synced 2026-10-26 10:37:38 +03:00
823c6cbc7f
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
422 lines
15 KiB
Rust
422 lines
15 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use sha2::{Digest, Sha256};
|
|
use subtle::ConstantTimeEq;
|
|
use tokio::sync::{Notify, OwnedSemaphorePermit};
|
|
|
|
use super::{SessionCloseReason, SessionNegotiationPhase, WebSession};
|
|
use crate::config::WebLimitsConfig;
|
|
use crate::web::manager::{ManagerError, TokenHash};
|
|
use crate::web::telemetry::WebSessionLifecycleObservation;
|
|
|
|
const MAX_WINDOW: usize = 4;
|
|
|
|
/// Preserves one head body and handler capacity beyond the bounded down-poll plane.
|
|
pub(crate) fn conveyor_waiter_limit(limits: &WebLimitsConfig) -> usize {
|
|
(limits.max_http_handlers / 4)
|
|
.min(limits.max_body_readers.saturating_sub(1))
|
|
.min((limits.max_body_bytes_global / limits.max_body_bytes.max(1)).saturating_sub(1))
|
|
}
|
|
|
|
/// HTTP-only failures distinguish expired exchanges from terminal protocol faults.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) enum ConveyorError {
|
|
/// The bridge already confirmed this sequence, so its digest was retired.
|
|
Stale,
|
|
/// A negotiated mode cannot change within the same bearer session.
|
|
Mode,
|
|
/// Existing carrier failure semantics remain authoritative.
|
|
Manager(ManagerError),
|
|
}
|
|
|
|
impl From<ManagerError> for ConveyorError {
|
|
fn from(error: ManagerError) -> Self {
|
|
Self::Manager(error)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct Slot {
|
|
sequence: u64,
|
|
owner: u64,
|
|
digest: Option<TokenHash>,
|
|
committed: bool,
|
|
}
|
|
|
|
struct Channel {
|
|
confirmed: u64,
|
|
committed: u64,
|
|
slots: [Option<Slot>; MAX_WINDOW],
|
|
notify: Arc<Notify>,
|
|
}
|
|
|
|
impl Channel {
|
|
fn new() -> Self {
|
|
Self {
|
|
confirmed: 0,
|
|
committed: 0,
|
|
slots: [None; MAX_WINDOW],
|
|
notify: Arc::new(Notify::new()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Session-owned replay metadata; bodies stay in their bounded HTTP handlers.
|
|
pub(super) struct ConveyorState {
|
|
offered: u8,
|
|
mode: Option<bool>,
|
|
next_owner: u64,
|
|
channels: HashMap<Option<u32>, Channel>,
|
|
}
|
|
|
|
impl Default for ConveyorState {
|
|
fn default() -> Self {
|
|
Self {
|
|
offered: 1,
|
|
mode: None,
|
|
next_owner: 1,
|
|
channels: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ConveyorState {
|
|
/// Publishes commit history under the same lock as frame application.
|
|
pub(super) fn commit(&mut self, lane: Option<u32>, sequence: u64, digest: TokenHash) {
|
|
if self.mode != Some(true) {
|
|
return;
|
|
}
|
|
if let Some(channel) = self.channels.get_mut(&lane) {
|
|
let index = sequence as usize % MAX_WINDOW;
|
|
channel.slots[index] = Some(Slot {
|
|
sequence,
|
|
digest: Some(digest),
|
|
committed: true,
|
|
owner: 0,
|
|
});
|
|
channel.committed = sequence;
|
|
}
|
|
}
|
|
|
|
/// Detaches retired-lane ordering state for notification after unlocking.
|
|
pub(super) fn remove(&mut self, lane: Option<u32>) -> Option<Arc<Notify>> {
|
|
self.channels.remove(&lane).map(|channel| channel.notify)
|
|
}
|
|
|
|
/// Detaches all waiter notifications for the session's deferred close effects.
|
|
pub(super) fn clear(&mut self) -> Vec<Arc<Notify>> {
|
|
self.channels
|
|
.drain()
|
|
.map(|(_, channel)| channel.notify)
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// An exact request owner; its body and ordinary body permit stay in the HTTP handler.
|
|
pub(crate) struct ConveyorClaim {
|
|
session: Arc<WebSession>,
|
|
lane: Option<u32>,
|
|
sequence: u64,
|
|
owner: u64,
|
|
notify: Arc<Notify>,
|
|
_waiter: Option<OwnedSemaphorePermit>,
|
|
}
|
|
|
|
impl WebSession {
|
|
/// Freezes the offer before the newly created session is published by its manager.
|
|
pub(crate) fn configure_conveyor(&self, requested: u8) {
|
|
let mut state = self.state.lock();
|
|
if state.conveyor.mode.is_none() {
|
|
state.conveyor.offered = if conveyor_waiter_limit(&self.limits) == 0 {
|
|
1
|
|
} else {
|
|
requested.clamp(1, MAX_WINDOW as u8)
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Returns the immutable chain ceiling even across a WebSocket candidate.
|
|
pub(crate) fn conveyor_offer(&self) -> u8 {
|
|
self.state.lock().conveyor.offered
|
|
}
|
|
|
|
/// Returns the HTTP window without changing carrier capability negotiation.
|
|
pub(crate) fn up_window(&self) -> u8 {
|
|
if self.carrier().uses_websocket() {
|
|
1
|
|
} else {
|
|
self.conveyor_offer()
|
|
}
|
|
}
|
|
|
|
/// Claims an HTTP sequence before allocating a body, or locks in legacy serialization.
|
|
pub(crate) fn claim_conveyor(
|
|
self: &Arc<Self>,
|
|
lane: Option<u32>,
|
|
sequence: u64,
|
|
confirmed: Option<u64>,
|
|
) -> Result<Option<ConveyorClaim>, ConveyorError> {
|
|
let mut state = self.state.lock();
|
|
self.ensure_carrier_active_locked(&state)?;
|
|
if state.closed {
|
|
return Err(ManagerError::Closed.into());
|
|
}
|
|
let enabled = confirmed.is_some();
|
|
if (enabled && state.conveyor.offered <= 1)
|
|
|| state.conveyor.mode.is_some_and(|mode| mode != enabled)
|
|
{
|
|
return Err(ConveyorError::Mode);
|
|
}
|
|
if !enabled {
|
|
state.conveyor.mode = Some(false);
|
|
return Ok(None);
|
|
}
|
|
let confirmed = confirmed.unwrap_or(0);
|
|
let window = u64::from(state.conveyor.offered);
|
|
let existing = state.conveyor.channels.get(&lane);
|
|
// Lane eviction retires its bounded digest history, not the parent session.
|
|
if existing.is_none() && lane.is_some() && confirmed > 0 {
|
|
return Err(ConveyorError::Stale);
|
|
}
|
|
let committed = existing.map_or(0, |channel| channel.committed);
|
|
let floor = existing
|
|
.map_or(0, |channel| channel.confirmed)
|
|
.max(confirmed);
|
|
if confirmed > committed || sequence == 0 || sequence > floor.saturating_add(window) {
|
|
drop(state);
|
|
self.close(SessionCloseReason::Protocol);
|
|
return Err(ManagerError::Protocol.into());
|
|
}
|
|
if sequence <= floor {
|
|
return Err(ConveyorError::Stale);
|
|
}
|
|
let index = sequence as usize % MAX_WINDOW;
|
|
if existing
|
|
.and_then(|channel| channel.slots[index])
|
|
.is_some_and(|slot| slot.sequence == sequence && !slot.committed)
|
|
{
|
|
return Err(ManagerError::Concurrent.into());
|
|
}
|
|
if existing.is_none() {
|
|
let limit = self
|
|
.profile
|
|
.max_streams_per_session
|
|
.saturating_add(self.limits.max_tombstones_per_session)
|
|
.saturating_add(1);
|
|
let provisional = state
|
|
.conveyor
|
|
.channels
|
|
.keys()
|
|
.filter(|id| id.is_some_and(|id| !state.carrier_lanes.contains_key(&id)))
|
|
.count();
|
|
if state.conveyor.channels.len() >= limit
|
|
|| (lane.is_some_and(|id| !state.carrier_lanes.contains_key(&id))
|
|
&& state.carrier_lanes.len().saturating_add(provisional) >= limit)
|
|
{
|
|
return Err(ManagerError::Limit.into());
|
|
}
|
|
if lane.is_some_and(|id| {
|
|
state.closed_streams.contains(&id) && !state.carrier_lanes.contains_key(&id)
|
|
}) {
|
|
return Err(ConveyorError::Stale);
|
|
}
|
|
}
|
|
let owner = state.conveyor.next_owner;
|
|
let next_owner = owner.checked_add(1).ok_or(ManagerError::Limit)?;
|
|
// All fallible validation precedes the permit so no early return drops it under state.
|
|
let waiter = if sequence > committed.saturating_add(1) {
|
|
Some(
|
|
self.manager
|
|
.upgrade()
|
|
.ok_or(ManagerError::Closed)?
|
|
.try_conveyor_waiter()
|
|
.ok_or(ManagerError::Backpressure)?,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
state.conveyor.mode = Some(true);
|
|
state.conveyor.next_owner = next_owner;
|
|
let channel = state
|
|
.conveyor
|
|
.channels
|
|
.entry(lane)
|
|
.or_insert_with(Channel::new);
|
|
channel.confirmed = floor;
|
|
for slot in &mut channel.slots {
|
|
if slot.is_some_and(|slot| slot.sequence <= floor) {
|
|
*slot = None;
|
|
}
|
|
}
|
|
let replay = sequence <= channel.committed;
|
|
if !replay {
|
|
channel.slots[index] = Some(Slot {
|
|
sequence,
|
|
owner,
|
|
digest: None,
|
|
committed: false,
|
|
});
|
|
}
|
|
Ok(Some(ConveyorClaim {
|
|
session: Arc::clone(self),
|
|
lane,
|
|
sequence,
|
|
owner: if replay { 0 } else { owner },
|
|
notify: Arc::clone(&channel.notify),
|
|
_waiter: waiter,
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl ConveyorClaim {
|
|
/// Revalidates exact ownership under the frame-application transaction's lock.
|
|
pub(super) fn validate_locked(
|
|
&self,
|
|
state: &super::SessionState,
|
|
digest: &TokenHash,
|
|
) -> Result<(), ManagerError> {
|
|
let channel = state
|
|
.conveyor
|
|
.channels
|
|
.get(&self.lane)
|
|
.filter(|channel| Arc::ptr_eq(&channel.notify, &self.notify))
|
|
.ok_or(ManagerError::Closed)?;
|
|
let slot = channel.slots[self.sequence as usize % MAX_WINDOW]
|
|
.filter(|slot| {
|
|
slot.sequence == self.sequence
|
|
&& slot.owner == self.owner
|
|
&& !slot.committed
|
|
&& slot
|
|
.digest
|
|
.is_some_and(|value| bool::from(value.ct_eq(digest)))
|
|
})
|
|
.ok_or(ManagerError::Closed)?;
|
|
if channel.committed.checked_add(1) != Some(slot.sequence) {
|
|
return Err(ManagerError::Concurrent);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Applies only the next sequence; duplicates verify the retained digest without replaying frames.
|
|
pub(crate) async fn process(&self, body: &[u8]) -> Result<u64, ConveyorError> {
|
|
let digest: TokenHash = Sha256::digest(body).into();
|
|
let operation = async {
|
|
loop {
|
|
let notified = self.notify.notified();
|
|
tokio::pin!(notified);
|
|
notified.as_mut().enable();
|
|
let ready = {
|
|
let mut state = self.session.state.lock();
|
|
self.session.ensure_carrier_active_locked(&state)?;
|
|
if state.closed {
|
|
return Err(ManagerError::Closed.into());
|
|
}
|
|
let Some(channel) = state
|
|
.conveyor
|
|
.channels
|
|
.get_mut(&self.lane)
|
|
.filter(|channel| Arc::ptr_eq(&channel.notify, &self.notify))
|
|
else {
|
|
return Err(ConveyorError::Stale);
|
|
};
|
|
if self.sequence <= channel.confirmed {
|
|
return Err(ConveyorError::Stale);
|
|
}
|
|
let Some(slot) = &mut channel.slots[self.sequence as usize % MAX_WINDOW] else {
|
|
return Err(ConveyorError::Stale);
|
|
};
|
|
if slot.sequence != self.sequence
|
|
|| (!slot.committed && slot.owner != self.owner)
|
|
{
|
|
return Err(ConveyorError::Stale);
|
|
}
|
|
if slot
|
|
.digest
|
|
.is_some_and(|previous| !bool::from(previous.ct_eq(&digest)))
|
|
{
|
|
drop(state);
|
|
self.session.close(SessionCloseReason::Protocol);
|
|
return Err(ManagerError::Protocol.into());
|
|
}
|
|
slot.digest = Some(digest);
|
|
if slot.committed {
|
|
self.session.touch_peer_locked(
|
|
&mut state,
|
|
Instant::now(),
|
|
WebSessionLifecycleObservation::HttpActivityAfterGap,
|
|
);
|
|
if self.session.automatic_carrier
|
|
&& state.negotiation_phase != SessionNegotiationPhase::Committed
|
|
{
|
|
return Err(ManagerError::Backpressure.into());
|
|
}
|
|
return Ok(self.sequence);
|
|
}
|
|
channel.committed.checked_add(1) == Some(self.sequence)
|
|
&& (self.lane.is_some()
|
|
|| !self
|
|
.session
|
|
.up_active
|
|
.load(std::sync::atomic::Ordering::Acquire))
|
|
};
|
|
if ready {
|
|
let result = match self.lane {
|
|
Some(lane) => self.session.process_up_lane_inner(
|
|
lane,
|
|
self.sequence,
|
|
body,
|
|
Some((self, digest)),
|
|
),
|
|
None => self
|
|
.session
|
|
.process_claimed_up(self.sequence, body, self, digest),
|
|
};
|
|
self.notify.notify_waiters();
|
|
return result.map_err(ConveyorError::from);
|
|
}
|
|
tokio::select! {
|
|
_ = self.session.cancel.cancelled() => return Err(ManagerError::Closed.into()),
|
|
_ = notified => {}
|
|
}
|
|
}
|
|
};
|
|
tokio::time::timeout(
|
|
Duration::from_secs(self.session.timeouts.body_secs),
|
|
operation,
|
|
)
|
|
.await
|
|
.map_err(|_| ConveyorError::Manager(ManagerError::Backpressure))?
|
|
}
|
|
}
|
|
|
|
impl Drop for ConveyorClaim {
|
|
fn drop(&mut self) {
|
|
if self.owner == 0 {
|
|
return;
|
|
}
|
|
let mut state = self.session.state.lock();
|
|
if let Some(channel) = state
|
|
.conveyor
|
|
.channels
|
|
.get_mut(&self.lane)
|
|
.filter(|channel| Arc::ptr_eq(&channel.notify, &self.notify))
|
|
{
|
|
let slot = &mut channel.slots[self.sequence as usize % MAX_WINDOW];
|
|
if slot.is_some_and(|slot| !slot.committed && slot.owner == self.owner) {
|
|
*slot = None;
|
|
}
|
|
if channel.committed == 0 && channel.slots.iter().all(Option::is_none) {
|
|
state.conveyor.channels.remove(&self.lane);
|
|
}
|
|
}
|
|
drop(state);
|
|
self.notify.notify_waiters();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "conveyor/tests.rs"]
|
|
mod tests;
|