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:
Generated
+1
-1
@@ -895,7 +895,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "session-rs"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"futures-util",
|
||||
|
||||
+1
-1
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user