Before Creating the Bug Report
Runtime platform environment
Linux, JDK 21, develop (ff8f6f7)
RocketMQ version
5.x develop
Describe the Bug
Since the introduction of BatchUnregistrationService, the decision that a broker should be unregistered (heartbeat expiry in scanNotActiveBroker, or a channel-close event) and its execution are separated by queue latency and blocking closeChannel I/O. setupUnRegisterRequest matches only by clusterName + brokerAddr in brokerAddrTable, and unRegisterBroker then removes unconditionally:
brokerLiveTable.remove(brokerAddrInfo) (no freshness re-check),
- the addr mapping via
removeIf(item -> item.getValue().equals(brokerAddr)) (matching by address only, ignoring the brokerId carried in the request — while registerBroker carefully distinguishes ids for the same address),
- and all topic QueueDatas for the broker.
If the broker is actually alive and re-registers between the expiry decision and the queued execution (e.g. after a namesrv GC pause / scan backlog that expired live brokers), the brand-new registration — new BrokerLiveInfo, address mapping and topic QueueDatas — is deleted. The live broker then vanishes from all routes until its next periodic re-registration (registerNameServerPeriod, ~30s), producing cluster-wide TOPIC_NOT_EXIST / no-route windows. The same defect lets a queued unregister for a slave at addr A remove a master entry re-registered at the same addr A, because removal matches on address only.
Note the onChannelDestroy(Channel) overload already guards by channel identity (entry.getValue().getChannel() == channel) at decision time — but the BrokerAddrInfo overload (used by the expiry scan) has no such check, and even the channel guard cannot cover the decision-to-execution gap.
Steps to Reproduce
- Register broker (channel1).
- Fire the destroy event for it:
onChannelDestroy(new BrokerAddrInfo(cluster, addr)) — the unregister request is queued asynchronously.
- Re-register the same broker with channel2 (fresh registration).
- Let the queued request execute: the fresh registration is removed;
pickupTopicRouteData returns null for its topics.
Expected Behavior
A destroy-derived unregister must not remove a registration that is newer than the event it was derived from: if the current live entry for the address belongs to a different (newer) channel, the queued request is stale and must be skipped. Explicitly initiated unregisters (the broker's own UNREGISTER_BROKER request) remain unconditional.
Corresponding PR
Before Creating the Bug Report
Runtime platform environment
Linux, JDK 21, develop (ff8f6f7)
RocketMQ version
5.x develop
Describe the Bug
Since the introduction of
BatchUnregistrationService, the decision that a broker should be unregistered (heartbeat expiry inscanNotActiveBroker, or a channel-close event) and its execution are separated by queue latency and blockingcloseChannelI/O.setupUnRegisterRequestmatches only byclusterName + brokerAddrinbrokerAddrTable, andunRegisterBrokerthen removes unconditionally:brokerLiveTable.remove(brokerAddrInfo)(no freshness re-check),removeIf(item -> item.getValue().equals(brokerAddr))(matching by address only, ignoring thebrokerIdcarried in the request — whileregisterBrokercarefully distinguishes ids for the same address),If the broker is actually alive and re-registers between the expiry decision and the queued execution (e.g. after a namesrv GC pause / scan backlog that expired live brokers), the brand-new registration — new
BrokerLiveInfo, address mapping and topic QueueDatas — is deleted. The live broker then vanishes from all routes until its next periodic re-registration (registerNameServerPeriod, ~30s), producing cluster-wide TOPIC_NOT_EXIST / no-route windows. The same defect lets a queued unregister for a slave at addr A remove a master entry re-registered at the same addr A, because removal matches on address only.Note the
onChannelDestroy(Channel)overload already guards by channel identity (entry.getValue().getChannel() == channel) at decision time — but theBrokerAddrInfooverload (used by the expiry scan) has no such check, and even the channel guard cannot cover the decision-to-execution gap.Steps to Reproduce
onChannelDestroy(new BrokerAddrInfo(cluster, addr))— the unregister request is queued asynchronously.pickupTopicRouteDatareturns null for its topics.Expected Behavior
A destroy-derived unregister must not remove a registration that is newer than the event it was derived from: if the current live entry for the address belongs to a different (newer) channel, the queued request is stale and must be skipped. Explicitly initiated unregisters (the broker's own UNREGISTER_BROKER request) remain unconditional.
Corresponding PR
Closes #11043in the PR description; contains the regression test that fails before the fix and passes after it).