fix(room): fix two major memory leaks
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions

1. WS disconnect now unsubscribes from user_notification_inner.
   Previously, every WebSocket connection created a broadcast channel
   for user notifications that was never removed on disconnect, causing
   unbounded growth proportional to unique connected users over time.

2. Room worker tasks now use the manager's room_shutdown_txs channel
   instead of a local broadcast channel. shutdown_room() sends on this
   channel, so when a room is deleted the worker task receives the signal
   and terminates, releasing its DashMap (capacity 10,000) and all
   captured closures. Previously the worker ran forever.
This commit is contained in:
ZhenYi 2026-04-26 16:52:20 +08:00
parent 15483b4e95
commit 0e53f4a69f
2 changed files with 12 additions and 10 deletions

View File

@ -445,6 +445,7 @@ pub async fn ws_universal(
for room_id in push_streams.keys() { for room_id in push_streams.keys() {
manager.unsubscribe(*room_id, user_id).await; manager.unsubscribe(*room_id, user_id).await;
} }
manager.unsubscribe_user_notification(user_id).await;
manager.metrics.ws_connections_active.decrement(1.0); manager.metrics.ws_connections_active.decrement(1.0);
manager.metrics.ws_disconnections_total.increment(1); manager.metrics.ws_disconnections_total.increment(1);
}); });

View File

@ -270,19 +270,21 @@ pub fn spawn_room_workers(
); );
let get_redis: Arc<dyn Fn() -> queue::worker::RedisFuture + Send + Sync> = let get_redis: Arc<dyn Fn() -> queue::worker::RedisFuture + Send + Sync> =
extract_get_redis(queue.clone()); extract_get_redis(queue.clone());
let manager = room_manager.clone(); let manager1 = room_manager.clone();
let redis_url_clone = redis_url.clone();
let semaphore = worker_semaphore.clone();
let manager2 = room_manager.clone(); let manager2 = room_manager.clone();
let manager3 = room_manager.clone();
let redis_url_clone = redis_url.clone();
let redis_url3 = redis_url.clone(); let redis_url3 = redis_url.clone();
let semaphore = worker_semaphore.clone();
tokio::spawn(async move { tokio::spawn(async move {
let _permit = match semaphore.acquire_owned().await { let _permit = match semaphore.acquire_owned().await {
Ok(p) => p, Ok(p) => p,
Err(_) => return, Err(_) => return,
}; };
let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1); // Use the manager's room shutdown channel so the worker terminates
// when the room is deleted (shutdown_room sends on room_shutdown_txs).
let shutdown_rx = manager1.register_room(room_id).await;
queue::room_worker_task( queue::room_worker_task(
room_id, room_id,
uuid::Uuid::new_v4().to_string(), uuid::Uuid::new_v4().to_string(),
@ -291,14 +293,13 @@ pub fn spawn_room_workers(
shutdown_rx, shutdown_rx,
) )
.await; .await;
let _ = shutdown_tx.send(());
}); });
tokio::spawn(async move { tokio::spawn(async move {
let shutdown_rx = manager.register_room(room_id).await; let shutdown_rx = manager2.register_room(room_id).await;
crate::connection::subscribe_room_events( crate::connection::subscribe_room_events(
redis_url_clone, redis_url_clone,
manager.clone(), manager2,
room_id, room_id,
shutdown_rx, shutdown_rx,
) )
@ -317,10 +318,10 @@ pub fn spawn_room_workers(
None => return, None => return,
} }
}; };
let shutdown_rx = manager2.register_project(project_id).await; let shutdown_rx = manager3.register_project(project_id).await;
crate::connection::subscribe_project_room_events( crate::connection::subscribe_project_room_events(
redis_url3, redis_url3,
manager2, manager3,
project_id, project_id,
shutdown_rx, shutdown_rx,
) )