Compare commits

...
2 Commits
Author SHA1 Message Date
Riz Ashraf b9e44e1fa3 fix(server): Fix memory leak in handle_socket on abrupt disconnects
- Used a SessionCleanup Drop guard (RAII pattern) to guarantee that the websocket session is evicted from the global clients map and background read/write tasks are forcibly aborted even when Axum terminates the handler mid-execution due to a TCP connection loss.
2026-09-20 23:08:40 +01:00
Riz Ashraf acf5ea8ebe fix(stub): Prevent infinite reconnect loop causing port exhaustion
- Added a break statement when send_task exits (STDIN closed).
- Increased reconnect delay from 50ms to 1 second to prevent ephemeral port exhaustion when the server repeatedly drops connections.
2026-09-20 23:02:55 +01:00
2 changed files with 44 additions and 18 deletions

No files matched your search

+37 -14
View File
@@ -557,22 +557,45 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, client_type: Str
); );
}); });
tokio::select! { struct SessionCleanup {
_ = (&mut send_task) => { session_id: String,
tracing::info!("Websocket send task finished for session {}", session_id); state: Arc<AppState>,
recv_task.abort(); send_task: Option<tokio::task::JoinHandle<()>>,
}, recv_task: Option<tokio::task::JoinHandle<()>>,
_ = (&mut recv_task) => { }
tracing::info!("Websocket recv task finished for session {}", session_id);
send_task.abort(); impl Drop for SessionCleanup {
}, fn drop(&mut self) {
self.state.clients.write().unwrap().remove(&self.session_id);
if let Some(task) = self.send_task.take() {
task.abort();
}
if let Some(task) = self.recv_task.take() {
task.abort();
}
tracing::info!(
"Websocket session {} closed and cleaned up",
self.session_id
);
}
}
let mut cleanup = SessionCleanup {
session_id: session_id.clone(),
state: Arc::clone(&state),
send_task: Some(send_task),
recv_task: Some(recv_task),
}; };
state.clients.write().unwrap().remove(&session_id); tokio::select! {
tracing::info!( _ = cleanup.send_task.as_mut().unwrap() => {
"Websocket session {} closed and removed from state", tracing::info!("Websocket send task finished for session {}", session_id);
session_id },
); _ = cleanup.recv_task.as_mut().unwrap() => {
tracing::info!("Websocket recv task finished for session {}", session_id);
},
};
// Drop guard automatically handles removal and aborts the other task.
} }
#[derive(serde::Deserialize, serde::Serialize, Debug)] #[derive(serde::Deserialize, serde::Serialize, Debug)]
+7 -4
View File
@@ -117,20 +117,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}); });
tokio::select! { tokio::select! {
_ = shutdown_rx.recv() => { tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; _ = shutdown_rx.recv() => {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
tracing::info!("Shutdown received while connected"); tracing::info!("Shutdown received while connected");
break;
} }
_ = &mut send_task => { _ = &mut send_task => {
tracing::error!("Send task exited"); tracing::error!("Send task exited");
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; recv_task.abort(); tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
recv_task.abort();
break;
} }
_ = &mut recv_task => { _ = &mut recv_task => {
tracing::error!("Recv task exited"); tracing::error!("Recv task exited");
send_task.abort(); send_task.abort();
} }
} }
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
} }
Err(e) => { Err(e) => {
tracing::error!("Failed to connect via WSS: {}", e); tracing::error!("Failed to connect via WSS: {}", e);