Files
MetaCrate/crates/libremetaverse/src/client_core.rs
Chili Palmer b413cd6f4d
Some checks failed
Native code generation / deterministic (push) Failing after 6m8s
Imaging and meshing gate / native (push) Failing after 17s
JPEG 2000 feature / linux (push) Failing after 59s
Skia feature / linux (push) Failing after 1m33s
Implement native client core lifecycle (#51)
2026-08-09 10:53:33 +00:00

834 lines
27 KiB
Rust

//! Runtime-neutral client configuration, composition, and lifecycle ownership.
use crate::{
AgentSettings, AssetCacheSettings, ConnectionSettings, LoggingSettings, PacketSettings,
ParcelSettings, TexturePipelineSettings, TimingSettings, WorldSettings,
};
use libremetaverse_types::compat::{CancellationToken, CancellationTokenSource, TimeProvider};
use std::fmt;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Condvar, Mutex};
/// A validated client-core failure with no credential or capability payload.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ClientCoreError {
/// A public settings field failed validation.
InvalidConfiguration {
field: &'static str,
reason: &'static str,
},
/// A lifecycle operation was requested in an incompatible state.
InvalidLifecycle {
operation: &'static str,
state: ClientLifecycleState,
},
/// An owned service could not complete orderly shutdown.
ServiceShutdown { service: &'static str },
}
impl fmt::Display for ClientCoreError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidConfiguration { field, reason } => {
write!(formatter, "invalid client setting {field}: {reason}")
}
Self::InvalidLifecycle { operation, state } => {
write!(formatter, "cannot {operation} while client is {state:?}")
}
Self::ServiceShutdown { service } => {
write!(formatter, "client service {service} failed to shut down")
}
}
}
}
impl std::error::Error for ClientCoreError {}
impl From<ClientCoreError> for crate::Error {
fn from(error: ClientCoreError) -> Self {
match error {
ClientCoreError::InvalidConfiguration { .. } => Self::Argument,
ClientCoreError::InvalidLifecycle { .. } | ClientCoreError::ServiceShutdown { .. } => {
Self::InvalidOperation
}
}
}
}
/// Observable lifecycle of a [`GridClient`].
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[repr(u8)]
pub enum ClientLifecycleState {
Active,
ShuttingDown,
Disposed,
}
impl ClientLifecycleState {
fn from_u8(value: u8) -> Self {
match value {
0 => Self::Active,
1 => Self::ShuttingDown,
_ => Self::Disposed,
}
}
}
/// Defines the C#-compatible shutdown order for explicitly composed services.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum ShutdownPhase {
Network = 0,
Manager = 1,
Http = 2,
RateLimiter = 3,
}
/// A client-owned subsystem that participates in deterministic teardown.
///
/// Implementations own their tasks and I/O resources. They must use the client
/// cancellation token and must not require `GridClient` to create an executor.
pub trait ClientService: Send + Sync {
/// Stable diagnostic name. It must not contain credentials or capability URLs.
fn name(&self) -> &'static str;
fn shutdown_phase(&self) -> ShutdownPhase {
ShutdownPhase::Manager
}
/// Cancels and joins resources owned by this service.
///
/// # Errors
///
/// Returns a redacted service error when orderly teardown cannot complete.
fn shutdown(&self) -> Result<(), ClientCoreError>;
}
struct ServiceRegistration {
insertion_order: usize,
service: Arc<dyn ClientService>,
}
struct ClientRuntime {
state: AtomicU8,
cancellation: CancellationTokenSource,
services: Mutex<Vec<ServiceRegistration>>,
shutdown_complete: Condvar,
shutdown_wait: Mutex<()>,
}
impl ClientRuntime {
fn new(services: Vec<Arc<dyn ClientService>>) -> Self {
Self {
state: AtomicU8::new(ClientLifecycleState::Active as u8),
cancellation: CancellationTokenSource::new(),
services: Mutex::new(
services
.into_iter()
.enumerate()
.map(|(insertion_order, service)| ServiceRegistration {
insertion_order,
service,
})
.collect(),
),
shutdown_complete: Condvar::new(),
shutdown_wait: Mutex::new(()),
}
}
fn state(&self) -> ClientLifecycleState {
ClientLifecycleState::from_u8(self.state.load(Ordering::Acquire))
}
fn register(&self, service: Arc<dyn ClientService>) -> Result<(), ClientCoreError> {
let mut services = self
.services
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let state = self.state();
if state != ClientLifecycleState::Active {
return Err(ClientCoreError::InvalidLifecycle {
operation: "register a service",
state,
});
}
let insertion_order = services.len();
services.push(ServiceRegistration {
insertion_order,
service,
});
Ok(())
}
fn shutdown(&self) -> Result<(), ClientCoreError> {
if self
.state
.compare_exchange(
ClientLifecycleState::Active as u8,
ClientLifecycleState::ShuttingDown as u8,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_err()
{
let mut wait = self
.shutdown_wait
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while self.state() == ClientLifecycleState::ShuttingDown {
wait = self
.shutdown_complete
.wait(wait)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
return Ok(());
}
self.cancellation.cancel();
let mut services = self
.services
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
services.sort_by_key(|entry| (entry.service.shutdown_phase(), entry.insertion_order));
let mut first_error = None;
for entry in services.iter() {
if let Err(error) = entry.service.shutdown()
&& first_error.is_none()
{
first_error = Some(error);
}
}
services.clear();
let shutdown_wait = self
.shutdown_wait
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.state
.store(ClientLifecycleState::Disposed as u8, Ordering::Release);
self.shutdown_complete.notify_all();
drop(shutdown_wait);
first_error.map_or(Ok(()), Err)
}
}
/// Native owner for the mapped C# `GridClient` lifecycle slice.
///
/// Construction is side-effect free: it creates no runtime, task, socket, or
/// HTTP request. Network-facing services are attached explicitly and are owned
/// until ordered shutdown or drop.
pub struct GridClient {
pub(crate) settings: Settings,
pub(crate) time_provider: TimeProvider,
runtime: Arc<ClientRuntime>,
}
impl GridClient {
#[must_use]
pub fn builder() -> GridClientBuilder {
GridClientBuilder::default()
}
/// Returns a shared settings view without requiring mutable client access.
#[must_use]
pub fn settings_ref(&self) -> &Settings {
&self.settings
}
#[must_use]
pub fn cancellation_token(&self) -> CancellationToken {
self.runtime.cancellation.token()
}
#[must_use]
pub fn lifecycle_state(&self) -> ClientLifecycleState {
self.runtime.state()
}
/// Adds an explicitly constructed service to this client's ownership tree.
///
/// # Errors
///
/// Registration fails after shutdown begins.
pub fn register_service(
&mut self,
service: Arc<dyn ClientService>,
) -> Result<(), ClientCoreError> {
self.runtime.register(service)
}
pub(crate) fn shutdown(&self) -> Result<(), ClientCoreError> {
self.runtime.shutdown()
}
}
impl fmt::Debug for GridClient {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let service_names: Vec<_> = self
.runtime
.services
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.map(|entry| entry.service.name())
.collect();
formatter
.debug_struct("GridClient")
.field("settings", &self.settings)
.field("lifecycle_state", &self.lifecycle_state())
.field("services", &service_names)
.field("time_provider", &self.time_provider)
.finish()
}
}
impl Drop for GridClient {
fn drop(&mut self) {
let _ = self.runtime.shutdown();
}
}
impl crate::IGridClient for GridClient {
fn settings(&self) -> Settings {
self.settings.clone()
}
fn time_provider(&self) -> TimeProvider {
self.time_provider.clone()
}
fn set_time_provider(&mut self, value: TimeProvider) {
self.time_provider = value;
}
}
/// Explicit, runtime-neutral composition entry point for [`GridClient`].
pub struct GridClientBuilder {
settings: Settings,
time_provider: TimeProvider,
services: Vec<Arc<dyn ClientService>>,
}
impl Default for GridClientBuilder {
fn default() -> Self {
Self {
settings: Settings::defaults(),
time_provider: TimeProvider::system(),
services: Vec::new(),
}
}
}
impl GridClientBuilder {
#[must_use]
pub fn with_settings(mut self, settings: Settings) -> Self {
self.settings = settings;
self
}
#[must_use]
pub fn with_time_provider(mut self, time_provider: TimeProvider) -> Self {
self.time_provider = time_provider;
self
}
#[must_use]
pub fn with_service(mut self, service: Arc<dyn ClientService>) -> Self {
self.services.push(service);
self
}
/// Validates configuration and creates a client without starting I/O.
///
/// # Errors
///
/// Returns the first invalid configuration field.
pub fn build(self) -> Result<GridClient, ClientCoreError> {
self.settings.validate()?;
Ok(GridClient {
settings: self.settings,
time_provider: self.time_provider,
runtime: Arc::new(ClientRuntime::new(self.services)),
})
}
}
/// Native storage for the mapped grouped settings object.
#[derive(Clone, PartialEq)]
pub struct Settings {
pub default_effect_color: libremetaverse_types::Color4,
pub(crate) connection: ConnectionSettings,
pub(crate) timing: TimingSettings,
pub(crate) packets: PacketSettings,
pub(crate) agent: AgentSettings,
pub(crate) world: WorldSettings,
pub(crate) parcel: ParcelSettings,
pub(crate) asset_cache: AssetCacheSettings,
pub(crate) texture_pipeline: TexturePipelineSettings,
pub(crate) logging: LoggingSettings,
pub(crate) upload_cost: i32,
}
impl Settings {
pub(crate) fn defaults() -> Self {
Self {
default_effect_color: libremetaverse_types::Color4 {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
connection: connection_settings_defaults(),
timing: timing_settings_defaults(),
packets: packet_settings_defaults(),
agent: agent_settings_defaults(),
world: world_settings_defaults(),
parcel: parcel_settings_defaults(),
asset_cache: asset_cache_settings_defaults(),
texture_pipeline: texture_pipeline_settings_defaults(),
logging: logging_settings_defaults(),
upload_cost: 0,
}
}
#[must_use]
pub fn connection_mut(&mut self) -> &mut ConnectionSettings {
&mut self.connection
}
#[must_use]
pub fn packets_mut(&mut self) -> &mut PacketSettings {
&mut self.packets
}
#[must_use]
pub fn agent_mut(&mut self) -> &mut AgentSettings {
&mut self.agent
}
#[must_use]
pub fn world_mut(&mut self) -> &mut WorldSettings {
&mut self.world
}
#[must_use]
pub fn parcel_mut(&mut self) -> &mut ParcelSettings {
&mut self.parcel
}
#[must_use]
pub fn asset_cache_mut(&mut self) -> &mut AssetCacheSettings {
&mut self.asset_cache
}
#[must_use]
pub fn texture_pipeline_mut(&mut self) -> &mut TexturePipelineSettings {
&mut self.texture_pipeline
}
#[must_use]
pub fn logging_mut(&mut self) -> &mut LoggingSettings {
&mut self.logging
}
/// Validates all durations, limits, endpoints, cache, and feature policies.
///
/// # Errors
///
/// Returns the first field that cannot be used safely by the native client.
pub fn validate(&self) -> Result<(), ClientCoreError> {
validate_endpoint("Connection.LoginServer", &self.connection.login_server)?;
for (field, value) in [
("Timing.TransferTimeout", self.timing.transfer_timeout),
("Timing.TeleportTimeout", self.timing.teleport_timeout),
("Timing.LogoutTimeout", self.timing.logout_timeout),
("Timing.CapsTimeout", self.timing.caps_timeout),
("Timing.LoginTimeout", self.timing.login_timeout),
("Timing.ResendTimeout", self.timing.resend_timeout),
("Timing.SimulatorTimeout", self.timing.simulator_timeout),
("Timing.MapRequestTimeout", self.timing.map_request_timeout),
(
"Timing.AgentUpdateInterval",
self.timing.agent_update_interval,
),
(
"Timing.InterpolationInterval",
self.timing.interpolation_interval,
),
] {
validate_positive(field, value)?;
}
validate_positive("Packets.MaxPendingAcks", self.packets.max_pending_acks)?;
validate_positive("Packets.StatsQueueSize", self.packets.stats_queue_size)?;
if self.packets.max_resend_count < 0 {
return invalid("Packets.MaxResendCount", "must be zero or greater");
}
if self.agent.send_updates_regularly && !self.agent.send_updates {
return invalid("Agent.SendUpdatesRegularly", "requires Agent.SendUpdates");
}
if (self.parcel.always_request_acl || self.parcel.always_request_dwell)
&& !self.parcel.track_parcels
{
return invalid(
"Parcel.TrackParcels",
"must be enabled for automatic parcel requests",
);
}
validate_positive(
"TexturePipeline.MaxConcurrentDownloads",
self.texture_pipeline.max_concurrent_downloads,
)?;
validate_positive(
"TexturePipeline.RequestTimeout",
self.texture_pipeline.request_timeout,
)?;
if self.asset_cache.enabled {
if self.asset_cache.dir.trim().is_empty() {
return invalid(
"AssetCache.Dir",
"must not be empty when caching is enabled",
);
}
if self.asset_cache.max_size <= 0 {
return invalid(
"AssetCache.MaxSize",
"must be positive when caching is enabled",
);
}
}
Ok(())
}
}
impl Default for Settings {
fn default() -> Self {
Self::defaults()
}
}
impl fmt::Debug for Settings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Settings")
.field("default_effect_color", &self.default_effect_color)
.field("connection", &self.connection)
.field("timing", &self.timing)
.field("packets", &self.packets)
.field("agent", &self.agent)
.field("world", &self.world)
.field("parcel", &self.parcel)
.field("asset_cache", &self.asset_cache)
.field("texture_pipeline", &self.texture_pipeline)
.field("logging", &self.logging)
.field("upload_cost", &self.upload_cost)
.finish()
}
}
impl fmt::Debug for ConnectionSettings {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ConnectionSettings")
.field("login_server", &"<redacted endpoint>")
.field("mfa_enabled", &self.mfa_enabled)
.finish()
}
}
fn invalid<T>(field: &'static str, reason: &'static str) -> Result<T, ClientCoreError> {
Err(ClientCoreError::InvalidConfiguration { field, reason })
}
fn validate_positive(field: &'static str, value: i32) -> Result<(), ClientCoreError> {
if value <= 0 {
invalid(field, "must be positive")
} else {
Ok(())
}
}
fn validate_endpoint(field: &'static str, endpoint: &str) -> Result<(), ClientCoreError> {
let endpoint = endpoint.trim();
let remainder = endpoint
.strip_prefix("https://")
.or_else(|| endpoint.strip_prefix("http://"))
.ok_or(ClientCoreError::InvalidConfiguration {
field,
reason: "must use an HTTP or HTTPS URL",
})?;
let authority = remainder.split(['/', '?', '#']).next().unwrap_or_default();
if authority.is_empty() || authority.contains('@') || endpoint.chars().any(char::is_whitespace)
{
return invalid(
field,
"must contain a host and must not embed credentials or whitespace",
);
}
Ok(())
}
pub(crate) fn agent_settings_defaults() -> AgentSettings {
AgentSettings {
disable_update_duplicate_check: true,
multiple_sims: false,
send_appearance: true,
send_throttle: true,
send_updates: true,
send_updates_regularly: true,
}
}
pub(crate) fn asset_cache_settings_defaults() -> AssetCacheSettings {
AssetCacheSettings {
dir: std::path::Path::new("linden")
.join("cache")
.to_string_lossy()
.into_owned(),
enabled: true,
max_size: 1_024 * 1_024 * 1_024,
}
}
pub(crate) fn connection_settings_defaults() -> ConnectionSettings {
ConnectionSettings {
login_server: Settings::AGNI_LOGIN_SERVER.to_owned(),
mfa_enabled: false,
}
}
pub(crate) fn logging_settings_defaults() -> LoggingSettings {
LoggingSettings {
log_disk_cache: true,
log_names: true,
log_resends: true,
}
}
pub(crate) fn packet_settings_defaults() -> PacketSettings {
PacketSettings {
enable_sim_stats: true,
max_pending_acks: 10,
max_resend_count: 3,
send_pings: true,
stats_queue_size: 5,
throttle_outgoing: true,
track_utilization: false,
}
}
pub(crate) fn parcel_settings_defaults() -> ParcelSettings {
ParcelSettings {
always_request_acl: true,
always_request_dwell: true,
pool_parcel_data: false,
track_parcels: true,
}
}
pub(crate) fn texture_pipeline_settings_defaults() -> TexturePipelineSettings {
TexturePipelineSettings {
enabled: true,
max_concurrent_downloads: 4,
request_timeout: 45_000,
use_http_textures: true,
}
}
pub(crate) fn timing_settings_defaults() -> TimingSettings {
TimingSettings {
agent_update_interval: 500,
caps_timeout: 60_000,
interpolation_interval: 250,
login_timeout: 60_000,
logout_timeout: 5_000,
map_request_timeout: 5_000,
resend_timeout: 4_000,
simulator_timeout: 30_000,
teleport_timeout: 40_000,
transfer_timeout: 90_000,
}
}
pub(crate) fn world_settings_defaults() -> WorldSettings {
WorldSettings {
always_decode_objects: true,
always_request_objects: true,
cache_primitives: false,
store_land_patches: false,
track_avatars: true,
track_objects: true,
use_interpolation_timer: true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct RecordingService {
name: &'static str,
phase: ShutdownPhase,
order: Arc<Mutex<Vec<&'static str>>>,
calls: AtomicUsize,
}
impl ClientService for RecordingService {
fn name(&self) -> &'static str {
self.name
}
fn shutdown_phase(&self) -> ShutdownPhase {
self.phase
}
fn shutdown(&self) -> Result<(), ClientCoreError> {
self.calls.fetch_add(1, Ordering::Relaxed);
self.order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(self.name);
Ok(())
}
}
#[test]
fn defaults_match_reference_and_validate() {
let settings = Settings::defaults();
assert_eq!(
settings.connection.login_server,
Settings::AGNI_LOGIN_SERVER
);
assert_eq!(settings.timing.transfer_timeout, 90_000);
assert_eq!(settings.timing.teleport_timeout, 40_000);
assert_eq!(settings.timing.logout_timeout, 5_000);
assert_eq!(settings.timing.caps_timeout, 60_000);
assert_eq!(settings.timing.login_timeout, 60_000);
assert_eq!(settings.timing.resend_timeout, 4_000);
assert_eq!(settings.timing.simulator_timeout, 30_000);
assert_eq!(settings.timing.map_request_timeout, 5_000);
assert_eq!(settings.timing.agent_update_interval, 500);
assert_eq!(settings.timing.interpolation_interval, 250);
assert_eq!(settings.packets.max_pending_acks, 10);
assert_eq!(settings.packets.stats_queue_size, 5);
assert_eq!(settings.packets.max_resend_count, 3);
assert!(settings.packets.throttle_outgoing);
assert!(settings.packets.enable_sim_stats);
assert!(settings.packets.send_pings);
assert!(!settings.packets.track_utilization);
assert!(settings.agent.send_updates);
assert!(settings.agent.send_updates_regularly);
assert!(settings.agent.send_appearance);
assert!(settings.agent.send_throttle);
assert!(settings.agent.disable_update_duplicate_check);
assert!(!settings.agent.multiple_sims);
assert!(settings.world.always_decode_objects);
assert!(settings.world.always_request_objects);
assert!(settings.world.track_objects);
assert!(settings.world.track_avatars);
assert!(!settings.world.cache_primitives);
assert!(settings.world.use_interpolation_timer);
assert!(!settings.world.store_land_patches);
assert!(settings.parcel.track_parcels);
assert!(settings.parcel.always_request_acl);
assert!(settings.parcel.always_request_dwell);
assert!(!settings.parcel.pool_parcel_data);
assert!(settings.asset_cache.enabled);
assert!(settings.asset_cache.dir.ends_with("cache"));
assert_eq!(settings.texture_pipeline.max_concurrent_downloads, 4);
assert!(settings.texture_pipeline.enabled);
assert!(settings.texture_pipeline.use_http_textures);
assert_eq!(settings.texture_pipeline.request_timeout, 45_000);
assert!(settings.logging.log_names);
assert!(settings.logging.log_resends);
assert!(settings.logging.log_disk_cache);
assert_eq!(settings.asset_cache.max_size, 1_073_741_824);
assert_eq!(settings.default_effect_color.r, 1.0);
assert_eq!(settings.upload_cost, 0);
assert_eq!(settings.validate(), Ok(()));
assert_eq!(Settings::PING_INTERVAL, 2_200);
assert_eq!(Settings::NETWORK_TICK_INTERVAL, 500);
assert_eq!(Settings::MAX_PACKET_SIZE, 1_200);
assert_eq!(Settings::MAX_SEQUENCE, 0xFF_FFFF);
assert_eq!(Settings::user_agent(), "LibreMetaverse");
assert_eq!(Settings::resource_dir(), "linden");
assert_eq!(Settings::max_http_connections(), 32);
assert_eq!(Settings::packet_archive_size(), 1_000);
assert_eq!(Settings::udp_receive_queue_capacity(), 512);
assert_eq!(Settings::texture_pipeline_refresh_interval(), 500.0);
assert_eq!(Settings::simulator_pool_timeout(), 120_000);
assert!(Settings::bind_address().is_unspecified());
}
#[test]
fn validation_reports_the_exact_invalid_field() {
let mut settings = Settings::defaults();
settings.timing.login_timeout = 0;
assert_eq!(
settings.validate(),
Err(ClientCoreError::InvalidConfiguration {
field: "Timing.LoginTimeout",
reason: "must be positive",
})
);
let mut settings = Settings::defaults();
settings.connection.login_server = "https://user:secret@example.test/login".to_owned();
assert!(matches!(
settings.validate(),
Err(ClientCoreError::InvalidConfiguration {
field: "Connection.LoginServer",
..
})
));
assert!(!format!("{settings:?}").contains("secret"));
let mut settings = Settings::defaults();
settings.agent.send_updates = false;
assert_eq!(
settings.validate(),
Err(ClientCoreError::InvalidConfiguration {
field: "Agent.SendUpdatesRegularly",
reason: "requires Agent.SendUpdates",
})
);
}
#[test]
fn construction_is_inert_and_shutdown_is_ordered_and_idempotent() {
let order = Arc::new(Mutex::new(Vec::new()));
let manager = Arc::new(RecordingService {
name: "manager",
phase: ShutdownPhase::Manager,
order: Arc::clone(&order),
calls: AtomicUsize::new(0),
});
let network = Arc::new(RecordingService {
name: "network",
phase: ShutdownPhase::Network,
order: Arc::clone(&order),
calls: AtomicUsize::new(0),
});
let client = GridClient::builder()
.with_service(manager.clone())
.with_service(network.clone())
.build()
.unwrap();
assert_eq!(client.lifecycle_state(), ClientLifecycleState::Active);
assert!(!client.cancellation_token().is_cancellation_requested());
client.shutdown().unwrap();
client.shutdown().unwrap();
assert_eq!(client.lifecycle_state(), ClientLifecycleState::Disposed);
assert!(client.cancellation_token().is_cancellation_requested());
assert_eq!(
*order
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec!["network", "manager"]
);
assert_eq!(network.calls.load(Ordering::Relaxed), 1);
assert_eq!(manager.calls.load(Ordering::Relaxed), 1);
}
}