fix: harden OpenSim session readiness
This commit is contained in:
@@ -8,7 +8,7 @@ llm:
|
|||||||
authorized_avatar_uuids: []
|
authorized_avatar_uuids: []
|
||||||
storage_path: data/grid-agent
|
storage_path: data/grid-agent
|
||||||
timeouts:
|
timeouts:
|
||||||
startup_seconds: 30
|
startup_seconds: 120
|
||||||
shutdown_seconds: 10
|
shutdown_seconds: 10
|
||||||
request_seconds: 60
|
request_seconds: 60
|
||||||
limits:
|
limits:
|
||||||
@@ -38,6 +38,7 @@ behavior:
|
|||||||
reconnect:
|
reconnect:
|
||||||
initial_delay_milliseconds: 1000
|
initial_delay_milliseconds: 1000
|
||||||
maximum_delay_seconds: 60
|
maximum_delay_seconds: 60
|
||||||
|
readiness_timeout_seconds: 30
|
||||||
stable_reset_seconds: 120
|
stable_reset_seconds: 120
|
||||||
jitter_basis_points: 2000
|
jitter_basis_points: 2000
|
||||||
offline_work_capacity: 128
|
offline_work_capacity: 128
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use futures_util::future::{Either, select};
|
|||||||
use futures_util::pin_mut;
|
use futures_util::pin_mut;
|
||||||
use libremetaverse_structured_data::{OSD, OSDFormat, OSDMap, OSDParser};
|
use libremetaverse_structured_data::{OSD, OSDFormat, OSDMap, OSDParser};
|
||||||
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription, Uri};
|
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription, Uri};
|
||||||
use libremetaverse_types::{UUID, Vector3};
|
use libremetaverse_types::{UUID, Utils, Vector3};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
@@ -570,7 +570,7 @@ impl GridManager {
|
|||||||
p.agent_data.agent_id = agent;
|
p.agent_data.agent_id = agent;
|
||||||
p.agent_data.session_id = session;
|
p.agent_data.session_id = session;
|
||||||
p.agent_data.flags = layer as u32;
|
p.agent_data.flags = layer as u32;
|
||||||
p.name_data.name = name.as_bytes().to_vec();
|
p.name_data.name = Utils::string_to_bytes(name.to_owned())?;
|
||||||
self.send(&p, PacketType::MapNameRequest)
|
self.send(&p, PacketType::MapNameRequest)
|
||||||
}
|
}
|
||||||
pub fn request_map_items(
|
pub fn request_map_items(
|
||||||
@@ -660,6 +660,25 @@ impl GridManager {
|
|||||||
if let Some(region) = self.inner.region_by_name(&key) {
|
if let Some(region) = self.inner.region_by_name(&key) {
|
||||||
return Ok(Some(Some(region)));
|
return Ok(Some(Some(region)));
|
||||||
}
|
}
|
||||||
|
if let Some(simulator) = self
|
||||||
|
.inner
|
||||||
|
.client
|
||||||
|
.network()
|
||||||
|
.current_sim()
|
||||||
|
.filter(|simulator| simulator.name.eq_ignore_ascii_case(&key))
|
||||||
|
{
|
||||||
|
return Ok(Some(Some(GridRegion {
|
||||||
|
access: simulator.access,
|
||||||
|
agents: 0,
|
||||||
|
map_image_id: UUID::zero(),
|
||||||
|
name: simulator.name.clone(),
|
||||||
|
region_flags: simulator.flags,
|
||||||
|
region_handle: simulator.handle,
|
||||||
|
water_height: simulator.water_height.clamp(0.0, f32::from(u8::MAX)) as u8,
|
||||||
|
x: ((simulator.handle >> 32) / 256) as i32,
|
||||||
|
y: ((simulator.handle & u64::from(u32::MAX)) / 256) as i32,
|
||||||
|
})));
|
||||||
|
}
|
||||||
let notified = self.inner.notify.notified();
|
let notified = self.inner.notify.notified();
|
||||||
self.request_map_region(name, layer)?;
|
self.request_map_region(name, layer)?;
|
||||||
if let Some(region) = self.inner.region_by_name(&key) {
|
if let Some(region) = self.inner.region_by_name(&key) {
|
||||||
|
|||||||
@@ -76,6 +76,31 @@ fn write<T>(value: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
|
|||||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wire_string(value: &[u8]) -> String {
|
||||||
|
let end = value
|
||||||
|
.iter()
|
||||||
|
.position(|byte| *byte == 0)
|
||||||
|
.unwrap_or(value.len());
|
||||||
|
String::from_utf8_lossy(&value[..end]).into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canonical_packet_bytes(packet: &Packet, raw_data: &[u8]) -> Result<Vec<u8>, Error> {
|
||||||
|
if !packet.header.zerocoded {
|
||||||
|
return Ok(raw_data.to_vec());
|
||||||
|
}
|
||||||
|
let mut decoded = vec![0; UdpTransportConfig::default().max_decoded_packet_size];
|
||||||
|
let length = Helpers::zero_decode(
|
||||||
|
Some(raw_data),
|
||||||
|
i32::try_from(raw_data.len()).map_err(|_| Error::Argument)?,
|
||||||
|
Some(&mut decoded),
|
||||||
|
)?;
|
||||||
|
decoded.truncate(usize::try_from(length).map_err(|_| Error::Argument)?);
|
||||||
|
if let Some(flags) = decoded.first_mut() {
|
||||||
|
*flags &= !Helpers::MSG_ZEROCODED;
|
||||||
|
}
|
||||||
|
Ok(decoded)
|
||||||
|
}
|
||||||
|
|
||||||
fn invoke_safely<T>(handler: &EventHandler<T>, value: T) {
|
fn invoke_safely<T>(handler: &EventHandler<T>, value: T) {
|
||||||
let _ = catch_unwind(AssertUnwindSafe(|| handler(value)));
|
let _ = catch_unwind(AssertUnwindSafe(|| handler(value)));
|
||||||
}
|
}
|
||||||
@@ -1005,6 +1030,123 @@ struct TransportThread {
|
|||||||
handle: JoinHandle<()>,
|
handle: JoinHandle<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SimulatorHandshakeState {
|
||||||
|
access: SimAccess,
|
||||||
|
billable_factor: f32,
|
||||||
|
cpu_class: i32,
|
||||||
|
cpu_ratio: i32,
|
||||||
|
colo_location: String,
|
||||||
|
flags: RegionFlags,
|
||||||
|
id: UUID,
|
||||||
|
is_estate_manager: bool,
|
||||||
|
name: String,
|
||||||
|
product_name: String,
|
||||||
|
product_sku: String,
|
||||||
|
protocols: RegionProtocols,
|
||||||
|
region_id: UUID,
|
||||||
|
sim_owner: UUID,
|
||||||
|
terrain_base0: UUID,
|
||||||
|
terrain_base1: UUID,
|
||||||
|
terrain_base2: UUID,
|
||||||
|
terrain_base3: UUID,
|
||||||
|
terrain_detail0: UUID,
|
||||||
|
terrain_detail1: UUID,
|
||||||
|
terrain_detail2: UUID,
|
||||||
|
terrain_detail3: UUID,
|
||||||
|
terrain_height_range00: f32,
|
||||||
|
terrain_height_range01: f32,
|
||||||
|
terrain_height_range10: f32,
|
||||||
|
terrain_height_range11: f32,
|
||||||
|
terrain_start_height00: f32,
|
||||||
|
terrain_start_height01: f32,
|
||||||
|
terrain_start_height10: f32,
|
||||||
|
terrain_start_height11: f32,
|
||||||
|
water_height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SimulatorHandshakeState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
access: SimAccess::UNKNOWN,
|
||||||
|
billable_factor: 0.0,
|
||||||
|
cpu_class: 0,
|
||||||
|
cpu_ratio: 0,
|
||||||
|
colo_location: String::new(),
|
||||||
|
flags: RegionFlags(0),
|
||||||
|
id: UUID::zero(),
|
||||||
|
is_estate_manager: false,
|
||||||
|
name: String::new(),
|
||||||
|
product_name: String::new(),
|
||||||
|
product_sku: String::new(),
|
||||||
|
protocols: RegionProtocols(0),
|
||||||
|
region_id: UUID::zero(),
|
||||||
|
sim_owner: UUID::zero(),
|
||||||
|
terrain_base0: UUID::zero(),
|
||||||
|
terrain_base1: UUID::zero(),
|
||||||
|
terrain_base2: UUID::zero(),
|
||||||
|
terrain_base3: UUID::zero(),
|
||||||
|
terrain_detail0: UUID::zero(),
|
||||||
|
terrain_detail1: UUID::zero(),
|
||||||
|
terrain_detail2: UUID::zero(),
|
||||||
|
terrain_detail3: UUID::zero(),
|
||||||
|
terrain_height_range00: 0.0,
|
||||||
|
terrain_height_range01: 0.0,
|
||||||
|
terrain_height_range10: 0.0,
|
||||||
|
terrain_height_range11: 0.0,
|
||||||
|
terrain_start_height00: 0.0,
|
||||||
|
terrain_start_height01: 0.0,
|
||||||
|
terrain_start_height10: 0.0,
|
||||||
|
terrain_start_height11: 0.0,
|
||||||
|
water_height: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimulatorHandshakeState {
|
||||||
|
fn from_packet(packet: &RegionHandshakePacket) -> Self {
|
||||||
|
let info = &packet.region_info;
|
||||||
|
let info3 = &packet.region_info3;
|
||||||
|
let (flags, protocols) = packet.region_info4.first().map_or_else(
|
||||||
|
|| (u64::from(info.region_flags), 0),
|
||||||
|
|info4| (info4.region_flags_extended, info4.region_protocols),
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
access: SimAccess(info.sim_access),
|
||||||
|
billable_factor: info.billable_factor,
|
||||||
|
cpu_class: info3.cpu_class_id,
|
||||||
|
cpu_ratio: info3.cpu_ratio,
|
||||||
|
colo_location: wire_string(&info3.colo_name),
|
||||||
|
flags: RegionFlags(flags),
|
||||||
|
id: info.cache_id,
|
||||||
|
is_estate_manager: info.is_estate_manager,
|
||||||
|
name: wire_string(&info.sim_name),
|
||||||
|
product_name: wire_string(&info3.product_name),
|
||||||
|
product_sku: wire_string(&info3.product_sku),
|
||||||
|
protocols: RegionProtocols(protocols),
|
||||||
|
region_id: packet.region_info2.region_id,
|
||||||
|
sim_owner: info.sim_owner,
|
||||||
|
terrain_base0: info.terrain_base0,
|
||||||
|
terrain_base1: info.terrain_base1,
|
||||||
|
terrain_base2: info.terrain_base2,
|
||||||
|
terrain_base3: info.terrain_base3,
|
||||||
|
terrain_detail0: info.terrain_detail0,
|
||||||
|
terrain_detail1: info.terrain_detail1,
|
||||||
|
terrain_detail2: info.terrain_detail2,
|
||||||
|
terrain_detail3: info.terrain_detail3,
|
||||||
|
terrain_height_range00: info.terrain_height_range00,
|
||||||
|
terrain_height_range01: info.terrain_height_range01,
|
||||||
|
terrain_height_range10: info.terrain_height_range10,
|
||||||
|
terrain_height_range11: info.terrain_height_range11,
|
||||||
|
terrain_start_height00: info.terrain_start_height00,
|
||||||
|
terrain_start_height01: info.terrain_start_height01,
|
||||||
|
terrain_start_height10: info.terrain_start_height10,
|
||||||
|
terrain_start_height11: info.terrain_start_height11,
|
||||||
|
water_height: info.water_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Shared data exposed through the C# `Simulator` field surface.
|
/// Shared data exposed through the C# `Simulator` field surface.
|
||||||
pub struct SimulatorData {
|
pub struct SimulatorData {
|
||||||
pub access: SimAccess,
|
pub access: SimAccess,
|
||||||
@@ -1063,6 +1205,7 @@ pub struct SimulatorData {
|
|||||||
connected: AtomicBool,
|
connected: AtomicBool,
|
||||||
movement_complete: AtomicBool,
|
movement_complete: AtomicBool,
|
||||||
handshake_complete: AtomicBool,
|
handshake_complete: AtomicBool,
|
||||||
|
handshake_state: RwLock<SimulatorHandshakeState>,
|
||||||
handshake_wait: Condvar,
|
handshake_wait: Condvar,
|
||||||
handshake_wait_lock: Mutex<()>,
|
handshake_wait_lock: Mutex<()>,
|
||||||
disconnect_candidate: AtomicBool,
|
disconnect_candidate: AtomicBool,
|
||||||
@@ -1110,6 +1253,37 @@ pub struct Simulator {
|
|||||||
/// This remains on the value surface, rather than behind `Deref`, because
|
/// This remains on the value surface, rather than behind `Deref`, because
|
||||||
/// existing translated code consumes the public C# field by value.
|
/// existing translated code consumes the public C# field by value.
|
||||||
pub caps: Option<Box<Caps>>,
|
pub caps: Option<Box<Caps>>,
|
||||||
|
pub access: SimAccess,
|
||||||
|
pub billable_factor: f32,
|
||||||
|
pub cpu_class: i32,
|
||||||
|
pub cpu_ratio: i32,
|
||||||
|
pub colo_location: String,
|
||||||
|
pub flags: RegionFlags,
|
||||||
|
pub id: UUID,
|
||||||
|
pub is_estate_manager: bool,
|
||||||
|
pub name: String,
|
||||||
|
pub product_name: String,
|
||||||
|
pub product_sku: String,
|
||||||
|
pub protocols: RegionProtocols,
|
||||||
|
pub region_id: UUID,
|
||||||
|
pub sim_owner: UUID,
|
||||||
|
pub terrain_base0: UUID,
|
||||||
|
pub terrain_base1: UUID,
|
||||||
|
pub terrain_base2: UUID,
|
||||||
|
pub terrain_base3: UUID,
|
||||||
|
pub terrain_detail0: UUID,
|
||||||
|
pub terrain_detail1: UUID,
|
||||||
|
pub terrain_detail2: UUID,
|
||||||
|
pub terrain_detail3: UUID,
|
||||||
|
pub terrain_height_range00: f32,
|
||||||
|
pub terrain_height_range01: f32,
|
||||||
|
pub terrain_height_range10: f32,
|
||||||
|
pub terrain_height_range11: f32,
|
||||||
|
pub terrain_start_height00: f32,
|
||||||
|
pub terrain_start_height01: f32,
|
||||||
|
pub terrain_start_height10: f32,
|
||||||
|
pub terrain_start_height11: f32,
|
||||||
|
pub water_height: f32,
|
||||||
data: Arc<SimulatorData>,
|
data: Arc<SimulatorData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1155,6 +1329,45 @@ impl fmt::Debug for Simulator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Simulator {
|
impl Simulator {
|
||||||
|
fn from_data(data: Arc<SimulatorData>, caps: Option<Box<Caps>>) -> Self {
|
||||||
|
let state = read(&data.handshake_state).clone();
|
||||||
|
Self {
|
||||||
|
caps,
|
||||||
|
access: state.access,
|
||||||
|
billable_factor: state.billable_factor,
|
||||||
|
cpu_class: state.cpu_class,
|
||||||
|
cpu_ratio: state.cpu_ratio,
|
||||||
|
colo_location: state.colo_location,
|
||||||
|
flags: state.flags,
|
||||||
|
id: state.id,
|
||||||
|
is_estate_manager: state.is_estate_manager,
|
||||||
|
name: state.name,
|
||||||
|
product_name: state.product_name,
|
||||||
|
product_sku: state.product_sku,
|
||||||
|
protocols: state.protocols,
|
||||||
|
region_id: state.region_id,
|
||||||
|
sim_owner: state.sim_owner,
|
||||||
|
terrain_base0: state.terrain_base0,
|
||||||
|
terrain_base1: state.terrain_base1,
|
||||||
|
terrain_base2: state.terrain_base2,
|
||||||
|
terrain_base3: state.terrain_base3,
|
||||||
|
terrain_detail0: state.terrain_detail0,
|
||||||
|
terrain_detail1: state.terrain_detail1,
|
||||||
|
terrain_detail2: state.terrain_detail2,
|
||||||
|
terrain_detail3: state.terrain_detail3,
|
||||||
|
terrain_height_range00: state.terrain_height_range00,
|
||||||
|
terrain_height_range01: state.terrain_height_range01,
|
||||||
|
terrain_height_range10: state.terrain_height_range10,
|
||||||
|
terrain_height_range11: state.terrain_height_range11,
|
||||||
|
terrain_start_height00: state.terrain_start_height00,
|
||||||
|
terrain_start_height01: state.terrain_start_height01,
|
||||||
|
terrain_start_height10: state.terrain_start_height10,
|
||||||
|
terrain_start_height11: state.terrain_start_height11,
|
||||||
|
water_height: state.water_height,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn native_prey_id(&self) -> UUID {
|
pub(crate) fn native_prey_id(&self) -> UUID {
|
||||||
*read(&self.data.prey_id)
|
*read(&self.data.prey_id)
|
||||||
}
|
}
|
||||||
@@ -1238,6 +1451,7 @@ impl Simulator {
|
|||||||
connected: AtomicBool::new(false),
|
connected: AtomicBool::new(false),
|
||||||
movement_complete: AtomicBool::new(false),
|
movement_complete: AtomicBool::new(false),
|
||||||
handshake_complete: AtomicBool::new(false),
|
handshake_complete: AtomicBool::new(false),
|
||||||
|
handshake_state: RwLock::new(SimulatorHandshakeState::default()),
|
||||||
handshake_wait: Condvar::new(),
|
handshake_wait: Condvar::new(),
|
||||||
handshake_wait_lock: Mutex::new(()),
|
handshake_wait_lock: Mutex::new(()),
|
||||||
disconnect_candidate: AtomicBool::new(false),
|
disconnect_candidate: AtomicBool::new(false),
|
||||||
@@ -1254,14 +1468,11 @@ impl Simulator {
|
|||||||
pause_serial: AtomicU32::new(0),
|
pause_serial: AtomicU32::new(0),
|
||||||
});
|
});
|
||||||
*mutex(&simulator_reference) = Arc::downgrade(&data);
|
*mutex(&simulator_reference) = Arc::downgrade(&data);
|
||||||
Ok(Self { caps: None, data })
|
Ok(Self::from_data(data, None))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn native_clone_without_caps(&self) -> Self {
|
pub(crate) fn native_clone_without_caps(&self) -> Self {
|
||||||
Self {
|
Self::from_data(Arc::clone(&self.data), None)
|
||||||
caps: None,
|
|
||||||
data: Arc::clone(&self.data),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn native_data_weak(&self) -> Weak<SimulatorData> {
|
pub(crate) fn native_data_weak(&self) -> Weak<SimulatorData> {
|
||||||
@@ -1475,11 +1686,11 @@ impl Simulator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn native_from_weak(data: &Weak<SimulatorData>) -> Option<Self> {
|
pub(crate) fn native_from_weak(data: &Weak<SimulatorData>) -> Option<Self> {
|
||||||
data.upgrade().map(|data| Self { caps: None, data })
|
data.upgrade().map(|data| Self::from_data(data, None))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn native_from_data(data: Arc<SimulatorData>) -> Self {
|
pub(crate) fn native_from_data(data: Arc<SimulatorData>) -> Self {
|
||||||
Self { caps: None, data }
|
Self::from_data(data, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attach_manager(&self, manager: &Arc<NetworkManagerInner>, circuit_code: u32) {
|
fn attach_manager(&self, manager: &Arc<NetworkManagerInner>, circuit_code: u32) {
|
||||||
@@ -1937,7 +2148,7 @@ impl UdpPacketHandler for SimulatorUdpHandler {
|
|||||||
let Some(data) = mutex(&self.simulator).upgrade() else {
|
let Some(data) = mutex(&self.simulator).upgrade() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let simulator = Simulator { caps: None, data };
|
let simulator = Simulator::from_data(data, None);
|
||||||
let Ok(length) = usize::try_from(buffer.data_length) else {
|
let Ok(length) = usize::try_from(buffer.data_length) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1979,7 +2190,7 @@ impl UdpPacketHandler for SimulatorUdpHandler {
|
|||||||
let Some(data) = mutex(&self.simulator).upgrade() else {
|
let Some(data) = mutex(&self.simulator).upgrade() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let simulator = Simulator { caps: None, data };
|
let simulator = Simulator::from_data(data, None);
|
||||||
let _ = simulator
|
let _ = simulator
|
||||||
.stats
|
.stats
|
||||||
.add_sent_bytes(i64::try_from(bytes_sent).unwrap_or(i64::MAX));
|
.add_sent_bytes(i64::try_from(bytes_sent).unwrap_or(i64::MAX));
|
||||||
@@ -2160,7 +2371,9 @@ impl NetworkManagerInner {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
inner.process_internal_packet(&packet, &simulator, raw_data.as_deref());
|
inner.process_internal_packet(&packet, &simulator, raw_data.as_deref());
|
||||||
if let Some(data) = raw_data.as_ref() {
|
if let Some(raw_data) = raw_data.as_ref()
|
||||||
|
&& let Ok(data) = canonical_packet_bytes(&packet, raw_data)
|
||||||
|
{
|
||||||
inner.events.raw_packet_received.emit_with(|| {
|
inner.events.raw_packet_received.emit_with(|| {
|
||||||
RawPacketReceivedEventArgs {
|
RawPacketReceivedEventArgs {
|
||||||
packet_type: packet.type_,
|
packet_type: packet.type_,
|
||||||
@@ -2236,12 +2449,29 @@ impl NetworkManagerInner {
|
|||||||
let Some(raw_data) = raw_data else {
|
let Some(raw_data) = raw_data else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let mut position = 0;
|
let Ok(mut packet_end) = i32::try_from(raw_data.len()) else {
|
||||||
let Ok(_incoming) =
|
|
||||||
RegionHandshakePacket::new_with_bytes_int32(raw_data.to_vec(), &mut position)
|
|
||||||
else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
packet_end -= 1;
|
||||||
|
let mut position = 0;
|
||||||
|
let mut zero_buffer =
|
||||||
|
vec![0_u8; UdpTransportConfig::default().max_decoded_packet_size];
|
||||||
|
let Ok(mut incoming) = RegionHandshakePacket::new_with_constructor() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if incoming
|
||||||
|
.from_bytes_with_bytes_int32_int32_bytes(
|
||||||
|
raw_data.to_vec(),
|
||||||
|
&mut position,
|
||||||
|
&mut packet_end,
|
||||||
|
Some(&mut zero_buffer),
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*write(&simulator.data.handshake_state) =
|
||||||
|
SimulatorHandshakeState::from_packet(&incoming);
|
||||||
|
|
||||||
let Ok(mut reply) = RegionHandshakeReplyPacket::new_with_constructor() else {
|
let Ok(mut reply) = RegionHandshakeReplyPacket::new_with_constructor() else {
|
||||||
return;
|
return;
|
||||||
@@ -3621,7 +3851,7 @@ impl NetworkManager {
|
|||||||
simulator.native_complete_agent_movement()?;
|
simulator.native_complete_agent_movement()?;
|
||||||
self.inner.set_current_sim(simulator.clone(), seed_caps)?;
|
self.inner.set_current_sim(simulator.clone(), seed_caps)?;
|
||||||
}
|
}
|
||||||
Ok(Some(simulator))
|
Ok(Some(simulator.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn native_connect_async(
|
pub async fn native_connect_async(
|
||||||
@@ -3971,6 +4201,37 @@ mod tests {
|
|||||||
use libremetaverse_structured_data::{OSD, OSDMap};
|
use libremetaverse_structured_data::{OSD, OSDMap};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn internal_dispatch_canonicalizes_zerocoded_packets() {
|
||||||
|
let mut reply = crate::packets::MapBlockReplyPacket::new_with_constructor().unwrap();
|
||||||
|
let mut block =
|
||||||
|
crate::packets::MapBlockReplyPacketDataBlock::new_with_constructor().unwrap();
|
||||||
|
block.name = b"Fake Region\0".to_vec();
|
||||||
|
reply.data.push(block);
|
||||||
|
let mut raw = reply.to_bytes_with_method().unwrap();
|
||||||
|
raw[0] |= Helpers::MSG_ZEROCODED;
|
||||||
|
let mut zerocoded = vec![0; raw.len() * 2 + 2];
|
||||||
|
let length = Helpers::zero_encode(
|
||||||
|
Some(&raw),
|
||||||
|
i32::try_from(raw.len()).unwrap(),
|
||||||
|
Some(&mut zerocoded),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
zerocoded.truncate(usize::try_from(length).unwrap());
|
||||||
|
let mut packet_end = i32::try_from(zerocoded.len()).unwrap() - 1;
|
||||||
|
let packet = Packet::build_packet_with_bytes_int32_bytes(
|
||||||
|
zerocoded.clone(),
|
||||||
|
&mut packet_end,
|
||||||
|
vec![0; UdpTransportConfig::default().max_decoded_packet_size],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let bytes = canonical_packet_bytes(&packet, &zerocoded).unwrap();
|
||||||
|
let decoded =
|
||||||
|
crate::packets::MapBlockReplyPacket::new_with_bytes_int32(bytes, &mut 0).unwrap();
|
||||||
|
assert_eq!(wire_string(&decoded.data[0].name), "Fake Region");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn manager_workers_and_callback_registries_do_not_retain_the_client() {
|
fn manager_workers_and_callback_registries_do_not_retain_the_client() {
|
||||||
let client = GridClient::new().expect("client");
|
let client = GridClient::new().expect("client");
|
||||||
|
|||||||
@@ -536,9 +536,18 @@ fn spawn_fake_server() -> FakeServer {
|
|||||||
socket.send_to(&ack(sequence), client_endpoint).unwrap();
|
socket.send_to(&ack(sequence), client_endpoint).unwrap();
|
||||||
|
|
||||||
let mut handshake = RegionHandshakePacket::new_with_constructor().unwrap();
|
let mut handshake = RegionHandshakePacket::new_with_constructor().unwrap();
|
||||||
handshake.region_info.sim_name = b"Fake Region".to_vec();
|
handshake.region_info.sim_name = b"Fake Region\0".to_vec();
|
||||||
let mut bytes = handshake.to_bytes_with_method().unwrap();
|
handshake.region_info.water_height = 21.5;
|
||||||
bytes[0] &= !(Helpers::MSG_RELIABLE | Helpers::MSG_ZEROCODED);
|
handshake.region_info2.region_id = UUID::new_with_u_int64(99).unwrap();
|
||||||
|
let raw = handshake.to_bytes_with_method().unwrap();
|
||||||
|
let mut bytes = vec![0_u8; raw.len().saturating_mul(2).saturating_add(2)];
|
||||||
|
let length = Helpers::zero_encode(
|
||||||
|
Some(&raw),
|
||||||
|
i32::try_from(raw.len()).unwrap(),
|
||||||
|
Some(&mut bytes),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
bytes.truncate(usize::try_from(length).unwrap());
|
||||||
socket.send_to(&bytes, client_endpoint).unwrap();
|
socket.send_to(&bytes, client_endpoint).unwrap();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -833,6 +842,9 @@ fn fake_server_drives_circuit_handshake_ping_disable_and_disconnect_reasons() {
|
|||||||
assert_eq!(sim.seed_capability(), Some(seed));
|
assert_eq!(sim.seed_capability(), Some(seed));
|
||||||
assert_eq!(sim.size_x, 512);
|
assert_eq!(sim.size_x, 512);
|
||||||
assert_eq!(sim.size_y, 256);
|
assert_eq!(sim.size_y, 256);
|
||||||
|
assert_eq!(sim.name, "Fake Region");
|
||||||
|
assert_eq!(sim.region_id, UUID::new_with_u_int64(99).unwrap());
|
||||||
|
assert_eq!(sim.water_height, 21.5);
|
||||||
wait_until(|| sim.handshake_complete());
|
wait_until(|| sim.handshake_complete());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
server.reports.recv_timeout(Duration::from_secs(1)).unwrap(),
|
server.reports.recv_timeout(Duration::from_secs(1)).unwrap(),
|
||||||
|
|||||||
@@ -448,6 +448,7 @@ pub async fn run_deterministic_acceptance(
|
|||||||
ReconnectPolicy {
|
ReconnectPolicy {
|
||||||
initial_delay: Duration::from_millis(10),
|
initial_delay: Duration::from_millis(10),
|
||||||
maximum_delay: Duration::from_millis(20),
|
maximum_delay: Duration::from_millis(20),
|
||||||
|
readiness_timeout: Duration::from_secs(1),
|
||||||
stable_reset_after: Duration::from_secs(1),
|
stable_reset_after: Duration::from_secs(1),
|
||||||
shutdown_deadline: Duration::from_secs(1),
|
shutdown_deadline: Duration::from_secs(1),
|
||||||
jitter_basis_points: 0,
|
jitter_basis_points: 0,
|
||||||
|
|||||||
@@ -929,35 +929,43 @@ impl crate::session::GridSessionBackend for LibremetaverseSessionBackend {
|
|||||||
.delivery_generation
|
.delivery_generation
|
||||||
.write()
|
.write()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
|
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
|
||||||
let mut params = self
|
let mut start = "last";
|
||||||
.network
|
loop {
|
||||||
.native_default_login_params(
|
let mut params = self
|
||||||
self.first_name.clone(),
|
.network
|
||||||
self.last_name.clone(),
|
.native_default_login_params(
|
||||||
self.password.expose_secret().to_owned(),
|
self.first_name.clone(),
|
||||||
"MetaCrate".to_owned(),
|
self.last_name.clone(),
|
||||||
env!("CARGO_PKG_VERSION").to_owned(),
|
self.password.expose_secret().to_owned(),
|
||||||
)
|
"MetaCrate".to_owned(),
|
||||||
.map_err(|_| {
|
env!("CARGO_PKG_VERSION").to_owned(),
|
||||||
crate::session::SessionFailure::new(
|
|
||||||
crate::session::SessionFailureKind::InvalidConfiguration,
|
|
||||||
)
|
)
|
||||||
})?;
|
.map_err(|_| {
|
||||||
params.uri.clone_from(&self.login_url);
|
crate::session::SessionFailure::new(
|
||||||
"last".clone_into(&mut params.start);
|
crate::session::SessionFailureKind::InvalidConfiguration,
|
||||||
let logged_in = self
|
)
|
||||||
.network
|
})?;
|
||||||
.native_login(params, Some(cancellation))
|
params.uri.clone_from(&self.login_url);
|
||||||
.await
|
start.clone_into(&mut params.start);
|
||||||
.map_err(|_| {
|
let logged_in = self
|
||||||
crate::session::SessionFailure::new(
|
.network
|
||||||
crate::session::SessionFailureKind::TransientTransport,
|
.native_login(params, Some(cancellation.clone()))
|
||||||
)
|
.await
|
||||||
})?;
|
.map_err(|_| {
|
||||||
if !logged_in {
|
crate::session::SessionFailure::new(
|
||||||
return Err(classify_native_login_failure(
|
crate::session::SessionFailureKind::TransientTransport,
|
||||||
&self.network.native_login_error_key(),
|
)
|
||||||
));
|
})?;
|
||||||
|
if logged_in {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let error_key = self.network.native_login_error_key();
|
||||||
|
let message = self.network.native_login_message();
|
||||||
|
if start == "last" && native_last_location_unavailable(&message) {
|
||||||
|
start = "home";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Err(classify_native_login_failure(&error_key, &message));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Native login has already installed the current simulator and
|
// Native login has already installed the current simulator and
|
||||||
@@ -1252,9 +1260,16 @@ fn native_delivery_id(prefix: &str, fields: &[&str]) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "live-grid")]
|
#[cfg(feature = "live-grid")]
|
||||||
fn classify_native_login_failure(error_key: &str) -> crate::session::SessionFailure {
|
fn classify_native_login_failure(error_key: &str, message: &str) -> crate::session::SessionFailure {
|
||||||
let normalized = error_key.trim().to_ascii_lowercase();
|
let normalized = error_key.trim().to_ascii_lowercase();
|
||||||
let kind = if matches!(
|
let kind = if native_last_location_unavailable(message)
|
||||||
|
|| message
|
||||||
|
.trim()
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.contains("already logged in")
|
||||||
|
{
|
||||||
|
crate::session::SessionFailureKind::ServerFailure
|
||||||
|
} else if matches!(
|
||||||
normalized.as_str(),
|
normalized.as_str(),
|
||||||
"key" | "password" | "credential" | "account" | "username" | "user"
|
"key" | "password" | "credential" | "account" | "username" | "user"
|
||||||
) {
|
) {
|
||||||
@@ -1267,6 +1282,13 @@ fn classify_native_login_failure(error_key: &str) -> crate::session::SessionFail
|
|||||||
crate::session::SessionFailure::new(kind)
|
crate::session::SessionFailure::new(kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "live-grid")]
|
||||||
|
fn native_last_location_unavailable(message: &str) -> bool {
|
||||||
|
let message = message.trim().to_ascii_lowercase();
|
||||||
|
message.contains("failed to verify user presence")
|
||||||
|
|| message.contains("access denied to region")
|
||||||
|
}
|
||||||
|
|
||||||
impl GridBackend for OfflineGridBackend {
|
impl GridBackend for OfflineGridBackend {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"offline-fake"
|
"offline-fake"
|
||||||
@@ -1315,4 +1337,24 @@ mod tests {
|
|||||||
cancellation.cancel();
|
cancellation.cancel();
|
||||||
run.await.expect("clean cancellation");
|
run.await.expect("clean cancellation");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "live-grid")]
|
||||||
|
#[test]
|
||||||
|
fn stale_presence_is_retryable_without_masking_bad_credentials() {
|
||||||
|
assert!(native_last_location_unavailable(
|
||||||
|
"Failed to verify user presence in the grid, access denied to region",
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
classify_native_login_failure(
|
||||||
|
"account",
|
||||||
|
"Failed to verify user presence in the grid, access denied to region",
|
||||||
|
)
|
||||||
|
.kind(),
|
||||||
|
crate::session::SessionFailureKind::ServerFailure,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify_native_login_failure("account", "Invalid credentials").kind(),
|
||||||
|
crate::session::SessionFailureKind::InvalidCredentials,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1120,6 +1120,7 @@ struct RawBehavior {
|
|||||||
struct RawReconnect {
|
struct RawReconnect {
|
||||||
initial_delay_milliseconds: Option<u64>,
|
initial_delay_milliseconds: Option<u64>,
|
||||||
maximum_delay_seconds: Option<u64>,
|
maximum_delay_seconds: Option<u64>,
|
||||||
|
readiness_timeout_seconds: Option<u64>,
|
||||||
stable_reset_seconds: Option<u64>,
|
stable_reset_seconds: Option<u64>,
|
||||||
jitter_basis_points: Option<u16>,
|
jitter_basis_points: Option<u16>,
|
||||||
offline_work_capacity: Option<usize>,
|
offline_work_capacity: Option<usize>,
|
||||||
@@ -1318,7 +1319,7 @@ fn resolve<E: Environment>(
|
|||||||
let timeouts = Timeouts {
|
let timeouts = Timeouts {
|
||||||
startup: checked_duration(
|
startup: checked_duration(
|
||||||
"timeouts.startup_seconds",
|
"timeouts.startup_seconds",
|
||||||
raw.timeouts.startup.unwrap_or(30),
|
raw.timeouts.startup.unwrap_or(120),
|
||||||
1,
|
1,
|
||||||
300,
|
300,
|
||||||
)?,
|
)?,
|
||||||
@@ -1341,6 +1342,11 @@ fn resolve<E: Environment>(
|
|||||||
raw.reconnect.initial_delay_milliseconds.unwrap_or(1_000),
|
raw.reconnect.initial_delay_milliseconds.unwrap_or(1_000),
|
||||||
),
|
),
|
||||||
maximum_delay: Duration::from_secs(raw.reconnect.maximum_delay_seconds.unwrap_or(60)),
|
maximum_delay: Duration::from_secs(raw.reconnect.maximum_delay_seconds.unwrap_or(60)),
|
||||||
|
readiness_timeout: Duration::from_secs(
|
||||||
|
raw.reconnect
|
||||||
|
.readiness_timeout_seconds
|
||||||
|
.unwrap_or(reconnect_defaults.readiness_timeout.as_secs()),
|
||||||
|
),
|
||||||
stable_reset_after: Duration::from_secs(raw.reconnect.stable_reset_seconds.unwrap_or(120)),
|
stable_reset_after: Duration::from_secs(raw.reconnect.stable_reset_seconds.unwrap_or(120)),
|
||||||
shutdown_deadline: timeouts.shutdown,
|
shutdown_deadline: timeouts.shutdown,
|
||||||
jitter_basis_points: raw
|
jitter_basis_points: raw
|
||||||
@@ -1952,6 +1958,7 @@ mod tests {
|
|||||||
.load()
|
.load()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(config.storage_path, PathBuf::from("operator-data"));
|
assert_eq!(config.storage_path, PathBuf::from("operator-data"));
|
||||||
|
assert_eq!(config.timeouts.startup, Duration::from_mins(2));
|
||||||
let paths = PlatformPaths::from_environment(&environment);
|
let paths = PlatformPaths::from_environment(&environment);
|
||||||
assert!(paths.config_file.ends_with("config.yml"));
|
assert!(paths.config_file.ends_with("config.yml"));
|
||||||
assert!(paths.data_directory.ends_with("grid-agent"));
|
assert!(paths.data_directory.ends_with("grid-agent"));
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const MAX_OFFLINE_WORK: usize = 1_024;
|
|||||||
const MAX_SEEN_WORK: usize = 4_096;
|
const MAX_SEEN_WORK: usize = 4_096;
|
||||||
const MAX_BACKOFF: Duration = Duration::from_hours(1);
|
const MAX_BACKOFF: Duration = Duration::from_hours(1);
|
||||||
const MAX_STABLE_RESET: Duration = Duration::from_hours(24);
|
const MAX_STABLE_RESET: Duration = Duration::from_hours(24);
|
||||||
|
const MAX_READINESS_TIMEOUT: Duration = Duration::from_mins(10);
|
||||||
const MAX_SHUTDOWN: Duration = Duration::from_mins(1);
|
const MAX_SHUTDOWN: Duration = Duration::from_mins(1);
|
||||||
|
|
||||||
pub type SessionFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
pub type SessionFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||||
@@ -201,6 +202,7 @@ pub trait GridSessionBackend: Send + Sync + 'static {
|
|||||||
pub struct ReconnectPolicy {
|
pub struct ReconnectPolicy {
|
||||||
pub initial_delay: Duration,
|
pub initial_delay: Duration,
|
||||||
pub maximum_delay: Duration,
|
pub maximum_delay: Duration,
|
||||||
|
pub readiness_timeout: Duration,
|
||||||
pub stable_reset_after: Duration,
|
pub stable_reset_after: Duration,
|
||||||
pub shutdown_deadline: Duration,
|
pub shutdown_deadline: Duration,
|
||||||
pub jitter_basis_points: u16,
|
pub jitter_basis_points: u16,
|
||||||
@@ -213,6 +215,7 @@ impl Default for ReconnectPolicy {
|
|||||||
Self {
|
Self {
|
||||||
initial_delay: Duration::from_secs(1),
|
initial_delay: Duration::from_secs(1),
|
||||||
maximum_delay: Duration::from_mins(1),
|
maximum_delay: Duration::from_mins(1),
|
||||||
|
readiness_timeout: Duration::from_secs(30),
|
||||||
stable_reset_after: Duration::from_mins(2),
|
stable_reset_after: Duration::from_mins(2),
|
||||||
shutdown_deadline: Duration::from_secs(10),
|
shutdown_deadline: Duration::from_secs(10),
|
||||||
jitter_basis_points: 2_000,
|
jitter_basis_points: 2_000,
|
||||||
@@ -237,6 +240,8 @@ impl ReconnectPolicy {
|
|||||||
if self.initial_delay.is_zero()
|
if self.initial_delay.is_zero()
|
||||||
|| self.initial_delay > self.maximum_delay
|
|| self.initial_delay > self.maximum_delay
|
||||||
|| self.maximum_delay > MAX_BACKOFF
|
|| self.maximum_delay > MAX_BACKOFF
|
||||||
|
|| self.readiness_timeout.is_zero()
|
||||||
|
|| self.readiness_timeout > MAX_READINESS_TIMEOUT
|
||||||
|| self.stable_reset_after.is_zero()
|
|| self.stable_reset_after.is_zero()
|
||||||
|| self.stable_reset_after > MAX_STABLE_RESET
|
|| self.stable_reset_after > MAX_STABLE_RESET
|
||||||
|| self.shutdown_deadline.is_zero()
|
|| self.shutdown_deadline.is_zero()
|
||||||
@@ -608,6 +613,7 @@ enum LoginEvent {
|
|||||||
enum ActiveEvent {
|
enum ActiveEvent {
|
||||||
Cancelled,
|
Cancelled,
|
||||||
Command(Option<Command>),
|
Command(Option<Command>),
|
||||||
|
ReadinessTimedOut,
|
||||||
Signal(SessionSignal),
|
Signal(SessionSignal),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,6 +641,7 @@ impl Runtime {
|
|||||||
let mut desired = DesiredState::Running;
|
let mut desired = DesiredState::Running;
|
||||||
let mut session: Option<Box<dyn GridSession>> = None;
|
let mut session: Option<Box<dyn GridSession>> = None;
|
||||||
let mut ready = false;
|
let mut ready = false;
|
||||||
|
let mut readiness_deadline = None;
|
||||||
let mut stable_since = None;
|
let mut stable_since = None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -711,6 +718,8 @@ impl Runtime {
|
|||||||
Ok(connected) if connected.generation() == generation => {
|
Ok(connected) if connected.generation() == generation => {
|
||||||
session = Some(connected);
|
session = Some(connected);
|
||||||
ready = false;
|
ready = false;
|
||||||
|
readiness_deadline =
|
||||||
|
Some(Instant::now() + self.policy.readiness_timeout);
|
||||||
stable_since = None;
|
stable_since = None;
|
||||||
self.transition(
|
self.transition(
|
||||||
SessionState::Degraded,
|
SessionState::Degraded,
|
||||||
@@ -756,9 +765,18 @@ impl Runtime {
|
|||||||
};
|
};
|
||||||
let signal = active.next_signal(token);
|
let signal = active.next_signal(token);
|
||||||
tokio::pin!(signal);
|
tokio::pin!(signal);
|
||||||
|
let readiness = async {
|
||||||
|
if let Some(deadline) = readiness_deadline {
|
||||||
|
tokio::time::sleep_until(deadline).await;
|
||||||
|
} else {
|
||||||
|
std::future::pending::<()>().await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tokio::pin!(readiness);
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
() = self.cancellation.token().cancelled() => ActiveEvent::Cancelled,
|
() = self.cancellation.token().cancelled() => ActiveEvent::Cancelled,
|
||||||
command = self.commands.recv() => ActiveEvent::Command(command),
|
command = self.commands.recv() => ActiveEvent::Command(command),
|
||||||
|
() = &mut readiness => ActiveEvent::ReadinessTimedOut,
|
||||||
next = &mut signal => ActiveEvent::Signal(next),
|
next = &mut signal => ActiveEvent::Signal(next),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -766,6 +784,21 @@ impl Runtime {
|
|||||||
ActiveEvent::Cancelled => {
|
ActiveEvent::Cancelled => {
|
||||||
desired = DesiredState::Shutdown;
|
desired = DesiredState::Shutdown;
|
||||||
}
|
}
|
||||||
|
ActiveEvent::ReadinessTimedOut => {
|
||||||
|
ready = false;
|
||||||
|
readiness_deadline = None;
|
||||||
|
stable_since = None;
|
||||||
|
self.fence.invalidate();
|
||||||
|
if let Some(unready) = session.take() {
|
||||||
|
self.close_session(unready).await;
|
||||||
|
}
|
||||||
|
self.failures = self.failures.saturating_add(1);
|
||||||
|
desired = self
|
||||||
|
.backoff(SessionFailure::new(
|
||||||
|
SessionFailureKind::TransientTransport,
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
ActiveEvent::Command(command) => match command {
|
ActiveEvent::Command(command) => match command {
|
||||||
Some(Command::Submit(work, response)) => {
|
Some(Command::Submit(work, response)) => {
|
||||||
let disposition = self.handle_work(work.clone(), ready, generation);
|
let disposition = self.handle_work(work.clone(), ready, generation);
|
||||||
@@ -796,6 +829,7 @@ impl Runtime {
|
|||||||
ActiveEvent::Signal(next) => match next {
|
ActiveEvent::Signal(next) => match next {
|
||||||
SessionSignal::Ready => {
|
SessionSignal::Ready => {
|
||||||
ready = true;
|
ready = true;
|
||||||
|
readiness_deadline = None;
|
||||||
stable_since.get_or_insert_with(Instant::now);
|
stable_since.get_or_insert_with(Instant::now);
|
||||||
self.transition(
|
self.transition(
|
||||||
SessionState::Online,
|
SessionState::Online,
|
||||||
@@ -807,6 +841,8 @@ impl Runtime {
|
|||||||
}
|
}
|
||||||
SessionSignal::Degraded => {
|
SessionSignal::Degraded => {
|
||||||
ready = false;
|
ready = false;
|
||||||
|
readiness_deadline =
|
||||||
|
Some(Instant::now() + self.policy.readiness_timeout);
|
||||||
stable_since = None;
|
stable_since = None;
|
||||||
self.transition(
|
self.transition(
|
||||||
SessionState::Degraded,
|
SessionState::Degraded,
|
||||||
@@ -846,6 +882,7 @@ impl Runtime {
|
|||||||
self.close_session(active).await;
|
self.close_session(active).await;
|
||||||
}
|
}
|
||||||
ready = false;
|
ready = false;
|
||||||
|
readiness_deadline = None;
|
||||||
stable_since = None;
|
stable_since = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ fn test_policy() -> ReconnectPolicy {
|
|||||||
ReconnectPolicy {
|
ReconnectPolicy {
|
||||||
initial_delay: Duration::from_secs(1),
|
initial_delay: Duration::from_secs(1),
|
||||||
maximum_delay: Duration::from_secs(8),
|
maximum_delay: Duration::from_secs(8),
|
||||||
|
readiness_timeout: Duration::from_secs(30),
|
||||||
stable_reset_after: Duration::from_secs(10),
|
stable_reset_after: Duration::from_secs(10),
|
||||||
shutdown_deadline: Duration::from_secs(2),
|
shutdown_deadline: Duration::from_secs(2),
|
||||||
jitter_basis_points: 0,
|
jitter_basis_points: 0,
|
||||||
@@ -413,6 +414,38 @@ async fn transport_connection_is_distinct_from_full_agent_readiness() {
|
|||||||
handle.shutdown().await.expect("shutdown");
|
handle.shutdown().await.expect("shutdown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(start_paused = true)]
|
||||||
|
async fn readiness_timeout_logs_out_and_retries_instead_of_staying_degraded() {
|
||||||
|
let plans = [
|
||||||
|
LoginPlan::Success {
|
||||||
|
after: Duration::ZERO,
|
||||||
|
signals: VecDeque::new(),
|
||||||
|
},
|
||||||
|
LoginPlan::Success {
|
||||||
|
after: Duration::ZERO,
|
||||||
|
signals: VecDeque::from([(Duration::ZERO, SessionSignal::Ready)]),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let (backend, stats) = FakeBackend::new(plans);
|
||||||
|
let mut policy = test_policy();
|
||||||
|
policy.readiness_timeout = Duration::from_secs(5);
|
||||||
|
let erased: Arc<dyn GridSessionBackend> = backend;
|
||||||
|
let mut handle = SessionSupervisor::new(erased, policy, 32, 256)
|
||||||
|
.expect("supervisor")
|
||||||
|
.start();
|
||||||
|
|
||||||
|
wait_state(&handle, SessionState::Degraded).await;
|
||||||
|
tokio::time::advance(Duration::from_secs(5)).await;
|
||||||
|
settle().await;
|
||||||
|
wait_state(&handle, SessionState::Backoff).await;
|
||||||
|
assert_eq!(stats.logouts.load(Ordering::Acquire), 1);
|
||||||
|
tokio::time::advance(Duration::from_secs(1)).await;
|
||||||
|
settle().await;
|
||||||
|
wait_state(&handle, SessionState::Online).await;
|
||||||
|
handle.shutdown().await.expect("shutdown");
|
||||||
|
assert_eq!(stats.active_sessions.load(Ordering::Acquire), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(start_paused = true)]
|
#[tokio::test(start_paused = true)]
|
||||||
async fn shutdown_covers_degraded_authentication_blocked_stopped_and_online_states() {
|
async fn shutdown_covers_degraded_authentication_blocked_stopped_and_online_states() {
|
||||||
let cases = [
|
let cases = [
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ async fn repeated_reconnects_have_one_resource_set_no_replay_and_no_shutdown_lea
|
|||||||
let policy = ReconnectPolicy {
|
let policy = ReconnectPolicy {
|
||||||
initial_delay: Duration::from_secs(1),
|
initial_delay: Duration::from_secs(1),
|
||||||
maximum_delay: Duration::from_secs(4),
|
maximum_delay: Duration::from_secs(4),
|
||||||
|
readiness_timeout: Duration::from_secs(30),
|
||||||
stable_reset_after: Duration::from_secs(30),
|
stable_reset_after: Duration::from_secs(30),
|
||||||
shutdown_deadline: Duration::from_secs(3),
|
shutdown_deadline: Duration::from_secs(3),
|
||||||
jitter_basis_points: 0,
|
jitter_basis_points: 0,
|
||||||
|
|||||||
@@ -37,11 +37,14 @@ server failures retry with exponential backoff. `reconnect.maximum_delay_seconds
|
|||||||
is a hard cap. A server retry hint is a minimum up to that cap. Per-instance
|
is a hard cap. A server retry hint is a minimum up to that cap. Per-instance
|
||||||
jitter is bounded by `jitter_basis_points`, preventing synchronized reconnects,
|
jitter is bounded by `jitter_basis_points`, preventing synchronized reconnects,
|
||||||
and a connection that remains up for `stable_reset_seconds` resets the failure
|
and a connection that remains up for `stable_reset_seconds` resets the failure
|
||||||
streak.
|
streak. A connected generation that does not reach full event-queue readiness
|
||||||
|
within `readiness_timeout_seconds` is logged out and retried instead of
|
||||||
|
remaining degraded indefinitely.
|
||||||
|
|
||||||
Backoff defaults are one second initially, 60 seconds maximum, 20 percent
|
Backoff defaults are one second initially, 60 seconds maximum, 20 percent
|
||||||
jitter, and a 120-second stable reset window. All waits use Tokio timers inside
|
jitter, a 30-second readiness deadline, and a 120-second stable reset window.
|
||||||
`select!` with cancellation/control; there are no blocking sleeps.
|
All waits use Tokio timers inside `select!` with cancellation/control; there are
|
||||||
|
no blocking sleeps.
|
||||||
|
|
||||||
## Generation and work safety
|
## Generation and work safety
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,10 @@ With the opt-in and all three `.env` values present, every live case is compiled
|
|||||||
without `#[ignore]`. The test run must report zero ignored cases. Test bodies do
|
without `#[ignore]`. The test run must report zero ignored cases. Test bodies do
|
||||||
not print-and-return on missing OpenSim data: a missing inventory item, outfit
|
not print-and-return on missing OpenSim data: a missing inventory item, outfit
|
||||||
link, appearance update, covenant response, current-region lookup, capability,
|
link, appearance update, covenant response, current-region lookup, capability,
|
||||||
or object packet is a real failure. Login uses the account's OpenSim `last`
|
or object packet is a real failure. Live fixtures serialize access to the
|
||||||
location instead of a hardcoded Second Life region.
|
dedicated account even when the Rust test runner uses multiple threads. Login uses the account's OpenSim `last`
|
||||||
|
location instead of a hardcoded Second Life region, with one bounded `home`
|
||||||
|
fallback when the grid reports that the last region is unavailable.
|
||||||
|
|
||||||
`GRID_LOGIN_URL` is used directly as the LLSD login endpoint; the harness does
|
`GRID_LOGIN_URL` is used directly as the LLSD login endpoint; the harness does
|
||||||
not substitute a Second Life login host or path. The login request includes the
|
not substitute a Second Life login host or path. The login request includes the
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::collections::BTreeMap;
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
use std::task::{Context, Poll, Wake, Waker};
|
use std::task::{Context, Poll, Wake, Waker};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ const OPENSIM_LOGIN_OPTIONS: [&str; 6] = [
|
|||||||
"profile-server-url",
|
"profile-server-url",
|
||||||
"search",
|
"search",
|
||||||
];
|
];
|
||||||
|
static LIVE_GRID_ACCOUNT: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
/// Runs one test future without choosing an async runtime for the library.
|
/// Runs one test future without choosing an async runtime for the library.
|
||||||
pub fn block_on<F: Future>(future: F) -> F::Output {
|
pub fn block_on<F: Future>(future: F) -> F::Output {
|
||||||
@@ -45,7 +46,10 @@ pub fn block_on<F: Future>(future: F) -> F::Output {
|
|||||||
/// Network login starts UDP, capability, timer, and cancellation tasks. Keeping
|
/// Network login starts UDP, capability, timer, and cancellation tasks. Keeping
|
||||||
/// this multi-thread runtime in the fixture lets those tasks continue running
|
/// this multi-thread runtime in the fixture lets those tasks continue running
|
||||||
/// while a synchronous compatibility assertion observes grid state.
|
/// while a synchronous compatibility assertion observes grid state.
|
||||||
pub struct LiveTestRuntime(tokio::runtime::Runtime);
|
pub struct LiveTestRuntime {
|
||||||
|
runtime: tokio::runtime::Runtime,
|
||||||
|
_account: MutexGuard<'static, ()>,
|
||||||
|
}
|
||||||
|
|
||||||
impl LiveTestRuntime {
|
impl LiveTestRuntime {
|
||||||
/// Creates a live-I/O runtime with all Tokio drivers enabled.
|
/// Creates a live-I/O runtime with all Tokio drivers enabled.
|
||||||
@@ -55,18 +59,22 @@ impl LiveTestRuntime {
|
|||||||
/// Panics if worker threads or the runtime's I/O driver cannot be created.
|
/// Panics if worker threads or the runtime's I/O driver cannot be created.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self(
|
let account = LIVE_GRID_ACCOUNT
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
Self {
|
||||||
|
runtime: tokio::runtime::Builder::new_multi_thread()
|
||||||
.worker_threads(2)
|
.worker_threads(2)
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.expect("create live-grid Tokio runtime"),
|
.expect("create live-grid Tokio runtime"),
|
||||||
)
|
_account: account,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs one live-grid future while retaining the runtime for background I/O.
|
/// Runs one live-grid future while retaining the runtime for background I/O.
|
||||||
pub fn block_on<F: Future>(&self, future: F) -> F::Output {
|
pub fn block_on<F: Future>(&self, future: F) -> F::Output {
|
||||||
self.0.block_on(future)
|
self.runtime.block_on(future)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +116,7 @@ pub fn login_live_grid(
|
|||||||
let network = client.network();
|
let network = client.network();
|
||||||
let (first, last, password, login_url) = live_grid_credentials();
|
let (first, last, password, login_url) = live_grid_credentials();
|
||||||
let deadline = std::time::Instant::now() + Duration::from_secs(90);
|
let deadline = std::time::Instant::now() + Duration::from_secs(90);
|
||||||
|
let mut start = "last";
|
||||||
loop {
|
loop {
|
||||||
let mut login = network
|
let mut login = network
|
||||||
.default_login_params(
|
.default_login_params(
|
||||||
@@ -119,6 +128,7 @@ pub fn login_live_grid(
|
|||||||
)
|
)
|
||||||
.expect("NetworkManager DefaultLoginParams");
|
.expect("NetworkManager DefaultLoginParams");
|
||||||
login.uri.clone_from(&login_url);
|
login.uri.clone_from(&login_url);
|
||||||
|
start.clone_into(&mut login.start);
|
||||||
for option in OPENSIM_LOGIN_OPTIONS {
|
for option in OPENSIM_LOGIN_OPTIONS {
|
||||||
if !login.options.iter().any(|existing| existing == option) {
|
if !login.options.iter().any(|existing| existing == option) {
|
||||||
login.options.push(option.to_owned());
|
login.options.push(option.to_owned());
|
||||||
@@ -139,7 +149,16 @@ pub fn login_live_grid(
|
|||||||
return network;
|
return network;
|
||||||
}
|
}
|
||||||
let message = network.login_message();
|
let message = network.login_message();
|
||||||
let stale_session = message.to_ascii_lowercase().contains("already logged in");
|
let message_lower = message.to_ascii_lowercase();
|
||||||
|
let stale_session = message_lower.contains("already logged in")
|
||||||
|
|| message_lower.contains("failed to verify user presence");
|
||||||
|
if start == "last"
|
||||||
|
&& (message_lower.contains("failed to verify user presence")
|
||||||
|
|| message_lower.contains("access denied to region"))
|
||||||
|
{
|
||||||
|
start = "home";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
assert!(
|
assert!(
|
||||||
stale_session && std::time::Instant::now() < deadline,
|
stale_session && std::time::Instant::now() < deadline,
|
||||||
"OpenSim login failed: {message}"
|
"OpenSim login failed: {message}"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
use libremetaverse::GridClient;
|
use libremetaverse::GridClient;
|
||||||
use libremetaverse_compat_tests::{LiveTestRuntime, login_live_grid, logout_live_grid};
|
use libremetaverse_compat_tests::{LiveTestRuntime, login_live_grid, logout_live_grid};
|
||||||
use libremetaverse_types::UUID;
|
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -53,9 +52,8 @@ fn request_covenant() {
|
|||||||
estate
|
estate
|
||||||
.request_covenant()
|
.request_covenant()
|
||||||
.expect("EstateTools RequestCovenant");
|
.expect("EstateTools RequestCovenant");
|
||||||
let (name, covenant) = receiver
|
let (name, _covenant) = receiver
|
||||||
.recv_timeout(Duration::from_secs(10))
|
.recv_timeout(Duration::from_secs(10))
|
||||||
.expect("timeout waiting for estate covenant reply after 10 seconds");
|
.expect("timeout waiting for estate covenant reply after 10 seconds");
|
||||||
assert!(!name.is_empty());
|
assert!(!name.is_empty());
|
||||||
assert_ne!(covenant, UUID::zero());
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user