RongtongJin commented on code in PR #11046:
URL: https://github.com/apache/rocketmq/pull/11046#discussion_r4227688656


##########
controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/DefaultBrokerHeartbeatManager.java:
##########
@@ -111,53 +112,70 @@ public void onBrokerHeartbeat(String clusterName, String 
brokerName, String brok
         Long timeoutMillis, Channel channel, Integer epoch, Long maxOffset, 
Long confirmOffset,
         Integer electionPriority) {
         BrokerIdentityInfo brokerIdentityInfo = new 
BrokerIdentityInfo(clusterName, brokerName, brokerId);
-        BrokerLiveInfo prev = this.brokerLiveTable.get(brokerIdentityInfo);
         int realEpoch = Optional.ofNullable(epoch).orElse(-1);
         long realBrokerId = Optional.ofNullable(brokerId).orElse(-1L);
         long realMaxOffset = Optional.ofNullable(maxOffset).orElse(-1L);
         long realConfirmOffset = 
Optional.ofNullable(confirmOffset).orElse(-1L);
         long realTimeoutMillis = 
Optional.ofNullable(timeoutMillis).orElse(DEFAULT_BROKER_CHANNEL_EXPIRED_TIME);
         int realElectionPriority = 
Optional.ofNullable(electionPriority).orElse(Integer.MAX_VALUE);
-        if (null == prev) {
-            this.brokerLiveTable.put(brokerIdentityInfo,
-                new BrokerLiveInfo(brokerName,
+        // compute() keeps the create-or-update decision and the channel 
rebinding atomic per
+        // broker identity, so onBrokerChannelClose can never observe a 
half-updated entry: a
+        // heartbeat that rebinds the entry and a close of the previous 
channel serialize on the
+        // same key instead of racing between the match and the removal.
+        this.brokerLiveTable.compute(brokerIdentityInfo, (key, prev) -> {
+            if (prev == null) {
+                log.info("new broker registered, {}, brokerId:{}", 
brokerIdentityInfo, realBrokerId);
+                return new BrokerLiveInfo(brokerName,
                     brokerAddr,
                     realBrokerId,
                     System.currentTimeMillis(),
                     realTimeoutMillis,
                     channel,
                     realEpoch,
                     realMaxOffset,
-                    realElectionPriority));
-            log.info("new broker registered, {}, brokerId:{}", 
brokerIdentityInfo, realBrokerId);
-        } else {
+                    realElectionPriority);
+            }
             prev.setLastUpdateTimestamp(System.currentTimeMillis());

Review Comment:
   [P1] Make expiry revalidation/removal atomic with heartbeat rebinding
   
   The per-key compute protects this update against onBrokerChannelClose, but 
scanNotActiveBroker still reads the timestamp, checks expiry, reads the 
channel, and removes the entry outside that atomic operation. A concrete 
interleaving is:
   
   1. The scanner reads the old lastUpdateTimestamp and pauses.
   2. A heartbeat on newChannel refreshes the timestamp and rebinds this same 
BrokerLiveInfo.
   3. The scanner resumes, decides it is expired using the old timestamp, reads 
newChannel from the updated object, removes the live entry, and closes the 
healthy new connection.
   
   I reproduced this with JDK 8 by pausing getLastUpdateTimestamp after 
capturing its old value, completing the replacement heartbeat, then resuming 
the scan. The assertion that newChannel remains active passes with the base 
manager and fails at this head. The expiry/removal race predates this PR, but 
closing the newly rebound connection is an additional consequence of this 
change.
   
   Please have the scan recheck the current timestamp/timeout and remove the 
entry within computeIfPresent for the same identity, capture the channel of the 
entry actually removed, and close that captured channel outside the operation. 
Add a coordinated regression test for this order; remove(key, value) alone is 
insufficient while heartbeats mutate the same value object.



##########
controller/src/main/java/org/apache/rocketmq/controller/impl/heartbeat/DefaultBrokerHeartbeatManager.java:
##########
@@ -111,53 +112,70 @@ public void onBrokerHeartbeat(String clusterName, String 
brokerName, String brok
         Long timeoutMillis, Channel channel, Integer epoch, Long maxOffset, 
Long confirmOffset,
         Integer electionPriority) {
         BrokerIdentityInfo brokerIdentityInfo = new 
BrokerIdentityInfo(clusterName, brokerName, brokerId);
-        BrokerLiveInfo prev = this.brokerLiveTable.get(brokerIdentityInfo);
         int realEpoch = Optional.ofNullable(epoch).orElse(-1);
         long realBrokerId = Optional.ofNullable(brokerId).orElse(-1L);
         long realMaxOffset = Optional.ofNullable(maxOffset).orElse(-1L);
         long realConfirmOffset = 
Optional.ofNullable(confirmOffset).orElse(-1L);
         long realTimeoutMillis = 
Optional.ofNullable(timeoutMillis).orElse(DEFAULT_BROKER_CHANNEL_EXPIRED_TIME);
         int realElectionPriority = 
Optional.ofNullable(electionPriority).orElse(Integer.MAX_VALUE);
-        if (null == prev) {
-            this.brokerLiveTable.put(brokerIdentityInfo,
-                new BrokerLiveInfo(brokerName,
+        // compute() keeps the create-or-update decision and the channel 
rebinding atomic per
+        // broker identity, so onBrokerChannelClose can never observe a 
half-updated entry: a
+        // heartbeat that rebinds the entry and a close of the previous 
channel serialize on the
+        // same key instead of racing between the match and the removal.
+        this.brokerLiveTable.compute(brokerIdentityInfo, (key, prev) -> {
+            if (prev == null) {
+                log.info("new broker registered, {}, brokerId:{}", 
brokerIdentityInfo, realBrokerId);
+                return new BrokerLiveInfo(brokerName,
                     brokerAddr,
                     realBrokerId,
                     System.currentTimeMillis(),
                     realTimeoutMillis,
                     channel,
                     realEpoch,
                     realMaxOffset,
-                    realElectionPriority));
-            log.info("new broker registered, {}, brokerId:{}", 
brokerIdentityInfo, realBrokerId);
-        } else {
+                    realElectionPriority);
+            }
             prev.setLastUpdateTimestamp(System.currentTimeMillis());
             prev.setHeartbeatTimeoutMillis(realTimeoutMillis);
             prev.setElectionPriority(realElectionPriority);
+            // Rebind the live entry to the current channel, otherwise a close 
event of a stale
+            // channel would evict a broker that is already alive on a new 
channel
+            if (channel != null && channel != prev.getChannel()) {
+                prev.setChannel(channel);

Review Comment:
   [P1] Do not let a late heartbeat restore an already closed channel
   
   This condition also accepts a heartbeat whose channel has already closed. A 
heartbeat request from oldChannel can be dispatched before disconnection but 
finish after a heartbeat on newChannel: controller requests use the shared 
request executor, RequestTask.run does not check channel liveness, and close 
events run on a separate NettyEventExecutor.
   
   The resulting order is: an old heartbeat is queued; oldChannel closes; 
newChannel's heartbeat updates the live entry; the queued old heartbeat runs 
and this assignment changes the channel back to oldChannel; the delayed old 
close callback then removes the broker even though newChannel is still active. 
I reproduced that order with an actual RequestTask and EmbeddedChannels on this 
head: the new connection remains active, but the live-table entry disappears.
   
   This is a remaining instance of the original failure, rather than a separate 
newly introduced bug. compute serializes the map operations but cannot 
establish which connection is current. Please reject heartbeats from closed 
channels inside the per-identity atomic update, covering creation and timestamp 
refresh as well as rebinding, and add a deterministic test where the old 
request executes after the new heartbeat. If two active connections may 
overlap, their ordering also needs an explicit policy; the broker epoch alone 
is not a connection generation.



-- 
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]

Reply via email to