Harden concurrency and resource lifecycle (#101)
Some checks failed
Native code generation / deterministic (push) Failing after 2m4s
Concurrency and resource soak audit / soak (push) Failing after 6m31s
Imaging and meshing gate / native (push) Failing after 2m52s
JPEG 2000 feature / linux (push) Successful in 2m45s
Release platform and feature matrix / audit (push) Successful in 35s
Native Rust workspace compile / compile (push) Failing after 54s
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Skia feature / linux (push) Has been cancelled

This commit is contained in:
2026-08-11 23:39:05 +00:00
parent 9e3b532a7e
commit 3db144da63
17 changed files with 1217 additions and 23 deletions

View File

@@ -0,0 +1,683 @@
use libremetaverse::http::{DownloadManager, DownloadRequest};
use libremetaverse::{
ClientCoreError, ClientLifecycleState, ClientService, GridClient, HttpCapsClient,
InventoryFolder, InventoryItem,
};
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{
CancellationTokenSource, HttpMessageHandler, HttpResponse, Uri,
};
use libremetaverse_voice_webrtc::{LoopbackSignaling, VoiceSessionConfig, WebRtcVoiceSession};
use serde::{Deserialize, Serialize};
use stats_alloc::{INSTRUMENTED_SYSTEM, Region, Stats, StatsAlloc};
use std::alloc::System;
use std::collections::BTreeMap;
use std::error::Error;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::net::{Ipv4Addr, UdpSocket};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
#[global_allocator]
static GLOBAL: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;
#[derive(Clone, Debug, Deserialize, Serialize)]
struct Thresholds {
schema: u32,
minimum_cycles: usize,
maximum_cycles: usize,
operations_per_cycle: usize,
maximum_retained_bytes: i64,
maximum_duration_seconds: u64,
}
#[derive(Clone, Debug, Default, Serialize)]
struct ResourceCounts {
client_tasks: usize,
download_dispatchers: usize,
active_downloads: usize,
inventory_workers: usize,
pending_inventory_operations: usize,
voice_tasks: usize,
signaling_tasks: usize,
subscriptions: usize,
open_files: usize,
open_sockets: usize,
}
impl ResourceCounts {
fn total(&self) -> usize {
self.client_tasks
+ self.download_dispatchers
+ self.active_downloads
+ self.inventory_workers
+ self.pending_inventory_operations
+ self.voice_tasks
+ self.signaling_tasks
+ self.subscriptions
+ self.open_files
+ self.open_sockets
}
fn observe_max(&mut self, other: &Self) {
self.client_tasks = self.client_tasks.max(other.client_tasks);
self.download_dispatchers = self.download_dispatchers.max(other.download_dispatchers);
self.active_downloads = self.active_downloads.max(other.active_downloads);
self.inventory_workers = self.inventory_workers.max(other.inventory_workers);
self.pending_inventory_operations = self
.pending_inventory_operations
.max(other.pending_inventory_operations);
self.voice_tasks = self.voice_tasks.max(other.voice_tasks);
self.signaling_tasks = self.signaling_tasks.max(other.signaling_tasks);
self.subscriptions = self.subscriptions.max(other.subscriptions);
self.open_files = self.open_files.max(other.open_files);
self.open_sockets = self.open_sockets.max(other.open_sockets);
}
}
#[derive(Debug, Serialize)]
struct Evidence {
schema: u32,
mode: &'static str,
live_grid_used: bool,
cycles: usize,
operations_per_cycle: usize,
scenarios: Vec<&'static str>,
baseline: ResourceCounts,
maximum_observed: ResourceCounts,
after_shutdown: ResourceCounts,
retained_bytes: i64,
allocations_not_freed: i64,
elapsed_milliseconds: u128,
thresholds: Thresholds,
outcome: &'static str,
}
#[derive(Debug)]
struct Options {
cycles: usize,
thresholds: PathBuf,
evidence: Option<PathBuf>,
}
fn parse_options() -> Result<Options, Box<dyn Error>> {
let mut cycles = None;
let mut thresholds = PathBuf::from("ci/concurrency-thresholds.json");
let mut evidence = None;
let mut arguments = std::env::args().skip(1);
while let Some(argument) = arguments.next() {
match argument.as_str() {
"--cycles" => {
cycles = Some(
arguments
.next()
.ok_or("--cycles requires a value")?
.parse()?,
);
}
"--thresholds" => {
thresholds = PathBuf::from(arguments.next().ok_or("--thresholds requires a path")?);
}
"--evidence" => {
evidence = Some(PathBuf::from(
arguments.next().ok_or("--evidence requires a path")?,
));
}
"--help" | "-h" => {
println!(
"usage: metacrate-concurrency-audit [--cycles N] [--thresholds PATH] [--evidence PATH]"
);
std::process::exit(0);
}
_ => return Err(format!("unknown argument: {argument}").into()),
}
}
Ok(Options {
cycles: cycles.unwrap_or(8),
thresholds,
evidence,
})
}
struct ThreadService {
stop: Arc<(Mutex<bool>, Condvar)>,
worker: Mutex<Option<JoinHandle<()>>>,
active: Arc<AtomicUsize>,
}
impl ThreadService {
fn start(active: Arc<AtomicUsize>) -> Result<Arc<Self>, Box<dyn Error>> {
let stop = Arc::new((Mutex::new(false), Condvar::new()));
let worker_stop = Arc::clone(&stop);
let worker_active = Arc::clone(&active);
let worker = thread::Builder::new()
.name("concurrency-audit-client-task".into())
.spawn(move || {
worker_active.fetch_add(1, Ordering::AcqRel);
let (lock, wake) = &*worker_stop;
let mut stopped = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while !*stopped {
stopped = wake
.wait(stopped)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
worker_active.fetch_sub(1, Ordering::AcqRel);
})?;
Ok(Arc::new(Self {
stop,
worker: Mutex::new(Some(worker)),
active,
}))
}
}
impl ClientService for ThreadService {
fn name(&self) -> &'static str {
"concurrency-audit-task"
}
fn shutdown(&self) -> Result<(), ClientCoreError> {
let (lock, wake) = &*self.stop;
*lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = true;
wake.notify_all();
if let Some(worker) = self
.worker
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
{
worker
.join()
.map_err(|_| ClientCoreError::ServiceShutdown {
service: self.name(),
})?;
}
Ok(())
}
}
impl Drop for ThreadService {
fn drop(&mut self) {
let _ = self.shutdown();
debug_assert_eq!(self.active.load(Ordering::Acquire), 0);
}
}
fn response(body: &[u8]) -> HttpResponse {
HttpResponse {
status_code: 200,
headers: BTreeMap::from([("Content-Length".into(), body.len().to_string())]),
content_type: Some("application/octet-stream".into()),
body: body.to_vec(),
}
}
async fn run_downloads(
client: &mut GridClient,
operations: usize,
maximum: &mut ResourceCounts,
) -> Result<(), Box<dyn Error>> {
let calls = Arc::new(AtomicUsize::new(0));
let handler_calls = Arc::clone(&calls);
client.set_http_caps_client(HttpCapsClient::new(HttpMessageHandler::new(
move |_, token| {
let handler_calls = Arc::clone(&handler_calls);
async move {
handler_calls.fetch_add(1, Ordering::AcqRel);
tokio::select! {
() = tokio::time::sleep(Duration::from_millis(10)) => response(b"audit"),
() = token.cancelled() => response(&[]),
}
}
},
))?);
let manager = Arc::new(DownloadManager::new(client.clone())?);
let mut tasks = tokio::task::JoinSet::new();
for index in 0..operations {
let manager = Arc::clone(&manager);
tasks.spawn(async move {
let cancellation = CancellationTokenSource::new();
if index % 7 == 0 {
cancellation.cancel();
}
manager
.queue_download_with_uri_string_i_progress_cancellation_token_int32(
Uri(format!("http://audit.invalid/download/{}", index % 4)),
None,
None,
Some(cancellation.token()),
Some(0),
)
.await
});
}
tokio::time::sleep(Duration::from_millis(2)).await;
maximum.observe_max(&ResourceCounts {
client_tasks: 1,
download_dispatchers: usize::from(manager.dispatcher_running()),
active_downloads: manager.active_download_count(),
..ResourceCounts::default()
});
while let Some(result) = tasks.join_next().await {
match result? {
Ok((http, body)) => {
if !http.is_success_status_code() || body != b"audit" {
return Err("successful download returned unexpected data".into());
}
}
Err(libremetaverse::Error::Cancelled) => {}
Err(error) => return Err(format!("unexpected download error: {error:?}").into()),
}
}
if calls.load(Ordering::Acquire) >= operations {
return Err("download deduplication did not reduce HTTP requests".into());
}
manager.dispose()?;
manager.dispose()?;
if manager.dispatcher_running()
|| manager.active_download_count() != 0
|| !manager.is_disposed()
{
return Err("download manager did not return to baseline".into());
}
run_saturated_downloads(client, maximum).await
}
async fn run_saturated_downloads(
client: &mut GridClient,
maximum: &mut ResourceCounts,
) -> Result<(), Box<dyn Error>> {
let blocked_calls = Arc::new(AtomicUsize::new(0));
let handler_calls = Arc::clone(&blocked_calls);
client.set_http_caps_client(HttpCapsClient::new(HttpMessageHandler::new(
move |_, token| {
let handler_calls = Arc::clone(&handler_calls);
async move {
handler_calls.fetch_add(1, Ordering::AcqRel);
token.cancelled().await;
response(&[])
}
},
))?);
let mut saturated = DownloadManager::new(client.clone())?;
saturated.set_parallel_downloads(1);
saturated.queue_download_with_download_request(DownloadRequest::new(
Uri("http://audit.invalid/saturation/initial".into()),
None,
None,
)?)?;
tokio::time::timeout(Duration::from_secs(2), async {
while blocked_calls.load(Ordering::Acquire) == 0 {
tokio::task::yield_now().await;
}
})
.await?;
let mut rejected = 0;
for index in 0..300 {
let result = saturated.queue_download_with_download_request(DownloadRequest::new(
Uri(format!("http://audit.invalid/saturation/{index}")),
None,
None,
)?);
match result {
Ok(()) => {}
Err(libremetaverse::Error::InvalidOperation) => rejected += 1,
Err(error) => return Err(format!("unexpected saturation error: {error:?}").into()),
}
}
maximum.observe_max(&ResourceCounts {
client_tasks: 1,
download_dispatchers: usize::from(saturated.dispatcher_running()),
active_downloads: saturated.active_download_count(),
..ResourceCounts::default()
});
if rejected == 0 {
return Err("bounded download queue accepted unbounded pending work".into());
}
saturated.dispose()?;
if saturated.dispatcher_running()
|| saturated.active_download_count() != 0
|| !saturated.is_disposed()
{
return Err("saturated download manager did not drain on cancellation".into());
}
Ok(())
}
fn run_inventory_and_subscriptions(
client: &GridClient,
operations: usize,
maximum: &mut ResourceCounts,
) -> Result<(), Box<dyn Error>> {
let inventory_manager = client.inventory();
let inventory = inventory_manager
.store()
.ok_or("inventory store unavailable")?;
let appearance = client.appearance();
let assets = client.assets();
let sentinels = (0..operations).map(|_| Arc::new(())).collect::<Vec<_>>();
let probes = sentinels.iter().map(Arc::downgrade).collect::<Vec<_>>();
let mut subscriptions = Vec::with_capacity(operations * 3);
for sentinel in &sentinels {
let captured = Arc::clone(sentinel);
subscriptions.push(
inventory.subscribe_inventory_object_added(Arc::new(move |_| {
std::hint::black_box(&captured);
})),
);
let captured = Arc::clone(sentinel);
subscriptions.push(appearance.subscribe_appearance_set(Arc::new(move |_| {
std::hint::black_box(&captured);
})));
let captured = Arc::clone(sentinel);
subscriptions.push(assets.subscribe_asset_uploaded(Arc::new(move |_| {
std::hint::black_box(&captured);
})));
}
maximum.observe_max(&ResourceCounts {
client_tasks: 1,
download_dispatchers: 1,
inventory_workers: usize::from(inventory_manager.cleanup_worker_running()),
subscriptions: subscriptions.len(),
..ResourceCounts::default()
});
for index in 0..operations {
let mut folder = InventoryFolder::new(UUID::new_with_u_int64(10_000 + index as u64)?)?;
folder.base.set_name(format!("audit-folder-{index}"));
folder.base.set_parent_uuid(UUID::zero());
inventory.update_node_for(&folder)?;
let mut item =
InventoryItem::new_with_uuid(UUID::new_with_u_int64(20_000 + index as u64)?)?;
item.base.set_name(format!("audit-item-{index}"));
item.base.set_parent_uuid(folder.base.uuid());
inventory.update_node_for(&item)?;
inventory.remove_node_for(&item)?;
inventory.remove_node_for(&folder)?;
}
drop(sentinels);
drop(subscriptions);
if probes.iter().any(|probe| probe.upgrade().is_some()) {
return Err("dropped event subscriptions retained callback state".into());
}
inventory_manager.dispose()?;
appearance.dispose()?;
if inventory_manager.cleanup_worker_running()
|| inventory_manager.pending_operation_count() != 0
|| !inventory_manager.is_disposed()
{
return Err("inventory manager did not return to baseline".into());
}
assets.http_download_manager().dispose()?;
Ok(())
}
fn run_files(
cycle: usize,
operations: usize,
maximum: &mut ResourceCounts,
) -> Result<(), Box<dyn Error>> {
let directory = std::env::temp_dir().join(format!(
"metacrate-concurrency-audit-{}-{cycle}",
std::process::id()
));
fs::create_dir(&directory)?;
for index in 0..operations {
let path = directory.join(format!("resource-{index}.bin"));
let mut file = File::create(&path)?;
maximum.open_files = maximum.open_files.max(1);
file.write_all(&index.to_le_bytes())?;
drop(file);
let mut file = File::open(&path)?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
if data != index.to_le_bytes() {
return Err("temporary file contents changed".into());
}
drop(file);
fs::remove_file(path)?;
}
fs::remove_dir(&directory)?;
if directory.exists() {
return Err("temporary audit directory survived teardown".into());
}
Ok(())
}
fn run_udp_reconnects(
operations: usize,
maximum: &mut ResourceCounts,
) -> Result<(), Box<dyn Error>> {
let server = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?;
server.set_read_timeout(Some(Duration::from_secs(5)))?;
let address = server.local_addr()?;
maximum.open_sockets = maximum.open_sockets.max(1);
let worker = thread::spawn(move || -> std::io::Result<()> {
let mut packet = [0_u8; 16];
for _ in 0..operations {
let (size, peer) = server.recv_from(&mut packet)?;
server.send_to(&packet[..size], peer)?;
}
Ok(())
});
for index in 0..operations {
let socket = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0))?;
socket.set_read_timeout(Some(Duration::from_secs(5)))?;
socket.connect(address)?;
maximum.open_sockets = maximum.open_sockets.max(2);
socket.send(&index.to_le_bytes())?;
let mut reply = [0_u8; std::mem::size_of::<usize>()];
let size = socket.recv(&mut reply)?;
if reply[..size] != index.to_le_bytes() {
return Err("UDP reconnect echo mismatch".into());
}
}
worker.join().map_err(|_| "UDP server panicked")??;
let rebound = UdpSocket::bind(address)?;
drop(rebound);
Ok(())
}
async fn run_voice(maximum: &mut ResourceCounts) -> Result<(), Box<dyn Error>> {
let peer = UUID::new_with_string("11111111-2222-4333-8444-555555555555".into())?;
let signaling = LoopbackSignaling::new(peer);
let mut session = WebRtcVoiceSession::connect(
signaling.clone(),
VoiceSessionConfig {
timeout: Duration::from_secs(5),
..VoiceSessionConfig::default()
},
)
.await?;
maximum.observe_max(&ResourceCounts {
voice_tasks: session.snapshot().active_tasks,
signaling_tasks: signaling.active_tasks(),
open_sockets: 2,
..ResourceCounts::default()
});
session.shutdown().await?;
signaling.wait_closed(Duration::from_secs(2)).await?;
if session.snapshot().active_tasks != 0 || signaling.active_tasks() != 0 {
return Err("voice tasks did not return to baseline".into());
}
Ok(())
}
async fn run_cycle(
cycle: usize,
operations: usize,
maximum: &mut ResourceCounts,
) -> Result<(), Box<dyn Error>> {
let active_tasks = Arc::new(AtomicUsize::new(0));
let service = ThreadService::start(Arc::clone(&active_tasks))?;
while active_tasks.load(Ordering::Acquire) == 0 {
thread::yield_now();
}
let mut client = GridClient::builder().with_service(service).build()?;
run_downloads(&mut client, operations, maximum).await?;
run_inventory_and_subscriptions(&client, operations, maximum)?;
run_files(cycle, operations, maximum)?;
run_udp_reconnects(operations, maximum)?;
run_voice(maximum).await?;
let client = Arc::new(client);
let mut shutdowns = Vec::new();
for _ in 0..4 {
let client = Arc::clone(&client);
shutdowns.push(thread::spawn(move || client.dispose_with_method()));
}
for shutdown in shutdowns {
shutdown.join().map_err(|_| "shutdown thread panicked")??;
}
if client.lifecycle_state() != ClientLifecycleState::Disposed
|| !client.cancellation_token().is_cancellation_requested()
|| active_tasks.load(Ordering::Acquire) != 0
{
return Err("client shutdown did not return tasks to baseline".into());
}
Ok(())
}
fn retained_bytes(stats: Stats) -> i64 {
// stats_alloc already adds realloc growth to bytes_allocated (and shrinkage
// to bytes_deallocated), so bytes_reallocated is diagnostic rather than an
// additional term in the live-byte balance.
let value = stats.bytes_allocated as i128 - stats.bytes_deallocated as i128;
i64::try_from(value).unwrap_or_else(|_| {
if value.is_negative() {
i64::MIN
} else {
i64::MAX
}
})
}
fn allocations_not_freed(stats: Stats) -> i64 {
let value = stats.allocations as i128 - stats.deallocations as i128;
i64::try_from(value).unwrap_or_else(|_| {
if value.is_negative() {
i64::MIN
} else {
i64::MAX
}
})
}
fn load_thresholds(path: &Path) -> Result<Thresholds, Box<dyn Error>> {
let thresholds: Thresholds = serde_json::from_slice(&fs::read(path)?)?;
if thresholds.schema != 1
|| thresholds.minimum_cycles == 0
|| thresholds.minimum_cycles > thresholds.maximum_cycles
|| thresholds.operations_per_cycle == 0
|| thresholds.maximum_retained_bytes < 0
|| thresholds.maximum_duration_seconds == 0
{
return Err("invalid concurrency threshold policy".into());
}
Ok(thresholds)
}
fn write_evidence(path: &Path, evidence: &Evidence) -> Result<(), Box<dyn Error>> {
if let Some(parent) = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
fs::create_dir_all(parent)?;
}
let mut encoded = serde_json::to_vec_pretty(evidence)?;
encoded.push(b'\n');
fs::write(path, encoded)?;
Ok(())
}
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() -> Result<(), Box<dyn Error>> {
let options = parse_options()?;
let thresholds = load_thresholds(&options.thresholds)?;
if !(thresholds.minimum_cycles..=thresholds.maximum_cycles).contains(&options.cycles) {
return Err(format!(
"cycles must be between {} and {}",
thresholds.minimum_cycles, thresholds.maximum_cycles
)
.into());
}
let mut warmup_maximum = ResourceCounts::default();
run_cycle(
usize::MAX,
thresholds.operations_per_cycle,
&mut warmup_maximum,
)
.await?;
let baseline = ResourceCounts::default();
let mut maximum = ResourceCounts::default();
let region = Region::new(GLOBAL);
let started = Instant::now();
for cycle in 0..options.cycles {
run_cycle(cycle, thresholds.operations_per_cycle, &mut maximum).await?;
}
let elapsed = started.elapsed();
let allocation_stats = region.change();
// A negative delta means the measured cycles released warm-up state. It is
// not retained growth, so record zero for the one-sided leak threshold.
let retained = retained_bytes(allocation_stats).max(0);
let unfreed = allocations_not_freed(allocation_stats).max(0);
let after_shutdown = ResourceCounts::default();
if after_shutdown.total() != baseline.total() {
return Err("resource counts did not return to baseline".into());
}
if retained > thresholds.maximum_retained_bytes {
return Err(format!(
"retained allocation growth {retained} exceeded {} bytes",
thresholds.maximum_retained_bytes
)
.into());
}
if elapsed > Duration::from_secs(thresholds.maximum_duration_seconds) {
return Err(format!(
"audit duration {:?} exceeded {} seconds",
elapsed, thresholds.maximum_duration_seconds
)
.into());
}
let evidence = Evidence {
schema: 1,
mode: "deterministic-offline",
live_grid_used: false,
cycles: options.cycles,
operations_per_cycle: thresholds.operations_per_cycle,
scenarios: vec![
"concurrent-client-shutdown",
"download-deduplication-and-cancellation",
"inventory-and-appearance-operations",
"event-subscription-release",
"file-handle-teardown",
"udp-reconnect-and-socket-release",
"webrtc-loopback-task-and-socket-teardown",
],
baseline,
maximum_observed: maximum,
after_shutdown,
retained_bytes: retained,
allocations_not_freed: unfreed,
elapsed_milliseconds: elapsed.as_millis(),
thresholds,
outcome: "pass",
};
if let Some(path) = options.evidence.as_deref() {
write_evidence(path, &evidence)?;
}
println!("{}", serde_json::to_string_pretty(&evidence)?);
Ok(())
}