From 05d5e53788899a30c9da0f47709f0ca23b871ea7 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 25 Sep 2026 05:45:07 +0200 Subject: [PATCH] Release handlers on close to break session reference cycles Handlers commonly capture a clone of their own Session, and the Session owns its handlers, so a closed session was never freed. On shutdown, take the on_close handler instead of cloning it and clear the request and notification handlers after it runs. Bump to 0.2.1. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/session.rs | 7 ++++++- tests/protocol.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48a6f12..219083f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -895,7 +895,7 @@ dependencies = [ [[package]] name = "session-rs" -version = "0.2.0" +version = "0.2.1" dependencies = [ "axum", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 4c6a873..63e0609 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "session-rs" -version = "0.2.0" +version = "0.2.1" edition = "2024" description = "A lightweight async request/response and notification protocol over WebSockets" license = "Apache-2.0" diff --git a/src/session.rs b/src/session.rs index e351bb1..f7f1c21 100644 --- a/src/session.rs +++ b/src/session.rs @@ -431,10 +431,15 @@ impl Session { // Dropping the senders fails every waiting `request`. drop(std::mem::take(&mut *self.inner.pending.lock().unwrap())); - let handler = self.inner.on_close.lock().unwrap().clone(); + let handler = self.inner.on_close.lock().unwrap().take(); if let Some(handler) = handler { let _ = handler().await; } + + // Handlers usually capture a clone of this session; dropping them + // breaks that cycle so the session can be freed. + self.inner.requests.lock().unwrap().clear(); + self.inner.notifications.lock().unwrap().clear(); } } diff --git a/tests/protocol.rs b/tests/protocol.rs index 9374b8f..5d57650 100644 --- a/tests/protocol.rs +++ b/tests/protocol.rs @@ -450,3 +450,43 @@ async fn axum_adapter_serves_sessions_next_to_http_routes() { tcp.read_to_string(&mut body).await.unwrap(); assert!(body.starts_with("HTTP/1.1 200") && body.ends_with("ok"), "{body}"); } + +#[tokio::test] +async fn closing_releases_handlers_that_capture_the_session() { + let client = connect(start_server().await).await; + let captured = Arc::new(()); + + // Handlers typically hold a clone of their own session; that must not keep + // the session (and everything the handlers capture) alive after close. + client + .on_request::({ + let session = client.clone(); + let captured = captured.clone(); + move |_, ()| { + let _ = (&session, &captured); + async { Ok(()) } + } + }) + .await; + client + .on_close({ + let session = client.clone(); + let captured = captured.clone(); + move || { + let _ = (&session, &captured); + async { Ok(()) } + } + }) + .await; + assert_eq!(Arc::strong_count(&captured), 3); + + client.close().await.unwrap(); + + tokio::time::timeout(WAIT, async { + while Arc::strong_count(&captured) > 1 { + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("handlers were not released after close"); +} -- 2.54.0