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 <[email protected]>
This commit is contained in:
2026-09-25 05:45:07 +02:00
co-authored by claude
parent c9df3b5717
commit 05d5e53788
4 changed files with 48 additions and 3 deletions
Generated
+1 -1
View File
@@ -895,7 +895,7 @@ dependencies = [
[[package]]
name = "session-rs"
version = "0.2.0"
version = "0.2.1"
dependencies = [
"axum",
"futures-util",
+1 -1
View File
@@ -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"
+6 -1
View File
@@ -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();
}
}
+40
View File
@@ -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::<Silent, _>({
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");
}