[ISSUE #10613] Fix async request-reply RequestCallback firing multiple times#10614
Open
wang-jiahua wants to merge 1 commit into
Open
[ISSUE #10613] Fix async request-reply RequestCallback firing multiple times#10614wang-jiahua wants to merge 1 commit into
wang-jiahua wants to merge 1 commit into
Conversation
…ultiple times - DefaultMQProducerImpl#request(Message, RequestCallback, long): drop the executeRequestCallback() call in the async send onSuccess so a send success no longer delivers a premature onSuccess(null); align with the other async request overloads which only set sendRequestOk here. - RequestResponseFuture: add an AtomicBoolean executeCallbackOnlyOnce guard so the callback fires at most once even if the reply and timeout paths race. - RequestFutureHolder#scanExpiredRequest: use ConcurrentHashMap.remove(key) to atomically claim ownership instead of iterator.remove(); also fix the log placeholder concatenation. - ClientRemotingProcessor#processReplyMessage: use atomic remove(correlationId) and route the reply through executeRequestCallback so the single-shot guard covers the reply-success path too. - Add RequestResponseFutureTest cases for success-then-timeout and concurrent single-callback semantics.
RockteMQ-AI
approved these changes
Jul 13, 2026
RockteMQ-AI
left a comment
Contributor
There was a problem hiding this comment.
Review by github-manager-bot
Summary
Fixes async request-reply RequestCallback firing multiple times due to (1) premature onSuccess(null) in the send callback and (2) a race between the reply-arrival and timeout-scan paths. Well-structured fix with defense-in-depth.
Findings
- [Info]
ClientRemotingProcessor.java:275— Atomicremove(correlationId)correctly eliminates the TOCTOU race between reply arrival and timeout scan. Clean fix. - [Info]
DefaultMQProducerImpl.java:1665— RemovingexecuteRequestCallback()from the sendonSuccessis correct and consistent with the other async overloads (request(msg, selector, arg, callback, timeout),request(msg, mq, callback, timeout)). The callback now fires only on reply, timeout, or send failure. - [Info]
RequestFutureHolder.java:57-62— Good catch on the malformed log placeholder. The original"CorrelationId={}" + rep.getCorrelationId()would produceCorrelationId={}abc123instead ofCorrelationId=abc123. Theremove(key)atomic claim pattern is correct. - [Info]
RequestResponseFuture.java:44-53—AtomicBooleanCAS guard is a solid defense-in-depth measure. Even if both paths somehow bypass the atomicremove(), the callback still fires exactly once. - [Info]
RequestResponseFutureTest.java— Tests cover both sequential double-call (success → timeout) and 16-thread concurrent contention. Good coverage of the fix.
Suggestions
- No blocking concerns. The fix is minimal, focused, and well-tested. The end-to-end verification table in the PR description (2000 requests,
doubleCallback: 2000 → 0) is convincing.
Automated review by github-manager-bot
RongtongJin
approved these changes
Jul 14, 2026
Contributor
|
LGTM~ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which Issue(s) This PR Fixes
Fixes #10613
Brief Description
The async request-reply API
DefaultMQProducer#request(Message, RequestCallback, long)could invoke the userRequestCallbackmore than once for a single request, due to two combined problems:onSuccess(null)— the async sendonSuccesscalledexecuteRequestCallback()right after the request message was sent, so the caller receivedonSuccess(null)before any reply arrived. The other async overloads (request(msg, selector, arg, callback, timeout),request(msg, mq, callback, timeout)) do not do this.ClientRemotingProcessor#processReplyMessage(non-atomicget+remove) andRequestFutureHolder#scanExpiredRequest(iterator.remove) could both take ownership of the same request, andRequestResponseFuture#executeRequestCallbackhad no single-shot guard.How Did You Fix It
DefaultMQProducerImpl#request(Message, RequestCallback, long): dropexecuteRequestCallback()from the async sendonSuccess; only setsendRequestOk, consistent with the other async overloads. The user callback now fires only on reply arrival, timeout, or send failure.RequestResponseFuture: add anAtomicBoolean executeCallbackOnlyOnceguard so the callback runs at most once regardless of which path wins.RequestFutureHolder#scanExpiredRequest: useConcurrentHashMap.remove(key)to atomically claim ownership instead ofiterator.remove(); also fix the malformed log placeholder.ClientRemotingProcessor#processReplyMessage: use atomicremove(correlationId)and route the reply throughexecuteRequestCallbackso the single-shot guard also covers the reply-success path.How to Verify
Unit tests (
RequestResponseFutureTest): success-then-timeout fires exactly once; 16-thread concurrent invocation fires exactly once. Fullclientmodule test suite passes.End-to-end on a 4-node cluster (2000 async requests, 16 threads,
timeout=1000ms, consumer reply delay=200ms), identical reply flow (consumed=1312, replySent=1280):After the fix each request delivers exactly one callback (286
onSuccess(reply)+ 1714onException(timeout)= 2000).