//! Bounded Tokio UDP transport and the C#-compatible transport value types. #![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility map. #![allow(clippy::needless_pass_by_value)] // Owned arrays and objects preserve mapped signatures. use crate::packets::{Packet, PacketAckPacket, PacketAckPacketPacketsBlock, PacketType}; use crate::{Error, GridClient, Helpers, Simulator}; use libremetaverse_types::compat::{Array, CancellationToken, CancellationTokenSource, Object}; use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::mpsc::{Receiver as AckReceiver, SyncSender as AckSender, sync_channel}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::net::UdpSocket; use tokio::sync::{mpsc, oneshot}; use tokio::task::JoinHandle; use tokio::time::{Instant, MissedTickBehavior}; const DEFAULT_DECODE_BUFFER_SIZE: usize = 8 * 1024; const THROTTLE_PERIOD: Duration = Duration::from_millis(100); const THROTTLE_MIN_BYTES_PER_PERIOD: usize = 200; const THROTTLE_BURST_PERIODS: usize = 4; /// A failure at the native UDP transport boundary. /// /// The variants deliberately carry no datagram bytes, credentials, endpoint /// query data, or operating-system error strings. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum UdpTransportError { InvalidConfiguration(&'static str), InvalidBuffer, MtuExceeded, NotRunning, AlreadyRunning, Backpressure, ReliableWindowFull, TooManyPeers, RuntimeUnavailable, Cancelled, Socket, } impl fmt::Display for UdpTransportError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::InvalidConfiguration(field) => { write!(formatter, "invalid UDP transport setting {field}") } Self::InvalidBuffer => formatter.write_str("invalid UDP packet buffer"), Self::MtuExceeded => formatter.write_str("UDP payload exceeds the protocol MTU"), Self::NotRunning => formatter.write_str("UDP transport is not running"), Self::AlreadyRunning => formatter.write_str("UDP transport is already running"), Self::Backpressure => formatter.write_str("UDP transport queue is full"), Self::ReliableWindowFull => formatter.write_str("UDP reliable-send window is full"), Self::TooManyPeers => formatter.write_str("UDP peer limit is reached"), Self::RuntimeUnavailable => { formatter.write_str("UDP transport requires a current Tokio runtime") } Self::Cancelled => formatter.write_str("UDP transport was cancelled"), Self::Socket => formatter.write_str("UDP socket operation failed"), } } } impl std::error::Error for UdpTransportError {} impl From for Error { fn from(error: UdpTransportError) -> Self { match error { UdpTransportError::InvalidConfiguration(_) | UdpTransportError::InvalidBuffer | UdpTransportError::MtuExceeded => Self::Argument, UdpTransportError::Cancelled => Self::Cancelled, UdpTransportError::Socket => Self::Socket, UdpTransportError::NotRunning | UdpTransportError::AlreadyRunning | UdpTransportError::Backpressure | UdpTransportError::ReliableWindowFull | UdpTransportError::TooManyPeers | UdpTransportError::RuntimeUnavailable => Self::InvalidOperation, } } } /// C# `UDPPacketBuffer`, with one owned allocation per queued datagram. #[derive(Clone, Debug, Eq, PartialEq)] pub struct UDPPacketBuffer { pub data: Vec, pub data_length: i32, pub remote_end_point: SocketAddr, } impl UDPPacketBuffer { pub const DEFAULT_BUFFER_SIZE: i32 = 4096; pub fn new_with_constructor() -> Result { Self::new_with_ip_end_point_int32( SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), Self::DEFAULT_BUFFER_SIZE, ) } pub fn new_with_bytes_int32_ip_end_point_int32( buffer: Vec, buffer_size: i32, destination: SocketAddr, _category: i32, ) -> Result { let mut packet = Self::new_with_ip_end_point_int32(destination, buffer_size)?; packet.copy_from_slice_with_length(&buffer, buffer_size)?; packet.data_length = buffer_size; Ok(packet) } pub fn new_with_ip_end_point(end_point: SocketAddr) -> Result { Self::new_with_ip_end_point_int32(end_point, Self::DEFAULT_BUFFER_SIZE) } pub fn new_with_ip_end_point_bytes( end_point: SocketAddr, data: Vec, ) -> Result { Ok(Self { data, // The C# constructor adopts the supplied array but deliberately // leaves the public DataLength field at its zero default. data_length: 0, remote_end_point: end_point, }) } pub fn new_with_ip_end_point_int32( end_point: SocketAddr, buffer_size: i32, ) -> Result { let buffer_size = usize::try_from(buffer_size).map_err(|_| Error::Argument)?; let mut data = Vec::new(); data.try_reserve_exact(buffer_size) .map_err(|_| Error::InvalidOperation)?; data.resize(buffer_size, 0); Ok(Self { data, data_length: 0, remote_end_point: end_point, }) } /// Copies a mapped CLR array into this buffer. /// /// `Object::Bytes` represents a boxed `byte[]`; a flat array of integer /// objects is also accepted so the mapped `System.Array` remains useful. pub fn copy_from_with_array(&mut self, src: Array) -> Result<(), Error> { let bytes = mapped_array_bytes(src)?; let length = i32::try_from(bytes.len()).map_err(|_| Error::Argument)?; self.copy_from_slice_with_length(&bytes, length) } pub fn copy_from_with_array_int32(&mut self, src: Array, length: i32) -> Result<(), Error> { let bytes = mapped_array_bytes(src)?; self.copy_from_slice_with_length(&bytes, length) } pub fn copy_from_slice(&mut self, src: &[u8]) -> Result<(), Error> { let length = i32::try_from(src.len()).map_err(|_| Error::Argument)?; self.copy_from_slice_with_length(src, length) } pub fn copy_from_slice_with_length(&mut self, src: &[u8], length: i32) -> Result<(), Error> { let length = usize::try_from(length).map_err(|_| Error::Argument)?; if length > src.len() || length > self.data.len() { return Err(Error::IndexOutOfRange); } self.data[..length].copy_from_slice(&src[..length]); Ok(()) } pub fn reset_endpoint(&mut self) -> Result<(), Error> { self.remote_end_point = match self.remote_end_point.ip() { IpAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), IpAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), }; Ok(()) } fn payload(&self) -> Result<&[u8], UdpTransportError> { let length = usize::try_from(self.data_length).map_err(|_| UdpTransportError::InvalidBuffer)?; self.data .get(..length) .ok_or(UdpTransportError::InvalidBuffer) } } fn mapped_array_bytes(src: Array) -> Result, Error> { if let [Object::Bytes(bytes)] = src.0.as_slice() { return Ok(bytes.clone()); } let mut bytes = Vec::new(); bytes .try_reserve_exact(src.0.len()) .map_err(|_| Error::InvalidOperation)?; for value in src.0 { let byte = match value { Object::Integer(value) => u8::try_from(value).map_err(|_| Error::Argument)?, Object::UInteger(value) => u8::try_from(value).map_err(|_| Error::Argument)?, _ => return Err(Error::Argument), }; bytes.push(byte); } Ok(bytes) } struct PacketArchiveState { items: Vec, members: HashSet, first: usize, next: usize, } /// Fixed-size duplicate archive matching `IncomingPacketIDCollection`. pub struct IncomingPacketIDCollection { capacity: usize, state: Mutex, } impl IncomingPacketIDCollection { pub fn new(capacity: i32) -> Result { let capacity = usize::try_from(capacity).map_err(|_| Error::Argument)?; if capacity == 0 { return Err(Error::Argument); } let mut items = Vec::new(); items .try_reserve_exact(capacity) .map_err(|_| Error::InvalidOperation)?; items.resize(capacity, 0); Ok(Self { capacity, state: Mutex::new(PacketArchiveState { items, members: HashSet::with_capacity(capacity), first: 0, next: 0, }), }) } pub fn try_enqueue(&self, ack: u32) -> bool { let mut state = self .state .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if !state.members.insert(ack) { return false; } let next = state.next; state.items[next] = ack; state.next = (next + 1) % self.capacity; if state.next == state.first { let first = state.first; let removed = state.items[first]; state.members.remove(&removed); state.first = (first + 1) % self.capacity; } true } } impl fmt::Debug for IncomingPacketIDCollection { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { let state = self .state .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); formatter .debug_struct("IncomingPacketIDCollection") .field("capacity", &self.capacity) .field("len", &state.members.len()) .finish() } } /// Injection boundary used by `AgentThrottle::Set` and the later network /// manager composition layer. pub trait AgentThrottleSender: Send + Sync { fn send_throttle( &self, throttle_bytes: &[u8], simulator: Option<&Simulator>, ) -> Result<(), Error>; } /// Exact seven-stream C# throttle values and little-endian wire encoding. #[derive(Clone)] pub struct AgentThrottle { resend: f32, land: f32, wind: f32, cloud: f32, task: f32, texture: f32, asset: f32, sender: Option>, } impl AgentThrottle { pub fn new_with_grid_client(client: GridClient) -> Result { Ok(Self { sender: client.agent_throttle_sender(), ..Self::default() }) } pub fn new_with_bytes_int32(data: Vec, pos: i32) -> Result { let pos = usize::try_from(pos).map_err(|_| Error::Argument)?; let end = pos.checked_add(28).ok_or(Error::Argument)?; let bytes = data.get(pos..end).ok_or(Error::IndexOutOfRange)?; let mut value = Self::default(); value.set_resend(read_f32(bytes, 0)?); value.set_land(read_f32(bytes, 4)?); value.set_wind(read_f32(bytes, 8)?); value.set_cloud(read_f32(bytes, 12)?); value.set_task(read_f32(bytes, 16)?); value.set_texture(read_f32(bytes, 20)?); value.set_asset(read_f32(bytes, 24)?); Ok(value) } #[must_use] pub fn with_sender(mut self, sender: Arc) -> Self { self.sender = Some(sender); self } pub fn set_with_method(&self) -> Result<(), Error> { if let Some(sender) = &self.sender { sender.send_throttle(&self.to_bytes()?, None)?; } Ok(()) } pub fn set_with_simulator(&self, simulator: Option) -> Result<(), Error> { if let (Some(sender), Some(simulator)) = (&self.sender, simulator.as_ref()) { sender.send_throttle(&self.to_bytes()?, Some(simulator))?; } Ok(()) } pub fn to_bytes(&self) -> Result, Error> { let mut output = Vec::with_capacity(28); for value in [ self.resend, self.land, self.wind, self.cloud, self.task, self.texture, self.asset, ] { output.extend_from_slice(&value.to_le_bytes()); } Ok(output) } #[must_use] pub const fn asset(&self) -> f32 { self.asset } pub fn set_asset(&mut self, value: f32) { self.asset = value.clamp(10_000.0, 220_000.0); } #[must_use] pub const fn cloud(&self) -> f32 { self.cloud } pub fn set_cloud(&mut self, value: f32) { self.cloud = value.clamp(0.0, 34_000.0); } #[must_use] pub const fn land(&self) -> f32 { self.land } pub fn set_land(&mut self, value: f32) { self.land = value.clamp(0.0, 170_000.0); } #[must_use] pub const fn resend(&self) -> f32 { self.resend } pub fn set_resend(&mut self, value: f32) { self.resend = value.clamp(10_000.0, 150_000.0); } #[must_use] pub const fn task(&self) -> f32 { self.task } pub fn set_task(&mut self, value: f32) { self.task = value.clamp(4_000.0, 1_338_000.0); } #[must_use] pub const fn texture(&self) -> f32 { self.texture } pub fn set_texture(&mut self, value: f32) { self.texture = value.clamp(4_000.0, 446_000.0); } #[must_use] pub fn total(&self) -> f32 { self.resend + self.land + self.wind + self.cloud + self.task + self.texture + self.asset } pub fn set_total(&mut self, value: f32) { self.set_resend(value * 0.1); self.set_land(value * 0.52 / 3.0); self.set_wind(value * 0.05); self.set_cloud(value * 0.05); self.set_task(value * 0.704 / 3.0); self.set_texture(value * 0.704 / 3.0); self.set_asset(value * 0.484 / 3.0); } #[must_use] pub const fn wind(&self) -> f32 { self.wind } pub fn set_wind(&mut self, value: f32) { self.wind = value.clamp(0.0, 34_000.0); } } impl Default for AgentThrottle { fn default() -> Self { let mut value = Self { resend: 0.0, land: 0.0, wind: 0.0, cloud: 0.0, task: 0.0, texture: 0.0, asset: 0.0, sender: None, }; value.set_total(1_536_000.0); value } } impl fmt::Debug for AgentThrottle { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("AgentThrottle") .field("resend", &self.resend) .field("land", &self.land) .field("wind", &self.wind) .field("cloud", &self.cloud) .field("task", &self.task) .field("texture", &self.texture) .field("asset", &self.asset) .finish_non_exhaustive() } } fn read_f32(bytes: &[u8], offset: usize) -> Result { Ok(f32::from_le_bytes( bytes .get(offset..offset + 4) .ok_or(Error::IndexOutOfRange)? .try_into() .map_err(|_| Error::IndexOutOfRange)?, )) } /// Outgoing categories used by the native token buckets. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum UdpThrottleCategory { Unthrottled, Task, Texture, Asset, } impl UdpThrottleCategory { #[must_use] pub const fn classify(packet_type: PacketType) -> Self { match packet_type { PacketType::UseCircuitCode | PacketType::CompleteAgentMovement | PacketType::AgentThrottle | PacketType::LogoutRequest | PacketType::PacketAck | PacketType::StartPingCheck | PacketType::CompletePingCheck | PacketType::CloseCircuit => Self::Unthrottled, PacketType::RequestImage => Self::Texture, PacketType::TransferRequest | PacketType::AbortXfer => Self::Asset, _ => Self::Task, } } } /// Bounded transport policy. Defaults match the C# settings used by a client /// simulator, with explicit limits for queues and per-peer reliable state. #[derive(Clone, Debug)] pub struct UdpTransportConfig { pub receive_queue_capacity: usize, pub command_queue_capacity: usize, pub write_queue_capacity: usize, pub packet_archive_size: usize, pub pending_ack_capacity: usize, pub max_pending_acks: usize, pub reliable_window_capacity: usize, pub max_resend_count: u32, pub resend_timeout: Duration, pub network_tick_interval: Duration, pub max_datagram_size: usize, pub max_decoded_packet_size: usize, pub protocol_mtu: usize, pub max_peers: usize, pub throttle: AgentThrottle, } impl Default for UdpTransportConfig { fn default() -> Self { Self { receive_queue_capacity: 512, command_queue_capacity: 512, write_queue_capacity: 512, packet_archive_size: 1000, pending_ack_capacity: 255, max_pending_acks: 10, reliable_window_capacity: 1024, max_resend_count: 3, resend_timeout: Duration::from_secs(4), network_tick_interval: Duration::from_millis(500), max_datagram_size: usize::try_from(UDPPacketBuffer::DEFAULT_BUFFER_SIZE) .unwrap_or(4096), max_decoded_packet_size: DEFAULT_DECODE_BUFFER_SIZE, protocol_mtu: usize::try_from(Packet::MTU).unwrap_or(1200), max_peers: 32, throttle: AgentThrottle::default(), } } } impl UdpTransportConfig { fn validate(&self) -> Result<(), UdpTransportError> { for (name, value) in [ ("receive_queue_capacity", self.receive_queue_capacity), ("command_queue_capacity", self.command_queue_capacity), ("write_queue_capacity", self.write_queue_capacity), ("packet_archive_size", self.packet_archive_size), ("pending_ack_capacity", self.pending_ack_capacity), ("max_pending_acks", self.max_pending_acks), ("reliable_window_capacity", self.reliable_window_capacity), ("max_datagram_size", self.max_datagram_size), ("max_decoded_packet_size", self.max_decoded_packet_size), ("protocol_mtu", self.protocol_mtu), ("max_peers", self.max_peers), ] { if value == 0 { return Err(UdpTransportError::InvalidConfiguration(name)); } } if self.pending_ack_capacity > usize::from(u8::MAX) { return Err(UdpTransportError::InvalidConfiguration( "pending_ack_capacity", )); } if self.max_pending_acks > self.pending_ack_capacity { return Err(UdpTransportError::InvalidConfiguration("max_pending_acks")); } if self.protocol_mtu < 10 || self.protocol_mtu > self.max_datagram_size { return Err(UdpTransportError::InvalidConfiguration("protocol_mtu")); } if self.max_decoded_packet_size < self.max_datagram_size { return Err(UdpTransportError::InvalidConfiguration( "max_decoded_packet_size", )); } if self.resend_timeout.is_zero() { return Err(UdpTransportError::InvalidConfiguration("resend_timeout")); } if self.network_tick_interval.is_zero() { return Err(UdpTransportError::InvalidConfiguration( "network_tick_interval", )); } Ok(()) } } /// Immutable, redacted snapshot of transport counters. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct UdpTransportStats { pub received_datagrams: u64, pub received_bytes: u64, pub sent_datagrams: u64, pub sent_bytes: u64, pub dropped_receive_queue: u64, pub dropped_send_queue: u64, pub malformed_datagrams: u64, pub rejected_sources: u64, pub duplicate_datagrams: u64, pub out_of_order_datagrams: u64, pub sequence_gaps: u64, pub acknowledgements_received: u64, pub acknowledgements_sent: u64, pub resent_datagrams: u64, pub failed_resends: u64, pub socket_errors: u64, pub handler_panics: u64, } #[derive(Default)] struct StatsCounters { received_datagrams: AtomicU64, received_bytes: AtomicU64, sent_datagrams: AtomicU64, sent_bytes: AtomicU64, dropped_receive_queue: AtomicU64, dropped_send_queue: AtomicU64, malformed_datagrams: AtomicU64, rejected_sources: AtomicU64, duplicate_datagrams: AtomicU64, out_of_order_datagrams: AtomicU64, sequence_gaps: AtomicU64, acknowledgements_received: AtomicU64, acknowledgements_sent: AtomicU64, resent_datagrams: AtomicU64, failed_resends: AtomicU64, socket_errors: AtomicU64, handler_panics: AtomicU64, } impl StatsCounters { fn snapshot(&self) -> UdpTransportStats { let load = |counter: &AtomicU64| counter.load(Ordering::Relaxed); UdpTransportStats { received_datagrams: load(&self.received_datagrams), received_bytes: load(&self.received_bytes), sent_datagrams: load(&self.sent_datagrams), sent_bytes: load(&self.sent_bytes), dropped_receive_queue: load(&self.dropped_receive_queue), dropped_send_queue: load(&self.dropped_send_queue), malformed_datagrams: load(&self.malformed_datagrams), rejected_sources: load(&self.rejected_sources), duplicate_datagrams: load(&self.duplicate_datagrams), out_of_order_datagrams: load(&self.out_of_order_datagrams), sequence_gaps: load(&self.sequence_gaps), acknowledgements_received: load(&self.acknowledgements_received), acknowledgements_sent: load(&self.acknowledgements_sent), resent_datagrams: load(&self.resent_datagrams), failed_resends: load(&self.failed_resends), socket_errors: load(&self.socket_errors), handler_panics: load(&self.handler_panics), } } } /// Callback boundary corresponding to the protected methods on C# `UDPBase`. pub trait UdpPacketHandler: Send + Sync + 'static { fn packet_received(&self, _buffer: UDPPacketBuffer) {} fn packet_sent(&self, _buffer: UDPPacketBuffer, _bytes_sent: usize) {} fn packet_dropped(&self) {} } struct NoopPacketHandler; impl UdpPacketHandler for NoopPacketHandler {} struct RuntimeTasks { cancellation: CancellationTokenSource, commands: mpsc::Sender, handles: Vec>, local_address: SocketAddr, } struct UdpInner { bind_address: SocketAddr, remote_end_point: Option, config: UdpTransportConfig, handler: Arc, parent_cancellation: CancellationToken, running: AtomicBool, runtime: Mutex>, stats: Arc, } impl Drop for UdpInner { fn drop(&mut self) { if let Some(runtime) = self .runtime .get_mut() .unwrap_or_else(std::sync::PoisonError::into_inner) .take() { runtime.cancellation.cancel(); for handle in runtime.handles { handle.abort(); } } } } /// Runtime-owning native implementation of the C# UDP transport slice. /// /// Construction creates no socket and no runtime. `start` binds a nonblocking /// socket and spawns tasks on the caller's current Tokio runtime. #[derive(Clone)] pub struct UDPBase { inner: Arc, } impl UDPBase { pub fn client( remote_end_point: SocketAddr, config: UdpTransportConfig, handler: Arc, parent_cancellation: CancellationToken, ) -> Result { let bind_address = match remote_end_point.ip() { IpAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), IpAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), }; Self::new( bind_address, Some(remote_end_point), config, handler, parent_cancellation, ) } pub fn server( bind_address: SocketAddr, config: UdpTransportConfig, handler: Arc, parent_cancellation: CancellationToken, ) -> Result { Self::new(bind_address, None, config, handler, parent_cancellation) } pub fn client_with_defaults(remote_end_point: SocketAddr) -> Result { Self::client( remote_end_point, UdpTransportConfig::default(), Arc::new(NoopPacketHandler), CancellationToken::default(), ) } fn new( bind_address: SocketAddr, remote_end_point: Option, config: UdpTransportConfig, handler: Arc, parent_cancellation: CancellationToken, ) -> Result { config.validate()?; Ok(Self { inner: Arc::new(UdpInner { bind_address, remote_end_point, config, handler, parent_cancellation, running: AtomicBool::new(false), runtime: Mutex::new(None), stats: Arc::new(StatsCounters::default()), }), }) } pub fn start(&self) -> Result<(), Error> { self.start_transport().map_err(Into::into) } pub fn start_transport(&self) -> Result<(), UdpTransportError> { let mut runtime = self .inner .runtime .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if runtime.is_some() { return Ok(()); } if self.inner.parent_cancellation.is_cancellation_requested() { return Err(UdpTransportError::Cancelled); } let handle = tokio::runtime::Handle::try_current() .map_err(|_| UdpTransportError::RuntimeUnavailable)?; let std_socket = std::net::UdpSocket::bind(self.inner.bind_address) .map_err(|_| UdpTransportError::Socket)?; std_socket .set_nonblocking(true) .map_err(|_| UdpTransportError::Socket)?; let local_address = std_socket .local_addr() .map_err(|_| UdpTransportError::Socket)?; let socket = Arc::new(UdpSocket::from_std(std_socket).map_err(|_| UdpTransportError::Socket)?); let cancellation = CancellationTokenSource::new_linked(std::slice::from_ref( &self.inner.parent_cancellation, )); let token = cancellation.token(); let (raw_sender, raw_receiver) = mpsc::channel(self.inner.config.receive_queue_capacity); let (command_sender, command_receiver) = mpsc::channel(self.inner.config.command_queue_capacity); let (write_sender, write_receiver) = mpsc::channel(self.inner.config.write_queue_capacity); let receiver_handle = handle.spawn(receive_loop( Arc::clone(&socket), raw_sender, token.clone(), self.inner.config.max_datagram_size, Arc::clone(&self.inner.stats), Arc::clone(&self.inner.handler), )); let coordinator_handle = handle.spawn(coordinator_loop( raw_receiver, command_receiver, write_sender, token.clone(), self.inner.remote_end_point, self.inner.config.clone(), Arc::clone(&self.inner.stats), Arc::clone(&self.inner.handler), )); let writer_handle = handle.spawn(writer_loop( socket, write_receiver, token, self.inner.config.throttle.clone(), Arc::clone(&self.inner.stats), Arc::clone(&self.inner.handler), )); *runtime = Some(RuntimeTasks { cancellation, commands: command_sender, handles: vec![receiver_handle, coordinator_handle, writer_handle], local_address, }); self.inner.running.store(true, Ordering::Release); Ok(()) } pub fn stop(&self) -> Result<(), Error> { self.stop_transport(); Ok(()) } pub fn stop_transport(&self) { let runtime = self .inner .runtime .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .take(); self.inner.running.store(false, Ordering::Release); if let Some(runtime) = runtime { runtime.cancellation.cancel(); for handle in runtime.handles { handle.abort(); } } } pub async fn stop_async(&self) { let runtime = self .inner .runtime .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .take(); self.inner.running.store(false, Ordering::Release); if let Some(runtime) = runtime { runtime.cancellation.cancel(); drop(runtime.commands); for handle in runtime.handles { let _ = handle.await; } } } #[must_use] pub fn is_running(&self) -> bool { self.inner.running.load(Ordering::Acquire) } #[must_use] pub fn local_address(&self) -> Option { self.inner .runtime .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .as_ref() .map(|runtime| runtime.local_address) } #[must_use] pub fn stats(&self) -> UdpTransportStats { self.inner.stats.snapshot() } pub fn async_begin_send(&self, buf: UDPPacketBuffer) -> Result<(), Error> { let Some(sender) = self.command_sender() else { return Ok(()); }; sender .try_send(CoordinatorCommand::Raw(buf)) .map_err(|error| { if matches!(error, mpsc::error::TrySendError::Full(_)) { self.inner .stats .dropped_send_queue .fetch_add(1, Ordering::Relaxed); Error::InvalidOperation } else { Error::Cancelled } }) } pub async fn send_packet( &self, data: Vec, packet_type: PacketType, do_zerocode: bool, cancellation: CancellationToken, ) -> Result { let destination = self .inner .remote_end_point .ok_or(UdpTransportError::InvalidBuffer)?; self.send_packet_to(data, destination, packet_type, do_zerocode, cancellation) .await } pub async fn send_packet_to( &self, data: Vec, destination: SocketAddr, packet_type: PacketType, do_zerocode: bool, cancellation: CancellationToken, ) -> Result { let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?; let (response_sender, response_receiver) = oneshot::channel(); let command = CoordinatorCommand::Packet { data, destination, packet_type, do_zerocode, response: response_sender, acknowledgement: None, }; tokio::select! { result = sender.send(command) => { result.map_err(|_| UdpTransportError::Cancelled)?; } () = cancellation.cancelled() => return Err(UdpTransportError::Cancelled), () = self.inner.parent_cancellation.cancelled() => { return Err(UdpTransportError::Cancelled); } } tokio::select! { result = response_receiver => result.map_err(|_| UdpTransportError::Cancelled)?, () = cancellation.cancelled() => Err(UdpTransportError::Cancelled), () = self.inner.parent_cancellation.cancelled() => Err(UdpTransportError::Cancelled), } } pub fn try_send_packet( &self, data: Vec, destination: SocketAddr, packet_type: PacketType, do_zerocode: bool, ) -> Result>, UdpTransportError> { let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?; let (response, receiver) = oneshot::channel(); sender .try_send(CoordinatorCommand::Packet { data, destination, packet_type, do_zerocode, response, acknowledgement: None, }) .map_err(|error| { if matches!(error, mpsc::error::TrySendError::Full(_)) { self.inner .stats .dropped_send_queue .fetch_add(1, Ordering::Relaxed); UdpTransportError::Backpressure } else { UdpTransportError::Cancelled } })?; Ok(receiver) } /// Queues a packet and resolves only after its reliable sequence is /// acknowledged by the peer. The receiver disconnects if the reliable /// packet is dropped or the transport shuts down. pub(crate) fn try_send_packet_wait_ack( &self, data: Vec, destination: SocketAddr, packet_type: PacketType, do_zerocode: bool, ) -> Result, UdpTransportError> { let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?; let (response, _queued) = oneshot::channel(); let (acknowledgement, receiver) = sync_channel(1); sender .try_send(CoordinatorCommand::Packet { data, destination, packet_type, do_zerocode, response, acknowledgement: Some(acknowledgement), }) .map_err(|error| { if matches!(error, mpsc::error::TrySendError::Full(_)) { self.inner .stats .dropped_send_queue .fetch_add(1, Ordering::Relaxed); UdpTransportError::Backpressure } else { UdpTransportError::Cancelled } })?; Ok(receiver) } /// Queues an immediate flush of acknowledgements pending for `destination`. /// /// The network manager uses this for the protocol's `OldestUnacked` /// request in `StartPingCheck`, matching `Simulator.SendAcks()` without /// exposing coordinator state or blocking the packet callback thread. pub(crate) fn try_flush_acks(&self, destination: SocketAddr) -> Result<(), UdpTransportError> { let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?; sender .try_send(CoordinatorCommand::FlushAcks { destination }) .map_err(|error| { if matches!(error, mpsc::error::TrySendError::Full(_)) { self.inner .stats .dropped_send_queue .fetch_add(1, Ordering::Relaxed); UdpTransportError::Backpressure } else { UdpTransportError::Cancelled } }) } pub fn update_throttle(&self, throttle: AgentThrottle) -> Result<(), UdpTransportError> { let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?; sender .try_send(CoordinatorCommand::UpdateThrottle(throttle)) .map_err(|error| { if matches!(error, mpsc::error::TrySendError::Full(_)) { UdpTransportError::Backpressure } else { UdpTransportError::Cancelled } }) } fn command_sender(&self) -> Option> { self.inner .runtime .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .as_ref() .map(|runtime| runtime.commands.clone()) } } impl fmt::Debug for UDPBase { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("UDPBase") .field( "mode", &self.inner.remote_end_point.map_or("server", |_| "client"), ) .field("is_running", &self.is_running()) .field("stats", &self.stats()) .finish_non_exhaustive() } } enum CoordinatorCommand { Raw(UDPPacketBuffer), Packet { data: Vec, destination: SocketAddr, packet_type: PacketType, do_zerocode: bool, response: oneshot::Sender>, acknowledgement: Option>, }, FlushAcks { destination: SocketAddr, }, UpdateThrottle(AgentThrottle), } enum WriteCommand { Datagram { buffer: UDPPacketBuffer, category: UdpThrottleCategory, }, UpdateThrottle(AgentThrottle), } struct ReliablePacket { buffer: UDPPacketBuffer, category: UdpThrottleCategory, last_sent: Instant, resend_count: u32, acknowledgement: Option>, } struct PeerState { sequence: u32, archive: IncomingPacketIDCollection, pending_acks: VecDeque, need_ack: BTreeMap, latest_received: Option, } impl PeerState { fn new(config: &UdpTransportConfig) -> Result { Ok(Self { sequence: 0, archive: IncomingPacketIDCollection::new( i32::try_from(config.packet_archive_size) .map_err(|_| UdpTransportError::InvalidConfiguration("packet_archive_size"))?, ) .map_err(|_| UdpTransportError::InvalidConfiguration("packet_archive_size"))?, pending_acks: VecDeque::with_capacity(config.pending_ack_capacity), need_ack: BTreeMap::new(), latest_received: None, }) } fn next_sequence(&mut self) -> u32 { // C# increments an Int32 and casts it to UInt32 for the four-byte // header. The equivalent wire sequence spans all u32 values and wraps // through zero. self.sequence = self.sequence.wrapping_add(1); self.sequence } fn observe_sequence(&mut self, sequence: u32, stats: &StatsCounters) { let Some(latest) = self.latest_received else { self.latest_received = Some(sequence); return; }; let expected = latest.wrapping_add(1); if sequence == expected { self.latest_received = Some(sequence); return; } let forward = sequence.wrapping_sub(latest); if forward != 0 && forward <= (u32::MAX / 2) + 1 { stats.sequence_gaps.fetch_add(1, Ordering::Relaxed); self.latest_received = Some(sequence); } else { stats.out_of_order_datagrams.fetch_add(1, Ordering::Relaxed); } } } struct CoordinatorState { peers: HashMap, remote_end_point: Option, config: UdpTransportConfig, stats: Arc, } impl CoordinatorState { fn canonical_peer(&self, source: SocketAddr) -> Result { if let Some(remote) = self.remote_end_point { if remote.ip() != source.ip() { return Err(UdpTransportError::InvalidBuffer); } Ok(remote) } else { Ok(source) } } fn peer_mut(&mut self, endpoint: SocketAddr) -> Result<&mut PeerState, UdpTransportError> { if !self.peers.contains_key(&endpoint) { if self.peers.len() >= self.config.max_peers { return Err(UdpTransportError::TooManyPeers); } let peer = PeerState::new(&self.config)?; self.peers.insert(endpoint, peer); } self.peers .get_mut(&endpoint) .ok_or(UdpTransportError::TooManyPeers) } fn prepare_packet( &mut self, data: Vec, destination: SocketAddr, packet_type: PacketType, do_zerocode: bool, acknowledgement: Option>, ) -> Result<(WriteCommand, u32), UdpTransportError> { let mut data = encode_for_transport(data, do_zerocode, self.config.protocol_mtu)?; if data.len() < 6 { return Err(UdpTransportError::InvalidBuffer); } let reliable = data[0] & Helpers::MSG_RELIABLE != 0; let category = UdpThrottleCategory::classify(packet_type); let reliable_capacity = self.config.reliable_window_capacity; let protocol_mtu = self.config.protocol_mtu; let peer = self.peer_mut(destination)?; if reliable && peer.need_ack.len() >= reliable_capacity { return Err(UdpTransportError::ReliableWindowFull); } append_pending_acks(&mut data, &mut peer.pending_acks, protocol_mtu)?; let sequence = peer.next_sequence(); data[1..5].copy_from_slice(&sequence.to_be_bytes()); let data_length = i32::try_from(data.len()).map_err(|_| UdpTransportError::InvalidBuffer)?; let buffer = UDPPacketBuffer { data, data_length, remote_end_point: destination, }; if reliable { peer.need_ack.insert( sequence, ReliablePacket { buffer: buffer.clone(), category, last_sent: Instant::now(), resend_count: 0, acknowledgement, }, ); } else if let Some(acknowledgement) = acknowledgement { let _ = acknowledgement.try_send(()); } Ok((WriteCommand::Datagram { buffer, category }, sequence)) } fn take_resends(&mut self) -> Vec { let now = Instant::now(); let mut writes = Vec::new(); for peer in self.peers.values_mut() { let mut failed = Vec::new(); for (&sequence, packet) in &mut peer.need_ack { if now.duration_since(packet.last_sent) <= self.config.resend_timeout { continue; } if packet.resend_count < self.config.max_resend_count { packet.resend_count += 1; packet.last_sent = now; if let Some(flags) = packet.buffer.data.first_mut() { *flags |= Helpers::MSG_RESENT; } self.stats.resent_datagrams.fetch_add(1, Ordering::Relaxed); writes.push(WriteCommand::Datagram { buffer: packet.buffer.clone(), category: packet.category, }); } else { failed.push(sequence); } } for sequence in failed { peer.need_ack.remove(&sequence); self.stats.failed_resends.fetch_add(1, Ordering::Relaxed); } } writes } } fn encode_for_transport( mut data: Vec, do_zerocode: bool, mtu: usize, ) -> Result, UdpTransportError> { if data.len() < 6 { return Err(UdpTransportError::InvalidBuffer); } if data.len() > mtu { return Err(UdpTransportError::MtuExceeded); } if !do_zerocode { return Ok(data); } data[0] |= Helpers::MSG_ZEROCODED; let mut encoded = vec![0_u8; mtu]; let source_length = i32::try_from(data.len()).map_err(|_| UdpTransportError::InvalidBuffer)?; match crate::packet_wire::zero_encode(Some(&data), source_length, Some(&mut encoded)) { Ok(length) => { let length = usize::try_from(length).map_err(|_| UdpTransportError::InvalidBuffer)?; encoded.truncate(length); Ok(encoded) } Err(Error::IndexOutOfRange) => { data[0] &= !Helpers::MSG_ZEROCODED; Ok(data) } Err(_) => Err(UdpTransportError::InvalidBuffer), } } fn append_pending_acks( data: &mut Vec, pending: &mut VecDeque, mtu: usize, ) -> Result<(), UdpTransportError> { if data .first() .is_some_and(|flags| flags & Helpers::MSG_APPENDED_ACKS != 0) { return Err(UdpTransportError::InvalidBuffer); } let mut count = 0_u8; while data.len().checked_add(5).is_some_and(|length| length < mtu) { let Some(ack) = pending.pop_front() else { break; }; data.extend_from_slice(&ack.to_be_bytes()); count = count.saturating_add(1); if count == u8::MAX { break; } } if count != 0 { data.push(count); data[0] |= Helpers::MSG_APPENDED_ACKS; } Ok(()) } async fn receive_loop( socket: Arc, sender: mpsc::Sender, cancellation: CancellationToken, max_datagram_size: usize, stats: Arc, handler: Arc, ) { let mut storage = vec![0_u8; max_datagram_size.saturating_add(1)]; loop { let received = tokio::select! { () = cancellation.cancelled() => break, result = socket.recv_from(&mut storage) => result, }; let Ok((length, source)) = received else { if !cancellation.is_cancellation_requested() { stats.socket_errors.fetch_add(1, Ordering::Relaxed); } break; }; stats.received_datagrams.fetch_add(1, Ordering::Relaxed); stats .received_bytes .fetch_add(u64::try_from(length).unwrap_or(u64::MAX), Ordering::Relaxed); if length > max_datagram_size { stats.malformed_datagrams.fetch_add(1, Ordering::Relaxed); continue; } let packet = UDPPacketBuffer { data: storage[..length].to_vec(), data_length: i32::try_from(length).unwrap_or(i32::MAX), remote_end_point: source, }; if sender.try_send(packet).is_err() { stats.dropped_receive_queue.fetch_add(1, Ordering::Relaxed); invoke_handler(&stats, || handler.packet_dropped()); } } } #[allow(clippy::too_many_arguments)] // Each argument is one explicit task ownership boundary. async fn coordinator_loop( mut incoming: mpsc::Receiver, mut commands: mpsc::Receiver, writer: mpsc::Sender, cancellation: CancellationToken, remote_end_point: Option, config: UdpTransportConfig, stats: Arc, handler: Arc, ) { let mut coordinator = CoordinatorState { peers: HashMap::new(), remote_end_point, config: config.clone(), stats: Arc::clone(&stats), }; let mut tick = tokio::time::interval(config.network_tick_interval); tick.set_missed_tick_behavior(MissedTickBehavior::Skip); tick.tick().await; loop { tokio::select! { biased; () = cancellation.cancelled() => break, Some(buffer) = incoming.recv() => { process_incoming(&mut coordinator, buffer, &writer, &handler).await; } Some(command) = commands.recv() => { process_command(&mut coordinator, command, &writer).await; } _ = tick.tick() => { flush_pending_acks(&mut coordinator, &writer).await; for resend in coordinator.take_resends() { if writer.send(resend).await.is_err() { return; } } } else => break, } } } async fn process_command( state: &mut CoordinatorState, command: CoordinatorCommand, writer: &mpsc::Sender, ) { match command { CoordinatorCommand::Raw(buffer) => { let valid = buffer.payload().is_ok() && usize::try_from(buffer.data_length) .is_ok_and(|length| length <= state.config.max_datagram_size); if !valid { state .stats .dropped_send_queue .fetch_add(1, Ordering::Relaxed); return; } let _ = writer .send(WriteCommand::Datagram { buffer, category: UdpThrottleCategory::Unthrottled, }) .await; } CoordinatorCommand::Packet { data, destination, packet_type, do_zerocode, response, acknowledgement, } => { let result = state.prepare_packet(data, destination, packet_type, do_zerocode, acknowledgement); let result = match result { Ok((write, sequence)) => writer .send(write) .await .map(|()| sequence) .map_err(|_| UdpTransportError::Cancelled), Err(error) => Err(error), }; let _ = response.send(result); } CoordinatorCommand::FlushAcks { destination } => { send_peer_acks(state, destination, writer).await; } CoordinatorCommand::UpdateThrottle(throttle) => { let _ = writer.send(WriteCommand::UpdateThrottle(throttle)).await; } } } async fn process_incoming( state: &mut CoordinatorState, buffer: UDPPacketBuffer, writer: &mpsc::Sender, handler: &Arc, ) { let Ok(endpoint) = state.canonical_peer(buffer.remote_end_point) else { state.stats.rejected_sources.fetch_add(1, Ordering::Relaxed); return; }; let Ok(payload) = buffer.payload() else { state .stats .malformed_datagrams .fetch_add(1, Ordering::Relaxed); return; }; let Ok(payload_length) = i32::try_from(payload.len()) else { state .stats .malformed_datagrams .fetch_add(1, Ordering::Relaxed); return; }; let mut packet_end = payload_length - 1; let mut zero_buffer = vec![0_u8; state.config.max_decoded_packet_size]; let Ok(packet) = crate::packet_wire::build_packet_from_bytes(payload, &mut packet_end, &mut zero_buffer) else { state .stats .malformed_datagrams .fetch_add(1, Ordering::Relaxed); return; }; let appended_acks = packet.header.ack_list.clone().unwrap_or_default(); let mut standalone_acks = Vec::new(); if packet.type_ == PacketType::PacketAck { let mut position = 0; if let Ok(ack_packet) = PacketAckPacket::new_with_bytes_int32(payload.to_vec(), &mut position) { standalone_acks.extend(ack_packet.packets.into_iter().map(|block| block.id)); } else { state .stats .malformed_datagrams .fetch_add(1, Ordering::Relaxed); return; } } let pending_threshold = state.config.max_pending_acks; let pending_capacity = state.config.pending_ack_capacity; let counters = Arc::clone(&state.stats); let Ok(peer) = state.peer_mut(endpoint) else { counters.rejected_sources.fetch_add(1, Ordering::Relaxed); return; }; for ack in appended_acks.into_iter().chain(standalone_acks) { if let Some(mut reliable) = peer.need_ack.remove(&ack) && let Some(acknowledgement) = reliable.acknowledgement.take() { let _ = acknowledgement.try_send(()); } counters .acknowledgements_received .fetch_add(1, Ordering::Relaxed); } if packet.header.reliable { if peer.pending_acks.len() < pending_capacity { peer.pending_acks.push_back(packet.header.sequence); } if !peer.archive.try_enqueue(packet.header.sequence) { counters.duplicate_datagrams.fetch_add(1, Ordering::Relaxed); if peer.pending_acks.len() >= pending_threshold { send_peer_acks(state, endpoint, writer).await; } return; } peer.observe_sequence(packet.header.sequence, &counters); } if state .peers .get(&endpoint) .is_some_and(|peer| peer.pending_acks.len() >= pending_threshold) { send_peer_acks(state, endpoint, writer).await; } invoke_handler(&state.stats, || handler.packet_received(buffer)); } async fn flush_pending_acks(state: &mut CoordinatorState, writer: &mpsc::Sender) { let endpoints: Vec<_> = state .peers .iter() .filter_map(|(endpoint, peer)| (!peer.pending_acks.is_empty()).then_some(*endpoint)) .collect(); for endpoint in endpoints { send_peer_acks(state, endpoint, writer).await; } } async fn send_peer_acks( state: &mut CoordinatorState, endpoint: SocketAddr, writer: &mpsc::Sender, ) { let Some(peer) = state.peers.get_mut(&endpoint) else { return; }; if peer.pending_acks.is_empty() { return; } let mut blocks = Vec::with_capacity(peer.pending_acks.len()); while let Some(id) = peer.pending_acks.pop_front() { blocks.push(PacketAckPacketPacketsBlock { id }); } let ack_count = blocks.len(); let Ok(mut packet) = PacketAckPacket::new_with_constructor() else { return; }; packet.packets = blocks; let bytes = match packet.to_bytes_with_method() { Ok(mut bytes) => { // C# `SendAcks` explicitly marks standalone PacketAck packets as // unreliable before serialization. if let Some(flags) = bytes.first_mut() { *flags &= !Helpers::MSG_RELIABLE; } bytes } Err(_) => return, }; let Ok((write, _)) = state.prepare_packet(bytes, endpoint, PacketType::PacketAck, false, None) else { return; }; if writer.send(write).await.is_ok() { state.stats.acknowledgements_sent.fetch_add( u64::try_from(ack_count).unwrap_or(u64::MAX), Ordering::Relaxed, ); } } fn invoke_handler(stats: &StatsCounters, callback: impl FnOnce()) { if catch_unwind(AssertUnwindSafe(callback)).is_err() { stats.handler_panics.fetch_add(1, Ordering::Relaxed); } } struct TokenBucket { tokens_per_period: usize, token_limit: usize, available: usize, last_replenishment: Instant, } impl TokenBucket { #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn new(bits_per_second: f32) -> Self { let calculated = (f64::from(bits_per_second) / 8.0 * THROTTLE_PERIOD.as_secs_f64()).max(0.0) as usize; let tokens_per_period = calculated.max(THROTTLE_MIN_BYTES_PER_PERIOD); let token_limit = tokens_per_period.saturating_mul(THROTTLE_BURST_PERIODS); Self { tokens_per_period, token_limit, available: token_limit, last_replenishment: Instant::now(), } } fn replenish(&mut self, now: Instant) { let elapsed = now.duration_since(self.last_replenishment); let periods = elapsed.as_nanos() / THROTTLE_PERIOD.as_nanos(); if periods == 0 { return; } let periods = usize::try_from(periods).unwrap_or(usize::MAX); self.available = self .available .saturating_add(self.tokens_per_period.saturating_mul(periods)) .min(self.token_limit); let periods_u32 = u32::try_from(periods).unwrap_or(u32::MAX); self.last_replenishment += THROTTLE_PERIOD.saturating_mul(periods_u32); } async fn acquire( &mut self, amount: usize, cancellation: &CancellationToken, ) -> Result<(), UdpTransportError> { let amount = amount.clamp(1, self.token_limit); loop { self.replenish(Instant::now()); if self.available >= amount { self.available -= amount; return Ok(()); } let deficit = amount - self.available; let periods = deficit.div_ceil(self.tokens_per_period); let periods = u32::try_from(periods).unwrap_or(u32::MAX); let deadline = self.last_replenishment + THROTTLE_PERIOD.saturating_mul(periods.max(1)); tokio::select! { () = cancellation.cancelled() => return Err(UdpTransportError::Cancelled), () = tokio::time::sleep_until(deadline) => {} } } } } struct UdpThrottle { task: TokenBucket, texture: TokenBucket, asset: TokenBucket, } impl UdpThrottle { fn new(throttle: &AgentThrottle) -> Self { Self { task: TokenBucket::new(throttle.task()), texture: TokenBucket::new(throttle.texture()), asset: TokenBucket::new(throttle.asset()), } } async fn acquire( &mut self, category: UdpThrottleCategory, amount: usize, cancellation: &CancellationToken, ) -> Result<(), UdpTransportError> { match category { UdpThrottleCategory::Unthrottled => Ok(()), UdpThrottleCategory::Task => self.task.acquire(amount, cancellation).await, UdpThrottleCategory::Texture => self.texture.acquire(amount, cancellation).await, UdpThrottleCategory::Asset => self.asset.acquire(amount, cancellation).await, } } } async fn writer_loop( socket: Arc, mut receiver: mpsc::Receiver, cancellation: CancellationToken, initial_throttle: AgentThrottle, stats: Arc, handler: Arc, ) { let mut throttle = UdpThrottle::new(&initial_throttle); loop { let command = tokio::select! { biased; () = cancellation.cancelled() => break, command = receiver.recv() => command, }; match command { Some(WriteCommand::UpdateThrottle(values)) => { throttle = UdpThrottle::new(&values); } Some(WriteCommand::Datagram { buffer, category }) => { let Ok(payload) = buffer.payload() else { stats.dropped_send_queue.fetch_add(1, Ordering::Relaxed); continue; }; if throttle .acquire(category, payload.len(), &cancellation) .await .is_err() { break; } match socket.send_to(payload, buffer.remote_end_point).await { Ok(bytes_sent) => { stats.sent_datagrams.fetch_add(1, Ordering::Relaxed); stats.sent_bytes.fetch_add( u64::try_from(bytes_sent).unwrap_or(u64::MAX), Ordering::Relaxed, ); invoke_handler(&stats, || handler.packet_sent(buffer, bytes_sent)); } Err(_) => { stats.socket_errors.fetch_add(1, Ordering::Relaxed); } } } None => break, } } } #[cfg(test)] mod tests { use super::*; #[test] fn sequence_rolls_over_across_the_full_reference_header_width() { let config = UdpTransportConfig::default(); let mut peer = PeerState::new(&config).unwrap(); peer.sequence = u32::MAX - 1; assert_eq!(peer.next_sequence(), u32::MAX); assert_eq!(peer.next_sequence(), 0); assert_eq!(peer.next_sequence(), 1); } #[tokio::test(start_paused = true)] async fn fake_time_drives_token_replenishment_and_reliable_expiry() { let cancellation = CancellationToken::default(); let mut bucket = TokenBucket::new(4_000.0); bucket.acquire(800, &cancellation).await.unwrap(); let started = Instant::now(); bucket.acquire(800, &cancellation).await.unwrap(); assert_eq!( Instant::now().duration_since(started), Duration::from_millis(400) ); let config = UdpTransportConfig { resend_timeout: Duration::from_secs(4), max_resend_count: 1, ..UdpTransportConfig::default() }; let mut coordinator = CoordinatorState { peers: HashMap::new(), remote_end_point: None, config, stats: Arc::new(StatsCounters::default()), }; let destination: SocketAddr = "127.0.0.1:13000".parse().unwrap(); let packet = vec![Helpers::MSG_RELIABLE, 0, 0, 0, 0, 0, 1]; coordinator .prepare_packet(packet, destination, PacketType::ObjectUpdate, false, None) .unwrap(); assert!(coordinator.take_resends().is_empty()); tokio::time::advance(Duration::from_millis(4_001)).await; let resend = coordinator.take_resends(); assert_eq!(resend.len(), 1); let WriteCommand::Datagram { buffer, .. } = &resend[0] else { panic!("expected resend datagram"); }; assert_ne!(buffer.data[0] & Helpers::MSG_RESENT, 0); assert_eq!(u32::from_be_bytes(buffer.data[1..5].try_into().unwrap()), 1); tokio::time::advance(Duration::from_millis(4_001)).await; assert!(coordinator.take_resends().is_empty()); assert_eq!(coordinator.stats.failed_resends.load(Ordering::Relaxed), 1); assert!(coordinator.peers[&destination].need_ack.is_empty()); } }