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

@@ -20,7 +20,7 @@ use std::io::Cursor;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::Path;
use std::pin::Pin;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use str0m::change::{SdpAnswer, SdpOffer};
@@ -1426,6 +1426,7 @@ where
/// Deterministic native WebRTC answerer used by CI and offline diagnostics.
pub struct LoopbackSignaling {
state: tokio::sync::Mutex<LoopbackState>,
provisioning: AtomicBool,
active_tasks: Arc<AtomicUsize>,
received_messages: Arc<Mutex<Vec<String>>>,
peer_id: UUID,
@@ -1448,6 +1449,7 @@ impl LoopbackSignaling {
completed: false,
closed: false,
}),
provisioning: AtomicBool::new(false),
active_tasks: Arc::new(AtomicUsize::new(0)),
received_messages: Arc::new(Mutex::new(Vec::new())),
peer_id,
@@ -1481,10 +1483,21 @@ impl LoopbackSignaling {
impl VoiceSignaling for LoopbackSignaling {
fn provision(&self, request: ProvisionRequest) -> SignalFuture<'_, ProvisionResponse> {
Box::pin(async move {
let mut state = self.state.lock().await;
if state.task.is_some() {
struct ProvisionGuard<'a>(&'a AtomicBool);
impl Drop for ProvisionGuard<'_> {
fn drop(&mut self) {
self.0.store(false, Ordering::Release);
}
}
if self
.provisioning
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return Err(WebRtcError::Signaling("duplicate provision request".into()));
}
let _provisioning = ProvisionGuard(&self.provisioning);
if request.jsep.kind != "offer"
|| request.channel_type != "local"
|| request.voice_server_type != "webrtc"
@@ -1511,6 +1524,10 @@ impl VoiceSignaling for LoopbackSignaling {
let viewer_session = UUID::random()
.map_err(|_| WebRtcError::Signaling("could not create session id".into()))?;
let credentials = VoiceSecret::new("offline-secret")?;
let mut state = self.state.lock().await;
if state.task.is_some() {
return Err(WebRtcError::Signaling("duplicate provision request".into()));
}
active.fetch_add(1, Ordering::AcqRel);
let task = tokio::spawn(async move {
Box::pin(run_loopback_peer(
@@ -1815,6 +1832,33 @@ mod tests {
assert_eq!(signaling.active_tasks(), 0);
}
#[tokio::test]
async fn concurrent_provisioning_admits_one_session_without_leaking_the_loser() {
let peer = UUID::new_with_string("11111111-2222-4333-8444-555555555555".into()).unwrap();
let signaling = LoopbackSignaling::new(peer);
let config = VoiceSessionConfig {
timeout: Duration::from_secs(10),
..VoiceSessionConfig::default()
};
let (first, second) = tokio::join!(
WebRtcVoiceSession::connect(signaling.clone(), config.clone()),
WebRtcVoiceSession::connect(signaling.clone(), config),
);
let (mut session, rejected) = match (first, second) {
(Ok(session), Err(error)) | (Err(error), Ok(session)) => (session, error),
(Ok(_), Ok(_)) => panic!("duplicate provisioning created two sessions"),
(Err(first), Err(second)) => {
panic!("both provisioning attempts failed: {first}; {second}")
}
};
assert!(matches!(rejected, WebRtcError::Signaling(_)));
session.shutdown().await.unwrap();
signaling.wait_closed(Duration::from_secs(2)).await.unwrap();
assert_eq!(session.snapshot().active_tasks, 0);
assert_eq!(signaling.active_tasks(), 0);
}
#[tokio::test]
async fn failed_answer_closes_provisioned_server_session() {
let signaling = Arc::new(InvalidAnswerSignaling {