messere1 opened a new issue, #11352: URL: https://github.com/apache/rocketmq/issues/11352
### Before Creating the Bug Report - [x] I found a bug, not just asking a question, which should be created in [GitHub Discussions](https://github.com/apache/rocketmq/discussions). - [x] I have searched the [GitHub Issues](https://github.com/apache/rocketmq/issues) and [GitHub Discussions](https://github.com/apache/rocketmq/discussions) of this repository and believe that this is not a duplicate. - [x] I have confirmed that this bug belongs to the current repository, not other repositories of RocketMQ. ### Runtime platform environment OS: Windows 11 / Linux (broker module, lite topic path) ### RocketMQ version branch: develop Git commit id: 9ea2ccdc2b41445aee5e177e14e501d606f5a1ed (2026-10-08) ### JDK Version Compiler: Temurin JDK 17.0.19 ### Describe the Bug `LiteSubscriptionRegistryImpl.excludeClientByLmqName` handles exclusive-mode takeover: when clientB claims a liteTopic that clientA still holds, clientA is evicted from that liteTopic, and when this was clientA's last liteTopic its `LiteSubscription` is removed from `client2Subscription` immediately (LiteSubscriptionRegistryImpl.java, eviction branch): ```java LiteSubscription liteSubscription = client2Subscription.get(clientGroup.clientId); if (liteSubscription != null) { liteSubscription.removeLmq(lmqName); // remove client if no more liteTopic if (liteSubscription.getLmqSet().isEmpty()) { client2Subscription.remove(clientGroup.clientId); } } ``` However, the evicted client's entry in `clientChannels` is **not** removed. Every other removal flow goes through `removeCompleteSubscription`, which cleans both maps: ```java public void removeCompleteSubscription(String clientId) { clientChannels.remove(clientId); // <-- cleaned here LiteSubscription thisSub = client2Subscription.remove(clientId); ... } ``` The expiry cleaner `cleanupExpiredSubscriptions` only walks `client2Subscription`. Once the eviction path has removed the subscription directly, the orphaned `clientChannels` entry is unreachable for any cleanup: it is never reclaimed until the broker restarts. Impact: 1. Under exclusive-mode client churn (every takeover evicts the previous client, e.g. clients restarting with new clientIds), `clientChannels` grows unboundedly, retaining netty `Channel` references and their associated resources. 2. `notifyUnsubscribeLite` keeps resolving the stale (typically closed) channel for the evicted clientId. ### Steps to Reproduce 1. Create a subscription group with `liteSubExclusive=true` bound to a LITE topic. 2. Start clientA, subscribe (PARTIAL_ADD) to liteTopic L and register its channel (`updateClientChannel`). 3. Start clientB, subscribe to the same liteTopic L (exclusive takeover). clientA is evicted and, since L was its only liteTopic, removed from `client2Subscription`. 4. Observe that `clientChannels` still contains clientA's channel entry; it stays there forever because `cleanupExpiredSubscriptions` only iterates `client2Subscription`. A unit-level reproduction: `LiteSubscriptionRegistryImplTest` — after the takeover in `testAddPartialSubscription_ExclusiveMode`, `registry.clientChannels.get("testClient1")` still returns the channel although `registry.getLiteSubscription("testClient1")` is already null. ### What Did You Expect to See? When the eviction removes the evicted client's last liteTopic and drops it from `client2Subscription`, its channel entry should be reclaimed from `clientChannels` as well (like `removeCompleteSubscription` does), so exclusive-mode takeover churn does not accumulate stale channels. ### What Did You See Instead? The evicted client's `Channel` stays in `clientChannels` permanently; the map grows with every exclusive takeover whose loser had a single liteTopic. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
