Implement native UDP transport reliability (#52)
Some checks failed
Native code generation / deterministic (push) Failing after 7m48s
Imaging and meshing gate / native (push) Failing after 17s
JPEG 2000 feature / linux (push) Failing after 58s
Skia feature / linux (push) Failing after 1m33s

This commit is contained in:
2026-08-09 11:39:59 +00:00
parent b413cd6f4d
commit c7777d42f5
13 changed files with 2593 additions and 214 deletions

View File

@@ -18,6 +18,10 @@ libremetaverse-imaging = { path = "../libremetaverse-imaging" }
libremetaverse-structured-data = { path = "../libremetaverse-structured-data" }
libremetaverse-types = { path = "../libremetaverse-types" }
roxmltree = "0.21.1"
tokio = { version = "1.47.1", features = ["macros", "net", "rt", "sync", "time"] }
[dev-dependencies]
tokio = { version = "1.47.1", features = ["macros", "net", "rt-multi-thread", "sync", "test-util", "time"] }
[lints]
workspace = true

View File

@@ -113,6 +113,7 @@ struct ClientRuntime {
state: AtomicU8,
cancellation: CancellationTokenSource,
services: Mutex<Vec<ServiceRegistration>>,
agent_throttle_sender: Mutex<Option<Arc<dyn crate::udp_transport::AgentThrottleSender>>>,
shutdown_complete: Condvar,
shutdown_wait: Mutex<()>,
}
@@ -132,6 +133,7 @@ impl ClientRuntime {
})
.collect(),
),
agent_throttle_sender: Mutex::new(None),
shutdown_complete: Condvar::new(),
shutdown_wait: Mutex::new(()),
}
@@ -200,6 +202,10 @@ impl ClientRuntime {
}
}
services.clear();
self.agent_throttle_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
let shutdown_wait = self
.shutdown_wait
.lock()
@@ -257,6 +263,41 @@ impl GridClient {
self.runtime.register(service)
}
/// Installs the network-owned callback used by mapped `AgentThrottle.Set`.
///
/// # Errors
///
/// Registration fails after client shutdown begins.
pub fn set_agent_throttle_sender(
&mut self,
sender: Arc<dyn crate::udp_transport::AgentThrottleSender>,
) -> Result<(), ClientCoreError> {
let mut registered = self
.runtime
.agent_throttle_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let state = self.lifecycle_state();
if state != ClientLifecycleState::Active {
return Err(ClientCoreError::InvalidLifecycle {
operation: "register the agent throttle sender",
state,
});
}
*registered = Some(sender);
Ok(())
}
pub(crate) fn agent_throttle_sender(
&self,
) -> Option<Arc<dyn crate::udp_transport::AgentThrottleSender>> {
self.runtime
.agent_throttle_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn shutdown(&self) -> Result<(), ClientCoreError> {
self.runtime.shutdown()
}

View File

@@ -3333,102 +3333,20 @@ impl AgentState {
}
/// C# type: `T:LibreMetaverse.AgentThrottle`.
pub struct AgentThrottle;
impl AgentThrottle {
/// C# member: `M:LibreMetaverse.AgentThrottle.#ctor(LibreMetaverse.GridClient)`.
pub fn new_with_grid_client(client: libremetaverse::GridClient) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.AgentThrottle.#ctor(LibreMetaverse.GridClient)",
)
}
/// C# member: `M:LibreMetaverse.AgentThrottle.#ctor(System.Byte[],System.Int32)`.
pub fn new_with_bytes_int32(data: Vec<u8>, pos: i32) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.AgentThrottle.#ctor(System.Byte[],System.Int32)",
)
}
/// C# member: `M:LibreMetaverse.AgentThrottle.Set`.
pub fn set_with_method(&self) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.AgentThrottle.Set")
}
/// C# member: `M:LibreMetaverse.AgentThrottle.Set(LibreMetaverse.Simulator)`.
pub fn set_with_simulator(
&self,
simulator: Option<libremetaverse::Simulator>,
) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.AgentThrottle.Set(LibreMetaverse.Simulator)",
)
}
/// C# member: `M:LibreMetaverse.AgentThrottle.ToBytes`.
pub fn to_bytes(&self) -> Result<Vec<u8>, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.AgentThrottle.ToBytes")
}
/// C# member: `P:LibreMetaverse.AgentThrottle.Asset`.
pub fn asset(&self) -> f32 {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Asset")
}
/// Setter for C# member: `P:LibreMetaverse.AgentThrottle.Asset`.
pub fn set_asset(&mut self, value: f32) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Asset")
}
/// C# member: `P:LibreMetaverse.AgentThrottle.Cloud`.
pub fn cloud(&self) -> f32 {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Cloud")
}
/// Setter for C# member: `P:LibreMetaverse.AgentThrottle.Cloud`.
pub fn set_cloud(&mut self, value: f32) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Cloud")
}
/// C# member: `P:LibreMetaverse.AgentThrottle.Land`.
pub fn land(&self) -> f32 {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Land")
}
/// Setter for C# member: `P:LibreMetaverse.AgentThrottle.Land`.
pub fn set_land(&mut self, value: f32) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Land")
}
/// C# member: `P:LibreMetaverse.AgentThrottle.Resend`.
pub fn resend(&self) -> f32 {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Resend")
}
/// Setter for C# member: `P:LibreMetaverse.AgentThrottle.Resend`.
pub fn set_resend(&mut self, value: f32) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Resend")
}
/// C# member: `P:LibreMetaverse.AgentThrottle.Task`.
pub fn task(&self) -> f32 {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Task")
}
/// Setter for C# member: `P:LibreMetaverse.AgentThrottle.Task`.
pub fn set_task(&mut self, value: f32) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Task")
}
/// C# member: `P:LibreMetaverse.AgentThrottle.Texture`.
pub fn texture(&self) -> f32 {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Texture")
}
/// Setter for C# member: `P:LibreMetaverse.AgentThrottle.Texture`.
pub fn set_texture(&mut self, value: f32) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Texture")
}
/// C# member: `P:LibreMetaverse.AgentThrottle.Total`.
pub fn total(&self) -> f32 {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Total")
}
/// Setter for C# member: `P:LibreMetaverse.AgentThrottle.Total`.
pub fn set_total(&mut self, value: f32) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Total")
}
/// C# member: `P:LibreMetaverse.AgentThrottle.Wind`.
pub fn wind(&self) -> f32 {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Wind")
}
/// Setter for C# member: `P:LibreMetaverse.AgentThrottle.Wind`.
pub fn set_wind(&mut self, value: f32) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentThrottle.Wind")
}
}
/// C# member: `M:LibreMetaverse.AgentThrottle.#ctor(LibreMetaverse.GridClient)`.
/// C# member: `M:LibreMetaverse.AgentThrottle.#ctor(System.Byte[],System.Int32)`.
/// C# member: `M:LibreMetaverse.AgentThrottle.Set`.
/// C# member: `M:LibreMetaverse.AgentThrottle.Set(LibreMetaverse.Simulator)`.
/// C# member: `M:LibreMetaverse.AgentThrottle.ToBytes`.
/// C# member: `P:LibreMetaverse.AgentThrottle.Asset`.
/// C# member: `P:LibreMetaverse.AgentThrottle.Cloud`.
/// C# member: `P:LibreMetaverse.AgentThrottle.Land`.
/// C# member: `P:LibreMetaverse.AgentThrottle.Resend`.
/// C# member: `P:LibreMetaverse.AgentThrottle.Task`.
/// C# member: `P:LibreMetaverse.AgentThrottle.Texture`.
/// C# member: `P:LibreMetaverse.AgentThrottle.Total`.
/// C# member: `P:LibreMetaverse.AgentThrottle.Wind`.
pub use crate::udp_transport::AgentThrottle;
/// C# type: `T:LibreMetaverse.AgentWearablesReplyEventArgs`.
pub struct AgentWearablesReplyEventArgs;
@@ -13666,21 +13584,9 @@ pub enum ImageType {
}
/// C# type: `T:LibreMetaverse.IncomingPacketIDCollection`.
pub struct IncomingPacketIDCollection;
impl IncomingPacketIDCollection {
/// C# member: `M:LibreMetaverse.IncomingPacketIDCollection.#ctor(System.Int32)`.
pub fn new(capacity: i32) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.IncomingPacketIDCollection.#ctor(System.Int32)",
)
}
/// C# member: `M:LibreMetaverse.IncomingPacketIDCollection.TryEnqueue(System.UInt32)`.
pub fn try_enqueue(&self, ack: u32) -> bool {
libremetaverse_types::unimplemented_api!(
"M:LibreMetaverse.IncomingPacketIDCollection.TryEnqueue(System.UInt32)"
)
}
}
/// C# member: `M:LibreMetaverse.IncomingPacketIDCollection.#ctor(System.Int32)`.
/// C# member: `M:LibreMetaverse.IncomingPacketIDCollection.TryEnqueue(System.UInt32)`.
pub use crate::udp_transport::IncomingPacketIDCollection;
/// C# type: `T:LibreMetaverse.InitiateDownloadEventArgs`.
pub struct InitiateDownloadEventArgs;
@@ -27995,106 +27901,26 @@ pub use crate::foliage_catalog::TreeDefinition;
pub use crate::foliage_catalog::TreeDefinitions;
/// C# type: `T:LibreMetaverse.UDPBase`.
pub struct UDPBase;
impl UDPBase {
/// C# member: `M:LibreMetaverse.UDPBase.AsyncBeginSend(LibreMetaverse.UDPPacketBuffer)`.
pub fn async_begin_send(
&self,
buf: libremetaverse::UDPPacketBuffer,
) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.UDPBase.AsyncBeginSend(LibreMetaverse.UDPPacketBuffer)",
)
}
/// C# member: `M:LibreMetaverse.UDPBase.Start`.
pub fn start(&self) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.UDPBase.Start")
}
/// C# member: `M:LibreMetaverse.UDPBase.Stop`.
pub fn stop(&self) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.UDPBase.Stop")
}
/// C# member: `P:LibreMetaverse.UDPBase.IsRunning`.
pub fn is_running(&self) -> bool {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.UDPBase.IsRunning")
}
}
/// C# member: `M:LibreMetaverse.UDPBase.AsyncBeginSend(LibreMetaverse.UDPPacketBuffer)`.
/// C# member: `M:LibreMetaverse.UDPBase.Start`.
/// C# member: `M:LibreMetaverse.UDPBase.Stop`.
/// C# member: `P:LibreMetaverse.UDPBase.IsRunning`.
pub use crate::udp_transport::UDPBase;
/// C# type: `T:LibreMetaverse.UDPPacketBuffer`.
pub struct UDPPacketBuffer {
/// C# member: `F:LibreMetaverse.UDPPacketBuffer.Data`.
pub data: Vec<u8>,
/// C# member: `F:LibreMetaverse.UDPPacketBuffer.DataLength`.
pub data_length: i32,
/// C# member: `F:LibreMetaverse.UDPPacketBuffer.RemoteEndPoint`.
pub remote_end_point: std::net::SocketAddr,
}
impl UDPPacketBuffer {
/// C# member: `F:LibreMetaverse.UDPPacketBuffer.DEFAULT_BUFFER_SIZE`.
pub const DEFAULT_BUFFER_SIZE: i32 = 4096;
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.#ctor`.
pub fn new_with_constructor() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.UDPPacketBuffer.#ctor")
}
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Byte[],System.Int32,System.Net.IPEndPoint,System.Int32)`.
pub fn new_with_bytes_int32_ip_end_point_int32(
buffer: Vec<u8>,
buffer_size: i32,
destination: std::net::SocketAddr,
category: i32,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Byte[],System.Int32,System.Net.IPEndPoint,System.Int32)",
)
}
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Net.IPEndPoint)`.
pub fn new_with_ip_end_point(end_point: std::net::SocketAddr) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Net.IPEndPoint)",
)
}
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Net.IPEndPoint,System.Byte[])`.
pub fn new_with_ip_end_point_bytes(
end_point: std::net::SocketAddr,
data: Vec<u8>,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Net.IPEndPoint,System.Byte[])",
)
}
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Net.IPEndPoint,System.Int32)`.
pub fn new_with_ip_end_point_int32(
end_point: std::net::SocketAddr,
buffer_size: i32,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Net.IPEndPoint,System.Int32)",
)
}
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.CopyFrom(System.Array)`.
pub fn copy_from_with_array(
&self,
src: libremetaverse_types::compat::Array,
) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.UDPPacketBuffer.CopyFrom(System.Array)",
)
}
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.CopyFrom(System.Array,System.Int32)`.
pub fn copy_from_with_array_int32(
&self,
src: libremetaverse_types::compat::Array,
length: i32,
) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.UDPPacketBuffer.CopyFrom(System.Array,System.Int32)",
)
}
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.ResetEndpoint`.
pub fn reset_endpoint(&self) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.UDPPacketBuffer.ResetEndpoint")
}
}
/// C# member: `F:LibreMetaverse.UDPPacketBuffer.DEFAULT_BUFFER_SIZE`.
/// C# member: `F:LibreMetaverse.UDPPacketBuffer.Data`.
/// C# member: `F:LibreMetaverse.UDPPacketBuffer.DataLength`.
/// C# member: `F:LibreMetaverse.UDPPacketBuffer.RemoteEndPoint`.
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.#ctor`.
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Byte[],System.Int32,System.Net.IPEndPoint,System.Int32)`.
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Net.IPEndPoint)`.
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Net.IPEndPoint,System.Byte[])`.
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.#ctor(System.Net.IPEndPoint,System.Int32)`.
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.CopyFrom(System.Array)`.
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.CopyFrom(System.Array,System.Int32)`.
/// C# member: `M:LibreMetaverse.UDPPacketBuffer.ResetEndpoint`.
pub use crate::udp_transport::UDPPacketBuffer;
/// C# type: `T:LibreMetaverse.UUIDNameReplyEventArgs`.
pub struct UUIDNameReplyEventArgs;

View File

@@ -20,6 +20,7 @@ mod skeleton;
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
mod skeleton_catalog;
mod targa;
mod udp_transport;
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
mod visual_catalog;
@@ -204,6 +205,10 @@ pub use libremetaverse_imaging as imaging_abstractions;
pub use libremetaverse_structured_data as structured_data;
pub use libremetaverse_types as types;
pub use libremetaverse_types::Error;
pub use udp_transport::{
AgentThrottleSender, UdpPacketHandler, UdpThrottleCategory, UdpTransportConfig,
UdpTransportError, UdpTransportStats,
};
/// Rust object-safe view of the C# `InventoryBase` inheritance hierarchy.
pub trait InventoryObjectClass: std::any::Any {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,511 @@
use libremetaverse::packets::{
AgentThrottlePacket, CompletePingCheckPacket, Packet, PacketAckPacket,
PacketAckPacketPacketsBlock, PacketType, UseCircuitCodePacket,
};
use libremetaverse::{
AgentThrottle, AgentThrottleSender, GridClient, Helpers, IncomingPacketIDCollection, Simulator,
UDPBase, UDPPacketBuffer, UdpPacketHandler, UdpTransportConfig, UdpTransportError,
};
use libremetaverse_types::Error;
use libremetaverse_types::compat::{Array, CancellationToken, Object};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::net::UdpSocket;
#[derive(Default)]
struct RecordingHandler {
received: Mutex<Vec<Vec<u8>>>,
sent: Mutex<Vec<Vec<u8>>>,
drops: Mutex<usize>,
}
impl UdpPacketHandler for RecordingHandler {
fn packet_received(&self, buffer: UDPPacketBuffer) {
self.received
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(buffer.data[..usize::try_from(buffer.data_length).unwrap()].to_vec());
}
fn packet_sent(&self, buffer: UDPPacketBuffer, bytes_sent: usize) {
self.sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(buffer.data[..bytes_sent].to_vec());
}
fn packet_dropped(&self) {
*self
.drops
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) += 1;
}
}
fn loopback() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
}
fn reliable_packet() -> Vec<u8> {
let mut packet = UseCircuitCodePacket::new_with_constructor().expect("packet constructor");
packet.circuit_code.code = 0x1122_3344;
packet.to_bytes_with_method().expect("packet bytes")
}
fn packet_ack(sequence: u32) -> Vec<u8> {
let mut packet = PacketAckPacket::new_with_constructor().expect("ACK constructor");
packet.packets = vec![PacketAckPacketPacketsBlock { id: sequence }];
let mut bytes = packet.to_bytes_with_method().expect("ACK bytes");
bytes[0] &= !Helpers::MSG_RELIABLE;
bytes
}
fn complete_ping_packet() -> Vec<u8> {
let mut packet = CompletePingCheckPacket::new_with_constructor().expect("ping constructor");
packet.ping_id.ping_id = 7;
packet.to_bytes_with_method().expect("ping bytes")
}
async fn wait_until(mut condition: impl FnMut() -> bool) {
tokio::time::timeout(Duration::from_secs(2), async {
while !condition() {
tokio::task::yield_now().await;
tokio::time::sleep(Duration::from_millis(1)).await;
}
})
.await
.expect("condition timed out");
}
#[test]
fn packet_buffer_constructors_and_copy_match_the_reference() {
let endpoint = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 13000);
let mut packet = UDPPacketBuffer::new_with_ip_end_point_int32(endpoint, 4).unwrap();
assert_eq!(packet.data, vec![0; 4]);
assert_eq!(packet.data_length, 0);
packet
.copy_from_with_array(Array(vec![Object::Bytes(vec![1, 2, 3, 4])]))
.unwrap();
assert_eq!(packet.data, vec![1, 2, 3, 4]);
assert_eq!(packet.data_length, 0);
assert_eq!(
packet.copy_from_slice(&[1, 2, 3, 4, 5]),
Err(Error::IndexOutOfRange)
);
packet.reset_endpoint().unwrap();
assert_eq!(packet.remote_end_point, "0.0.0.0:0".parse().unwrap());
let borrowed = vec![9, 8, 7];
let packet = UDPPacketBuffer::new_with_ip_end_point_bytes(endpoint, borrowed).unwrap();
assert_eq!(packet.data, vec![9, 8, 7]);
assert_eq!(packet.data_length, 0);
}
#[test]
fn incoming_packet_archive_is_bounded_and_rejects_duplicates() {
let archive = IncomingPacketIDCollection::new(3).unwrap();
assert!(archive.try_enqueue(1));
assert!(!archive.try_enqueue(1));
assert!(archive.try_enqueue(2));
// The C# ring keeps one slot empty, so adding the third item evicts 1.
assert!(archive.try_enqueue(3));
assert!(archive.try_enqueue(1));
assert_eq!(
IncomingPacketIDCollection::new(0).unwrap_err(),
Error::Argument
);
}
#[test]
fn agent_throttle_clamps_and_round_trips_golden_little_endian_values() {
let throttle = AgentThrottle::default();
assert_eq!(throttle.resend(), 150_000.0);
assert_eq!(throttle.land(), 170_000.0);
assert_eq!(throttle.wind(), 34_000.0);
assert_eq!(throttle.cloud(), 34_000.0);
assert_eq!(throttle.task(), 360_448.0);
assert_eq!(throttle.texture(), 360_448.0);
assert_eq!(throttle.asset(), 220_000.0);
assert_eq!(throttle.total(), 1_328_896.0);
let bytes = throttle.to_bytes().unwrap();
assert_eq!(bytes.len(), 28);
let decoded = AgentThrottle::new_with_bytes_int32(bytes, 0).unwrap();
assert_eq!(decoded.to_bytes().unwrap(), throttle.to_bytes().unwrap());
let mut limits = AgentThrottle::default();
limits.set_total(-1.0);
assert_eq!(limits.resend(), 10_000.0);
assert_eq!(limits.task(), 4_000.0);
assert_eq!(limits.land(), 0.0);
}
#[derive(Default)]
struct RecordingThrottleSender(Mutex<Vec<Vec<u8>>>);
impl AgentThrottleSender for RecordingThrottleSender {
fn send_throttle(
&self,
throttle_bytes: &[u8],
_simulator: Option<&Simulator>,
) -> Result<(), Error> {
self.0.lock().unwrap().push(throttle_bytes.to_vec());
Ok(())
}
}
#[test]
fn mapped_agent_throttle_set_uses_the_client_network_binding() {
let sender = Arc::new(RecordingThrottleSender::default());
let mut client = GridClient::new().unwrap();
client.set_agent_throttle_sender(sender.clone()).unwrap();
let throttle = AgentThrottle::new_with_grid_client(client).unwrap();
throttle.set_with_method().unwrap();
assert_eq!(
sender.0.lock().unwrap().as_slice(),
&[throttle.to_bytes().unwrap()]
);
}
#[test]
fn construction_is_runtime_neutral_and_start_requires_an_injected_runtime() {
let transport = UDPBase::client_with_defaults("127.0.0.1:13000".parse().unwrap()).unwrap();
assert!(!transport.is_running());
assert_eq!(transport.start(), Err(Error::InvalidOperation));
assert!(!transport.is_running());
}
#[tokio::test(flavor = "current_thread")]
async fn bounded_command_queue_reports_backpressure_without_unbounded_buffering() {
let server = UdpSocket::bind(loopback()).await.unwrap();
let config = UdpTransportConfig {
command_queue_capacity: 1,
..UdpTransportConfig::default()
};
let transport = UDPBase::client(
server.local_addr().unwrap(),
config,
Arc::new(RecordingHandler::default()),
CancellationToken::default(),
)
.unwrap();
transport.start().unwrap();
// A current-thread runtime does not poll spawned tasks until this test
// yields, making the capacity-one boundary deterministic.
transport
.try_send_packet(
complete_ping_packet(),
server.local_addr().unwrap(),
PacketType::CompletePingCheck,
false,
)
.unwrap();
assert!(matches!(
transport.try_send_packet(
complete_ping_packet(),
server.local_addr().unwrap(),
PacketType::CompletePingCheck,
false,
),
Err(UdpTransportError::Backpressure)
));
assert_eq!(transport.stats().dropped_send_queue, 1);
transport.stop_async().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn local_socket_send_assigns_sequence_and_ack_stops_resends() {
let server = UdpSocket::bind(loopback()).await.unwrap();
let server_address = server.local_addr().unwrap();
let handler = Arc::new(RecordingHandler::default());
let config = UdpTransportConfig {
resend_timeout: Duration::from_millis(60),
network_tick_interval: Duration::from_millis(10),
..UdpTransportConfig::default()
};
let transport = UDPBase::client(
server_address,
config,
handler.clone(),
CancellationToken::default(),
)
.unwrap();
transport.start().unwrap();
let sequence = transport
.send_packet(
reliable_packet(),
PacketType::UseCircuitCode,
false,
CancellationToken::default(),
)
.await
.unwrap();
assert_eq!(sequence, 1);
let mut receive = [0_u8; 4096];
let (length, client_address) =
tokio::time::timeout(Duration::from_secs(2), server.recv_from(&mut receive))
.await
.unwrap()
.unwrap();
assert_eq!(u32::from_be_bytes(receive[1..5].try_into().unwrap()), 1);
assert_ne!(receive[0] & Helpers::MSG_RELIABLE, 0);
server
.send_to(&packet_ack(sequence), client_address)
.await
.unwrap();
wait_until(|| transport.stats().acknowledgements_received == 1).await;
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(transport.stats().resent_datagrams, 0);
assert_eq!(handler.sent.lock().unwrap().len(), 1);
assert_eq!(length, handler.sent.lock().unwrap()[0].len());
transport.stop_async().await;
assert!(!transport.is_running());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn reliable_packets_resend_with_same_sequence_then_expire() {
let server = UdpSocket::bind(loopback()).await.unwrap();
let config = UdpTransportConfig {
resend_timeout: Duration::from_millis(20),
network_tick_interval: Duration::from_millis(5),
max_resend_count: 2,
..UdpTransportConfig::default()
};
let transport = UDPBase::client(
server.local_addr().unwrap(),
config,
Arc::new(RecordingHandler::default()),
CancellationToken::default(),
)
.unwrap();
transport.start().unwrap();
transport
.send_packet(
reliable_packet(),
PacketType::UseCircuitCode,
false,
CancellationToken::default(),
)
.await
.unwrap();
let mut datagrams = Vec::new();
let mut receive = [0_u8; 4096];
for _ in 0..3 {
let (length, _) =
tokio::time::timeout(Duration::from_secs(1), server.recv_from(&mut receive))
.await
.unwrap()
.unwrap();
datagrams.push(receive[..length].to_vec());
}
assert_eq!(
datagrams
.iter()
.map(|bytes| u32::from_be_bytes(bytes[1..5].try_into().unwrap()))
.collect::<Vec<_>>(),
vec![1, 1, 1]
);
assert_eq!(datagrams[0][0] & Helpers::MSG_RESENT, 0);
assert_ne!(datagrams[1][0] & Helpers::MSG_RESENT, 0);
wait_until(|| transport.stats().failed_resends == 1).await;
assert_eq!(transport.stats().resent_datagrams, 2);
transport.stop_async().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn incoming_reliable_packets_are_acked_and_duplicates_are_not_dispatched() {
let server = UdpSocket::bind(loopback()).await.unwrap();
let handler = Arc::new(RecordingHandler::default());
let config = UdpTransportConfig {
max_pending_acks: 2,
network_tick_interval: Duration::from_secs(1),
..UdpTransportConfig::default()
};
let transport = UDPBase::client(
server.local_addr().unwrap(),
config,
handler.clone(),
CancellationToken::default(),
)
.unwrap();
transport.start().unwrap();
let client_address = transport.local_address().unwrap();
let mut first = reliable_packet();
first[1..5].copy_from_slice(&10_u32.to_be_bytes());
let mut second = reliable_packet();
second[1..5].copy_from_slice(&12_u32.to_be_bytes());
let mut late = reliable_packet();
late[1..5].copy_from_slice(&11_u32.to_be_bytes());
server.send_to(&first, client_address).await.unwrap();
server.send_to(&first, client_address).await.unwrap();
server.send_to(&second, client_address).await.unwrap();
server.send_to(&late, client_address).await.unwrap();
let mut receive = [0_u8; 4096];
let mut ack_groups = Vec::new();
for _ in 0..2 {
let (length, _) =
tokio::time::timeout(Duration::from_secs(2), server.recv_from(&mut receive))
.await
.unwrap()
.unwrap();
let mut position = 0;
let ack = PacketAckPacket::new_with_bytes_int32(receive[..length].to_vec(), &mut position)
.unwrap();
ack_groups.push(ack.packets.iter().map(|block| block.id).collect::<Vec<_>>());
}
assert_eq!(ack_groups, vec![vec![10, 10], vec![12, 11]]);
wait_until(|| handler.received.lock().unwrap().len() == 3).await;
assert_eq!(transport.stats().duplicate_datagrams, 1);
assert_eq!(transport.stats().sequence_gaps, 1);
assert_eq!(transport.stats().out_of_order_datagrams, 1);
assert_eq!(transport.stats().acknowledgements_sent, 4);
transport.stop_async().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pending_ack_is_piggybacked_before_the_mtu_boundary() {
let server = UdpSocket::bind(loopback()).await.unwrap();
let handler = Arc::new(RecordingHandler::default());
let config = UdpTransportConfig {
network_tick_interval: Duration::from_secs(10),
..UdpTransportConfig::default()
};
let transport = UDPBase::client(
server.local_addr().unwrap(),
config,
handler.clone(),
CancellationToken::default(),
)
.unwrap();
transport.start().unwrap();
let mut reliable = reliable_packet();
reliable[1..5].copy_from_slice(&20_u32.to_be_bytes());
server
.send_to(&reliable, transport.local_address().unwrap())
.await
.unwrap();
wait_until(|| handler.received.lock().unwrap().len() == 1).await;
transport
.send_packet(
complete_ping_packet(),
PacketType::CompletePingCheck,
false,
CancellationToken::default(),
)
.await
.unwrap();
let mut receive = [0_u8; 4096];
let (length, _) = server.recv_from(&mut receive).await.unwrap();
assert_ne!(receive[0] & Helpers::MSG_APPENDED_ACKS, 0);
let mut end = i32::try_from(length).unwrap() - 1;
let decoded = Packet::build_packet_with_bytes_int32_bytes(
receive[..length].to_vec(),
&mut end,
vec![0; 8192],
)
.unwrap();
assert_eq!(decoded.type_, PacketType::CompletePingCheck);
assert_eq!(decoded.header.ack_list, Some(vec![20]));
transport.stop_async().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn zerocoding_mtu_malformed_input_and_cancellation_are_bounded() {
let server = UdpSocket::bind(loopback()).await.unwrap();
let handler = Arc::new(RecordingHandler::default());
let transport = UDPBase::client(
server.local_addr().unwrap(),
UdpTransportConfig::default(),
handler.clone(),
CancellationToken::default(),
)
.unwrap();
transport.start().unwrap();
let client_address = transport.local_address().unwrap();
let mut packet = AgentThrottlePacket::new_with_constructor().unwrap();
packet.throttle.throttles = vec![0; 28];
let raw = packet.to_bytes_with_method().unwrap();
transport
.send_packet(
raw.clone(),
PacketType::AgentThrottle,
true,
CancellationToken::default(),
)
.await
.unwrap();
let mut receive = [0_u8; 4096];
let (length, _) = server.recv_from(&mut receive).await.unwrap();
assert_ne!(receive[0] & Helpers::MSG_ZEROCODED, 0);
assert!(length < raw.len());
let mut end = i32::try_from(length).unwrap() - 1;
Packet::build_packet_with_bytes_int32_bytes(
receive[..length].to_vec(),
&mut end,
vec![0; 8192],
)
.unwrap();
assert_eq!(
transport
.send_packet(
vec![0; 1201],
PacketType::AgentThrottle,
false,
CancellationToken::default(),
)
.await,
Err(UdpTransportError::MtuExceeded)
);
for malformed in [vec![], vec![0x40], vec![0x40, 0, 0, 0, 1, 0, 0xfe]] {
server.send_to(&malformed, client_address).await.unwrap();
}
wait_until(|| transport.stats().malformed_datagrams >= 3).await;
assert!(handler.received.lock().unwrap().is_empty());
transport.stop_async().await;
assert!(!transport.is_running());
assert_eq!(
transport
.send_packet(
reliable_packet(),
PacketType::UseCircuitCode,
false,
CancellationToken::default(),
)
.await,
Err(UdpTransportError::NotRunning)
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dropping_the_last_transport_handle_releases_its_socket_tasks() {
let server = UdpSocket::bind(loopback()).await.unwrap();
let transport = UDPBase::client_with_defaults(server.local_addr().unwrap()).unwrap();
transport.start().unwrap();
let local_address = transport.local_address().unwrap();
drop(transport);
tokio::time::timeout(Duration::from_secs(2), async {
loop {
match UdpSocket::bind(local_address).await {
Ok(rebound) => break rebound,
Err(_) => tokio::time::sleep(Duration::from_millis(1)).await,
}
}
})
.await
.expect("transport tasks retained the UDP socket after drop");
}