mirror of
https://github.com/telemt/telemt.git
synced 2026-10-07 09:55:57 +03:00
531 lines
19 KiB
Rust
531 lines
19 KiB
Rust
use std::io;
|
|
use std::pin::Pin;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
use std::task::{Context, Poll};
|
|
use std::time::Duration;
|
|
|
|
use tokio::io::{AsyncBufRead, AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf, copy_buf};
|
|
use tokio::time::Instant;
|
|
use tokio_util::sync::CancellationToken;
|
|
use tracing::{debug, warn};
|
|
|
|
use crate::error::{ProxyError, Result};
|
|
use crate::proxy::adaptive_buffers::{
|
|
AdaptiveTier, RelaySignalSample, SessionAdaptiveController, TierTransitionReason,
|
|
direct_copy_buffers_for_tier_with_ceilings,
|
|
};
|
|
use crate::proxy::direct_buffer_budget::{
|
|
DIRECT_BASE_C2S_BYTES, DIRECT_BASE_S2C_BYTES, DirectBufferBudget, DirectBufferLease,
|
|
};
|
|
use crate::proxy::traffic_limiter::TrafficLease;
|
|
use crate::stats::{Stats, UserQuotaHandle};
|
|
|
|
use super::WATCHDOG_INTERVAL;
|
|
use super::io::{SharedCounters, StatsIo, is_quota_io_error};
|
|
use super::watchdog_delta;
|
|
|
|
mod write_pressure;
|
|
|
|
use self::write_pressure::WritePressureIo;
|
|
|
|
struct AdaptiveBufferState {
|
|
desired_bytes: AtomicUsize,
|
|
actual_bytes: AtomicUsize,
|
|
}
|
|
|
|
impl AdaptiveBufferState {
|
|
fn new(bytes: usize) -> Arc<Self> {
|
|
Arc::new(Self {
|
|
desired_bytes: AtomicUsize::new(bytes.max(1)),
|
|
actual_bytes: AtomicUsize::new(bytes.max(1)),
|
|
})
|
|
}
|
|
}
|
|
|
|
struct AdaptiveBufReader<R> {
|
|
inner: R,
|
|
buffer: Box<[u8]>,
|
|
pos: usize,
|
|
cap: usize,
|
|
state: Arc<AdaptiveBufferState>,
|
|
}
|
|
|
|
impl<R> AdaptiveBufReader<R> {
|
|
fn new(inner: R, state: Arc<AdaptiveBufferState>) -> Self {
|
|
let bytes = state.actual_bytes.load(Ordering::Relaxed).max(1);
|
|
Self {
|
|
inner,
|
|
buffer: vec![0; bytes].into_boxed_slice(),
|
|
pos: 0,
|
|
cap: 0,
|
|
state,
|
|
}
|
|
}
|
|
|
|
fn resize_if_drained(&mut self) {
|
|
if self.pos != self.cap {
|
|
return;
|
|
}
|
|
let desired = self.state.desired_bytes.load(Ordering::Acquire).max(1);
|
|
if desired == self.buffer.len() {
|
|
return;
|
|
}
|
|
self.buffer = vec![0; desired].into_boxed_slice();
|
|
self.pos = 0;
|
|
self.cap = 0;
|
|
self.state.actual_bytes.store(desired, Ordering::Release);
|
|
}
|
|
}
|
|
|
|
impl<R: AsyncRead + Unpin> AsyncRead for AdaptiveBufReader<R> {
|
|
fn poll_read(
|
|
self: Pin<&mut Self>,
|
|
cx: &mut Context<'_>,
|
|
output: &mut ReadBuf<'_>,
|
|
) -> Poll<io::Result<()>> {
|
|
let this = self.get_mut();
|
|
if this.pos < this.cap {
|
|
let available = &this.buffer[this.pos..this.cap];
|
|
let copied = available.len().min(output.remaining());
|
|
output.put_slice(&available[..copied]);
|
|
this.pos += copied;
|
|
return Poll::Ready(Ok(()));
|
|
}
|
|
this.resize_if_drained();
|
|
Pin::new(&mut this.inner).poll_read(cx, output)
|
|
}
|
|
}
|
|
|
|
impl<R: AsyncRead + Unpin> AsyncBufRead for AdaptiveBufReader<R> {
|
|
fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<&[u8]>> {
|
|
let this = self.get_mut();
|
|
if this.pos < this.cap {
|
|
return Poll::Ready(Ok(&this.buffer[this.pos..this.cap]));
|
|
}
|
|
|
|
this.resize_if_drained();
|
|
let mut read_buf = ReadBuf::new(&mut this.buffer);
|
|
match Pin::new(&mut this.inner).poll_read(cx, &mut read_buf) {
|
|
Poll::Ready(Ok(())) => {
|
|
this.pos = 0;
|
|
this.cap = read_buf.filled().len();
|
|
Poll::Ready(Ok(&this.buffer[..this.cap]))
|
|
}
|
|
Poll::Ready(Err(error)) => Poll::Ready(Err(error)),
|
|
Poll::Pending => Poll::Pending,
|
|
}
|
|
}
|
|
|
|
fn consume(self: Pin<&mut Self>, amount: usize) {
|
|
let this = self.get_mut();
|
|
this.pos = this.pos.saturating_add(amount).min(this.cap);
|
|
}
|
|
}
|
|
|
|
enum AdaptiveRelayOutcome {
|
|
Copy(io::Result<(u64, u64)>),
|
|
ActivityTimeout,
|
|
UserDisabled,
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
/// Relays one Direct session with independently resizable directional buffers.
|
|
pub(crate) async fn relay_direct_adaptive<CR, CW, SR, SW>(
|
|
client_reader: CR,
|
|
client_writer: CW,
|
|
server_reader: SR,
|
|
server_writer: SW,
|
|
ceiling_c2s_bytes: usize,
|
|
ceiling_s2c_bytes: usize,
|
|
max_connections: u32,
|
|
user: &str,
|
|
stats: Arc<Stats>,
|
|
quota_handle: UserQuotaHandle,
|
|
quota_limit: Option<u64>,
|
|
traffic_lease: Option<Arc<TrafficLease>>,
|
|
activity_timeout: Duration,
|
|
session_cancel: CancellationToken,
|
|
budget: Arc<DirectBufferBudget>,
|
|
) -> Result<()>
|
|
where
|
|
CR: AsyncRead + Unpin + Send + 'static,
|
|
CW: AsyncWrite + Unpin + Send + 'static,
|
|
SR: AsyncRead + Unpin + Send + 'static,
|
|
SW: AsyncWrite + Unpin + Send + 'static,
|
|
{
|
|
let activity_timeout = activity_timeout.max(Duration::from_secs(1));
|
|
let epoch = Instant::now();
|
|
let counters = Arc::new(SharedCounters::new());
|
|
let quota_exceeded = Arc::new(AtomicBool::new(false));
|
|
let user_owned = user.to_string();
|
|
|
|
let (base_c2s, base_s2c) = initial_base_sizes(
|
|
ceiling_c2s_bytes,
|
|
ceiling_s2c_bytes,
|
|
max_connections,
|
|
budget.target_bytes(),
|
|
);
|
|
let base_total = base_c2s.saturating_add(base_s2c);
|
|
let mut lease = match budget.try_reserve(base_total, false) {
|
|
Some(lease) => lease,
|
|
None => {
|
|
let minimum_total = DIRECT_BASE_C2S_BYTES + DIRECT_BASE_S2C_BYTES;
|
|
match budget.try_reserve(minimum_total, true) {
|
|
Some(lease) => {
|
|
budget.increment_minimum_fallback();
|
|
lease
|
|
}
|
|
None => {
|
|
budget.increment_admission_rejected();
|
|
return Err(ProxyError::Proxy(
|
|
"Direct relay buffer pressure: budget exhausted".to_string(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let effective_base = if lease.reserved_bytes() < base_total {
|
|
(DIRECT_BASE_C2S_BYTES, DIRECT_BASE_S2C_BYTES)
|
|
} else {
|
|
(base_c2s, base_s2c)
|
|
};
|
|
let c2s_state = AdaptiveBufferState::new(effective_base.0);
|
|
let s2c_state = AdaptiveBufferState::new(effective_base.1);
|
|
|
|
let mut controller = SessionAdaptiveController::new(AdaptiveTier::Base);
|
|
|
|
let c2s_client = StatsIo::new_with_traffic_lease(
|
|
client_reader,
|
|
Arc::clone(&counters),
|
|
Arc::clone(&stats),
|
|
user_owned.clone(),
|
|
quota_handle.clone(),
|
|
traffic_lease.clone(),
|
|
quota_limit,
|
|
Arc::clone("a_exceeded),
|
|
epoch,
|
|
);
|
|
let client_writer = StatsIo::new_with_traffic_lease(
|
|
client_writer,
|
|
Arc::clone(&counters),
|
|
Arc::clone(&stats),
|
|
user_owned.clone(),
|
|
quota_handle,
|
|
traffic_lease,
|
|
quota_limit,
|
|
Arc::clone("a_exceeded),
|
|
epoch,
|
|
);
|
|
let mut client_writer = WritePressureIo::new(client_writer, Arc::clone(&counters));
|
|
let mut c2s_reader = AdaptiveBufReader::new(c2s_client, Arc::clone(&c2s_state));
|
|
let mut s2c_reader = AdaptiveBufReader::new(server_reader, Arc::clone(&s2c_state));
|
|
let mut server_writer = server_writer;
|
|
let mut pressure_rx = budget.subscribe_pressure();
|
|
|
|
let relay_outcome = {
|
|
let copy = async {
|
|
let c2s = async {
|
|
let copied = copy_buf(&mut c2s_reader, &mut server_writer).await?;
|
|
server_writer.shutdown().await?;
|
|
Ok::<u64, io::Error>(copied)
|
|
};
|
|
let s2c = async {
|
|
let copied = copy_buf(&mut s2c_reader, &mut client_writer).await?;
|
|
client_writer.shutdown().await?;
|
|
Ok::<u64, io::Error>(copied)
|
|
};
|
|
tokio::try_join!(c2s, s2c)
|
|
};
|
|
tokio::pin!(copy);
|
|
|
|
let mut interval = tokio::time::interval(WATCHDOG_INTERVAL);
|
|
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
|
interval.tick().await;
|
|
let mut previous = RelaySignalSample::default();
|
|
let mut previous_log_c2s = 0u64;
|
|
let mut previous_log_s2c = 0u64;
|
|
let mut previous_sample_at = epoch;
|
|
|
|
loop {
|
|
tokio::select! {
|
|
result = &mut copy => break AdaptiveRelayOutcome::Copy(result),
|
|
_ = session_cancel.cancelled() => break AdaptiveRelayOutcome::UserDisabled,
|
|
changed = pressure_rx.changed() => {
|
|
if changed.is_ok() {
|
|
apply_global_pressure_demotion(
|
|
&mut controller,
|
|
&mut lease,
|
|
&c2s_state,
|
|
&s2c_state,
|
|
effective_base,
|
|
(ceiling_c2s_bytes, ceiling_s2c_bytes),
|
|
budget.as_ref(),
|
|
);
|
|
reconcile_reservation(&mut lease, &c2s_state, &s2c_state);
|
|
}
|
|
}
|
|
_ = interval.tick() => {
|
|
let now = Instant::now();
|
|
let idle = counters.idle_duration(now, epoch);
|
|
if quota_exceeded.load(Ordering::Acquire) {
|
|
warn!(user = %user_owned, "User data quota reached, closing relay");
|
|
break AdaptiveRelayOutcome::ActivityTimeout;
|
|
}
|
|
if idle >= activity_timeout {
|
|
warn!(
|
|
user = %user_owned,
|
|
c2s_bytes = counters.c2s_bytes.load(Ordering::Relaxed),
|
|
s2c_bytes = counters.s2c_bytes.load(Ordering::Relaxed),
|
|
idle_secs = idle.as_secs(),
|
|
"Activity timeout"
|
|
);
|
|
break AdaptiveRelayOutcome::ActivityTimeout;
|
|
}
|
|
|
|
let sample = current_sample(counters.as_ref());
|
|
let c2s_delta = watchdog_delta(sample.c2s_bytes, previous_log_c2s);
|
|
let s2c_delta = watchdog_delta(sample.s2c_written_bytes, previous_log_s2c);
|
|
if c2s_delta > 0 || s2c_delta > 0 {
|
|
let secs = now.saturating_duration_since(previous_sample_at).as_secs_f64();
|
|
debug!(
|
|
user = %user_owned,
|
|
c2s_kbps = (c2s_delta as f64 / secs / 1024.0) as u64,
|
|
s2c_kbps = (s2c_delta as f64 / secs / 1024.0) as u64,
|
|
c2s_total = sample.c2s_bytes,
|
|
s2c_total = sample.s2c_written_bytes,
|
|
"Relay active"
|
|
);
|
|
}
|
|
|
|
let delta = sample_delta(sample, previous);
|
|
let tick_secs = now.saturating_duration_since(previous_sample_at).as_secs_f64();
|
|
if let Some(transition) = controller.observe(delta, tick_secs) {
|
|
apply_controller_transition(
|
|
transition,
|
|
&mut controller,
|
|
&mut lease,
|
|
&c2s_state,
|
|
&s2c_state,
|
|
effective_base,
|
|
(ceiling_c2s_bytes, ceiling_s2c_bytes),
|
|
budget.as_ref(),
|
|
);
|
|
}
|
|
reconcile_reservation(&mut lease, &c2s_state, &s2c_state);
|
|
previous = sample;
|
|
previous_log_c2s = sample.c2s_bytes;
|
|
previous_log_s2c = sample.s2c_written_bytes;
|
|
previous_sample_at = now;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let _ = client_writer.shutdown().await;
|
|
let _ = server_writer.shutdown().await;
|
|
let c2s_ops = counters.c2s_ops.load(Ordering::Relaxed);
|
|
let s2c_ops = counters.s2c_ops.load(Ordering::Relaxed);
|
|
let duration = epoch.elapsed();
|
|
match relay_outcome {
|
|
AdaptiveRelayOutcome::Copy(Ok((c2s, s2c))) => {
|
|
debug!(
|
|
user = %user_owned,
|
|
c2s_bytes = c2s,
|
|
s2c_bytes = s2c,
|
|
c2s_msgs = c2s_ops,
|
|
s2c_msgs = s2c_ops,
|
|
duration_secs = duration.as_secs(),
|
|
"Relay finished"
|
|
);
|
|
Ok(())
|
|
}
|
|
AdaptiveRelayOutcome::Copy(Err(error)) if is_quota_io_error(&error) => {
|
|
warn!(
|
|
user = %user_owned,
|
|
c2s_bytes = counters.c2s_bytes.load(Ordering::Relaxed),
|
|
s2c_bytes = counters.s2c_bytes.load(Ordering::Relaxed),
|
|
c2s_msgs = c2s_ops,
|
|
s2c_msgs = s2c_ops,
|
|
duration_secs = duration.as_secs(),
|
|
"Data quota reached, closing relay"
|
|
);
|
|
Err(ProxyError::DataQuotaExceeded { user: user_owned })
|
|
}
|
|
AdaptiveRelayOutcome::Copy(Err(error)) => {
|
|
debug!(
|
|
user = %user_owned,
|
|
c2s_bytes = counters.c2s_bytes.load(Ordering::Relaxed),
|
|
s2c_bytes = counters.s2c_bytes.load(Ordering::Relaxed),
|
|
c2s_msgs = c2s_ops,
|
|
s2c_msgs = s2c_ops,
|
|
duration_secs = duration.as_secs(),
|
|
error = %error,
|
|
"Relay error"
|
|
);
|
|
Err(error.into())
|
|
}
|
|
AdaptiveRelayOutcome::ActivityTimeout => {
|
|
debug!(
|
|
user = %user_owned,
|
|
c2s_bytes = counters.c2s_bytes.load(Ordering::Relaxed),
|
|
s2c_bytes = counters.s2c_bytes.load(Ordering::Relaxed),
|
|
c2s_msgs = c2s_ops,
|
|
s2c_msgs = s2c_ops,
|
|
duration_secs = duration.as_secs(),
|
|
"Relay finished (activity timeout)"
|
|
);
|
|
Ok(())
|
|
}
|
|
AdaptiveRelayOutcome::UserDisabled => {
|
|
debug!(
|
|
user = %user_owned,
|
|
c2s_bytes = counters.c2s_bytes.load(Ordering::Relaxed),
|
|
s2c_bytes = counters.s2c_bytes.load(Ordering::Relaxed),
|
|
c2s_msgs = c2s_ops,
|
|
s2c_msgs = s2c_ops,
|
|
duration_secs = duration.as_secs(),
|
|
"Relay finished (user disabled)"
|
|
);
|
|
Err(ProxyError::UserDisabled { user: user_owned })
|
|
}
|
|
}
|
|
}
|
|
|
|
fn initial_base_sizes(
|
|
ceiling_c2s: usize,
|
|
ceiling_s2c: usize,
|
|
max_connections: u32,
|
|
target_bytes: usize,
|
|
) -> (usize, usize) {
|
|
let configured_total = ceiling_c2s.saturating_add(ceiling_s2c);
|
|
let configured_worst_case = configured_total.saturating_mul(max_connections as usize);
|
|
if max_connections != 0 && configured_worst_case <= target_bytes {
|
|
return (ceiling_c2s, ceiling_s2c);
|
|
}
|
|
(
|
|
DIRECT_BASE_C2S_BYTES.min(ceiling_c2s),
|
|
DIRECT_BASE_S2C_BYTES.min(ceiling_s2c),
|
|
)
|
|
}
|
|
|
|
fn current_sample(counters: &SharedCounters) -> RelaySignalSample {
|
|
RelaySignalSample {
|
|
c2s_bytes: counters.c2s_bytes.load(Ordering::Relaxed),
|
|
s2c_requested_bytes: counters.s2c_requested_bytes.load(Ordering::Relaxed),
|
|
s2c_written_bytes: counters.s2c_bytes.load(Ordering::Relaxed),
|
|
s2c_write_ops: counters.s2c_ops.load(Ordering::Relaxed),
|
|
s2c_partial_writes: counters.s2c_partial_writes.load(Ordering::Relaxed),
|
|
s2c_consecutive_pending_writes: counters
|
|
.s2c_consecutive_pending_writes
|
|
.load(Ordering::Relaxed),
|
|
}
|
|
}
|
|
|
|
fn sample_delta(current: RelaySignalSample, previous: RelaySignalSample) -> RelaySignalSample {
|
|
RelaySignalSample {
|
|
c2s_bytes: current.c2s_bytes.saturating_sub(previous.c2s_bytes),
|
|
s2c_requested_bytes: current
|
|
.s2c_requested_bytes
|
|
.saturating_sub(previous.s2c_requested_bytes),
|
|
s2c_written_bytes: current
|
|
.s2c_written_bytes
|
|
.saturating_sub(previous.s2c_written_bytes),
|
|
s2c_write_ops: current.s2c_write_ops.saturating_sub(previous.s2c_write_ops),
|
|
s2c_partial_writes: current
|
|
.s2c_partial_writes
|
|
.saturating_sub(previous.s2c_partial_writes),
|
|
s2c_consecutive_pending_writes: current.s2c_consecutive_pending_writes,
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn apply_controller_transition(
|
|
transition: crate::proxy::adaptive_buffers::TierTransition,
|
|
controller: &mut SessionAdaptiveController,
|
|
lease: &mut DirectBufferLease,
|
|
c2s_state: &AdaptiveBufferState,
|
|
s2c_state: &AdaptiveBufferState,
|
|
base: (usize, usize),
|
|
ceilings: (usize, usize),
|
|
budget: &DirectBufferBudget,
|
|
) {
|
|
let sizes = direct_copy_buffers_for_tier_with_ceilings(
|
|
transition.to,
|
|
base.0,
|
|
base.1,
|
|
ceilings.0,
|
|
ceilings.1,
|
|
);
|
|
if transition.to > transition.from {
|
|
if !lease.try_grow_to(sizes.0.saturating_add(sizes.1)) {
|
|
*controller = SessionAdaptiveController::new(transition.from);
|
|
return;
|
|
}
|
|
} else {
|
|
match transition.reason {
|
|
TierTransitionReason::QuietDemotion => budget.increment_quiet_demotion(),
|
|
TierTransitionReason::SustainedWritePressure => {
|
|
budget.increment_write_pressure_demotion();
|
|
}
|
|
TierTransitionReason::SoftConfirmed | TierTransitionReason::HardPressure => {}
|
|
}
|
|
}
|
|
set_desired_sizes(c2s_state, s2c_state, sizes);
|
|
lease.set_tier(transition.to.as_u8() as usize);
|
|
}
|
|
|
|
fn apply_global_pressure_demotion(
|
|
controller: &mut SessionAdaptiveController,
|
|
lease: &mut DirectBufferLease,
|
|
c2s_state: &AdaptiveBufferState,
|
|
s2c_state: &AdaptiveBufferState,
|
|
base: (usize, usize),
|
|
ceilings: (usize, usize),
|
|
budget: &DirectBufferBudget,
|
|
) {
|
|
let current = controller.tier();
|
|
let target = current.demote();
|
|
if target == current {
|
|
return;
|
|
}
|
|
*controller = SessionAdaptiveController::new(target);
|
|
let sizes =
|
|
direct_copy_buffers_for_tier_with_ceilings(target, base.0, base.1, ceilings.0, ceilings.1);
|
|
set_desired_sizes(c2s_state, s2c_state, sizes);
|
|
lease.set_tier(target.as_u8() as usize);
|
|
budget.increment_global_pressure_demotion();
|
|
}
|
|
|
|
fn set_desired_sizes(
|
|
c2s_state: &AdaptiveBufferState,
|
|
s2c_state: &AdaptiveBufferState,
|
|
sizes: (usize, usize),
|
|
) {
|
|
c2s_state
|
|
.desired_bytes
|
|
.store(sizes.0.max(1), Ordering::Release);
|
|
s2c_state
|
|
.desired_bytes
|
|
.store(sizes.1.max(1), Ordering::Release);
|
|
}
|
|
|
|
fn reconcile_reservation(
|
|
lease: &mut DirectBufferLease,
|
|
c2s_state: &AdaptiveBufferState,
|
|
s2c_state: &AdaptiveBufferState,
|
|
) {
|
|
// Promotion reserves the desired allocation before either reader grows.
|
|
// Demotion keeps the actual allocation covered until its buffered bytes drain.
|
|
let covered_c2s = c2s_state
|
|
.actual_bytes
|
|
.load(Ordering::Acquire)
|
|
.max(c2s_state.desired_bytes.load(Ordering::Acquire));
|
|
let covered_s2c = s2c_state
|
|
.actual_bytes
|
|
.load(Ordering::Acquire)
|
|
.max(s2c_state.desired_bytes.load(Ordering::Acquire));
|
|
lease.shrink_to(covered_c2s.saturating_add(covered_s2c));
|
|
}
|