Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions aimdb-websocket-connector/src/server/client_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ struct SubEntry {
dropped: AtomicU64,
}

/// Drop guard for SubEntry
struct SubEntryGuard {
id: u64,
subs: Arc<DashMap<u64, SubEntry>>,
}

impl Drop for SubEntryGuard {
fn drop(&mut self) {
self.subs.remove(&self.id);
}
}

/// Shared per-topic broadcast bus. Cloning is cheap (all clones share state).
#[derive(Clone)]
pub struct ClientManager {
Expand Down Expand Up @@ -96,8 +108,13 @@ impl ClientManager {
dropped: AtomicU64::new(0),
},
);
let stream = futures_util::stream::unfold(rx, |mut rx| async move {
rx.recv().await.map(|item| (item, rx))
// A drop guard for RAII, thankfully self.subs is already Arc<_>
let guard = SubEntryGuard {
id,
subs: self.subs.clone(),
};
let stream = futures_util::stream::unfold((rx, guard), |(mut rx, guard)| async move {
rx.recv().await.map(|item| (item, (rx, guard)))
});
(id, Box::pin(stream))
}
Expand Down Expand Up @@ -260,4 +277,19 @@ mod tests {
"every subscriber shares the one payload Arc"
);
}

// When a stream is dropped, its subscription in ClientManager
// together with its Sender must also be removed
#[tokio::test]
async fn subscription_dropped_when_stream_dropped() {
let mgr = ClientManager::new(256);
let (_id, stream) = mgr.subscribe("quiet.topic");

// Count before stream dropping
assert_eq!(mgr.subscription_count(), 1);
drop(stream);

// Associated entry must be unsubscribed
assert_eq!(mgr.subscription_count(), 0);
}
}