From a9c4260b4ee03524ce5cd2d36d36a643198ae3b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 04:58:24 +0000 Subject: [PATCH] fix: close QUIC connection on hangup so relay removes participant immediately stop_call() now calls close_now() on the stored transport handle before killing the tokio runtime. This sends a QUIC CONNECTION_CLOSE frame so the relay's recv loop breaks immediately, triggering leave() + RoomUpdate broadcast. Previously the runtime was killed first, so transport.close() never ran and the relay kept stale participants until idle timeout. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/wzp-android/src/engine.rs | 12 ++++++++++++ crates/wzp-transport/src/quic.rs | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/crates/wzp-android/src/engine.rs b/crates/wzp-android/src/engine.rs index 08ec63e..b459bc7 100644 --- a/crates/wzp-android/src/engine.rs +++ b/crates/wzp-android/src/engine.rs @@ -67,6 +67,9 @@ pub(crate) struct EngineState { pub playout_ring: AudioRing, /// Current audio level (RMS) for UI display, updated by capture path. pub audio_level_rms: AtomicU32, + /// QUIC transport handle — stored so stop_call() can close it immediately, + /// triggering relay-side leave + RoomUpdate broadcast. + pub quic_transport: Mutex>>, } pub struct WzpEngine { @@ -87,6 +90,7 @@ impl WzpEngine { capture_ring: AudioRing::new(), playout_ring: AudioRing::new(), audio_level_rms: AtomicU32::new(0), + quic_transport: Mutex::new(None), }); Self { state, @@ -145,6 +149,11 @@ impl WzpEngine { pub fn stop_call(&mut self) { self.state.running.store(false, Ordering::Release); + // Close QUIC connection immediately so the relay detects disconnect + // and removes us from the room (broadcasts RoomUpdate to others). + if let Some(transport) = self.state.quic_transport.lock().unwrap().take() { + transport.close_now(); + } let _ = self.state.command_tx.send(EngineCommand::Stop); if let Some(rt) = self.tokio_runtime.take() { rt.shutdown_background(); @@ -223,6 +232,9 @@ async fn run_call( let transport = Arc::new(wzp_transport::QuinnTransport::new(conn)); + // Store transport handle so stop_call() can close the connection immediately + *state.quic_transport.lock().unwrap() = Some(transport.clone()); + // Crypto handshake let mut kx = WarzoneKeyExchange::from_identity_seed(identity_seed); let ephemeral_pub = kx.generate_ephemeral(); diff --git a/crates/wzp-transport/src/quic.rs b/crates/wzp-transport/src/quic.rs index 68fddb2..40c0cea 100644 --- a/crates/wzp-transport/src/quic.rs +++ b/crates/wzp-transport/src/quic.rs @@ -33,6 +33,12 @@ impl QuinnTransport { &self.connection } + /// Close the QUIC connection immediately (synchronous, no async needed). + /// The relay will detect the close and remove this participant from the room. + pub fn close_now(&self) { + self.connection.close(quinn::VarInt::from_u32(0), b"hangup"); + } + /// Feed an external RTT observation (e.g. from QUIC path stats) into the path monitor. pub fn feed_rtt(&self, rtt_ms: u32) { self.path_monitor.lock().unwrap().observe_rtt(rtt_ms);