Skip to content

[BUG] Dropped WS subscriptions remain registered until a matching broadcast #206

Description

@thaodt

Describe the bug
ClientManager::subscribe inserts each subscription into ClientManager.subs. Dropping the returned stream closes its receiver, but does not remove the corresponding map entry.

Dead subscriptions are detected only inside ClientManager::broadcast, after the broadcast topic matches the subscription pattern and try_send returns Closed. Therefore, a dropped subscription for a quiet topic remains registered indefinitely unless a future broadcast matches that pattern.

This also affects subscriptions dropped during unsubscribe or connection teardown.

To Reproduce
The behavior can be reproduced with a focused unit test in aimdb-websocket-connector/src/server/client_manager.rs:

#[tokio::test]
async fn dropped_quiet_subscription_is_pruned_without_matching_traffic() {
    let mgr = ClientManager::new(256);
    let (_id, stream) = mgr.subscribe("quiet.topic");

    assert_eq!(mgr.subscription_count(), 1);
    drop(stream);

    // A non-matching broadcast does not inspect the closed sender.
    mgr.broadcast("other.topic", b"value").await;

    assert_eq!(mgr.subscription_count(), 0);
}

The final assertion currently fails with subscription_count() == 1. Broadcasting to quiet.topic afterward removes the entry, confirming that cleanup depends on matching traffic.

Expected behavior
Dropping a subscription stream should remove its entry from ClientManager.subs w/o requiring any future broadcast.

Unsubscribe and connection tear-down should therefore release all associated subscription state promptly, including subscriptions for topics that may never receive another update.

Environment

  • OS: N/A - source-level and unit-test reproduction
  • Rust Version: workspace toolchain
  • AimDB Version: feat/retire-aimdb-ws-protocol at 81696c7659f44cbc6c988a112de8eaba76b9a8b7
  • Also present on the current main branch, this is not a regression introduced by PR Feat/retire aimdb ws protocol #201

Additional context
Stale entries retain their pattern, sender and counters. Every later broadcast still scans these entries and evaluates topic_matches, so repeated subscribe/disconnect cycles on quiet or unique topics can grow both memory use and fan-out work over the server lifetime. The per-connection subscription limit does not bound this accumulated global state.

A possible fix is to return a stream with an RAII drop guard that removes its subscription ID from the shared map, or provide an explicit removal path used by unsubscribe and session teardown.

Please add a regression proving that dropping a stream removes the entry without requiring matching traffic.

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions