[ISSUE #10906] Prevent duplicate MQClientInstance creation - #10907
Conversation
Serialize construction per client ID and roll back resources when construction or start fails. Use identity-aware removal so stale shutdown cannot remove a replacement instance. Signed-off-by: qianye <wuxingcan.wxc@alibaba-inc.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #10907 +/- ##
=============================================
- Coverage 48.44% 48.33% -0.11%
+ Complexity 13576 13550 -26
=============================================
Files 1380 1380
Lines 101165 101195 +30
Branches 13127 13130 +3
=============================================
- Hits 49009 48916 -93
- Misses 46196 46293 +97
- Partials 5960 5986 +26 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
RockteMQ-AI
left a comment
There was a problem hiding this comment.
- Identity-aware removal (
ConcurrentMap.remove(key, value)) inshutdown()andcleanupAfterStartFailure()correctly prevents ABA issues cleanupAfterConstructionFailureproperly handles the case whereclientAPIis null (failure beforeMQClientAPIImplcreation)runCleanupcorrectly suppresses secondary exceptions without duplicating the primary cause
Performance ✅
- No global lock added — independent client IDs in different
ConcurrentHashMapbins can still construct concurrently - The bin lock is held during construction, but this is acceptable since construction is infrequent relative to lookups (the fast-path
get()is lock-free)
Tests ✅
- 433 lines of comprehensive tests covering concurrent creation, resource cleanup on failure, identity-aware removal, and GC behavior
- Tests verify the exact bug scenario (8 concurrent callers, only 1 instance created, no leaked threads)
Compatibility ✅
- Existing API preserved — the new
removeClientFactory(String, MQClientInstance)is additive - No behavioral change for single-threaded callers
Minor Note
The factoryIndexGenerator.getAndIncrement() inside the lambda is only consumed on actual creation, which is correct. If a construction fails and computeIfAbsent is retried, the next call will get a new index — this is fine since failed instances are discarded.
LGTM. Well-designed fix with thorough cleanup and excellent test coverage.
Automated review by github-manager-bot
Clarify computeIfAbsent recursion constraints, failed-start cleanup semantics, identity-aware removal, and test implementation assumptions. Signed-off-by: qianye <wuxingcan.wxc@alibaba-inc.com>
3716dcc to
5b9e638
Compare
|
CI follow-up The original JDK 8 integration failure was unrelated to the MQClientManager production change, but it exposed two real timing assumptions in existing integration tests. Commit
A later macOS/JDK 8 Build run exposed a separate flaky Validation on the final head
Local Oracle JDK 8 validation also passed |
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Summary
This PR modifies 6 file(s) with 961 lines of changes.
Review Notes
- Files changed: client/src/main/java/org/apache/rocketmq/client/impl/MQClientManager.java,client/src/main/java/org/apache/rocketmq/client/impl/factory/MQClientInstance.java,client/src/test/java/org/apache/rocketmq/client/impl/MQClientManagerTest.java,common/src/test/java/org/apache/rocketmq/common/ServiceThreadTest.java,test/src/test/java/org/apache/rocketmq/test/client/consumer/pop/BatchAckIT.java ... and 1 more
- Diff size: 961 lines
- CLA status: unknown
- ✅ Tests included
Observations
- Potential null safety: 3 chained .get() calls without null checks
Verdict
Code changes look reasonable. No critical issues detected in the structural review.
Automated review by github-manager-bot
Which Issue(s) This PR Fixes
Brief Description
Concurrent first access to
MQClientManagerpreviously usedget -> construct -> putIfAbsent. Callers racing on one client ID could therefore construct multipleMQClientInstancecandidates. Every candidate startsMQClientFactoryScheduledThreadduringConsumerStatsManagerconstruction and registers 30 periodicStatsItemSettasks, so discarded candidates leaked live scheduler threads and tasks.The unmodified-code reproduction used 8 synchronized callers and observed:
The losing outer instances were garbage-collected while the scheduler threads and periodic tasks remained live, confirming the resource reference chain independently of
MQClientInstancereachability.This change:
ConcurrentHashMap.computeIfAbsentso one client ID has at most one successful construction and all callers receive the same instance;ClientConfigbefore entering the mapping function, keeping overridable configuration code outside the map's per-bin reservation;start()fails, removes the failed mapping, and preserves the existingSTART_FAILEDstate;Design trade-offs
start()would be a much larger lifecycle and compatibility change, and by itself would not prevent duplicate candidates.computeIfAbsentprovides the required atomic publication and exception behavior with the existing Java 8ConcurrentHashMap: an exception installs no mapping, and a later call may retry.The fast-path
getand existing public removal method remain intact. Normal repeatedstart()/shutdown()behavior and theSTART_FAILEDretry rejection on the same object are preserved.Resource audit
The constructor/start path was audited for thread, scheduled-task, and retained-resource creation:
ConsumerStatsManager: fiveStatsItemSetobjects with six periodic tasks each; zero-delay sampling startsMQClientFactoryScheduledThreadduring construction.MQClientAPIImpl -> NettyRemotingClient: timer, event-loop/selector, public and scan executors; most threads are lazy, but the remoting client is shut down on partial construction/start failure.PullMessageService: service thread plus its scheduled executor; both are stopped on start failure.RebalanceService: service thread stopped on start failure.DefaultMQProducer: registration/executors and request-future/detector lifecycle are shut down when started.MQClientInstance.startScheduledTask(): all client periodic tasks use the scheduler that is shut down on failure.How Did You Test This Change?
All commands used the repository-configured Maven source/target 1.8; no compiler target was changed.
Full client reactor regression on Amazon Corretto 11.0.23:
mvn -pl client -am -DskipITs -Dspotbugs.skip=true testResults:
rocketmq-common: 243 tests, 0 failures/errorsrocketmq-remoting: 174 tests, 0 failures/errorsrocketmq-client: 585 tests, 0 failures/errors, 1 skippedFocused regression plus SpotBugs/checkstyle on Amazon Corretto 11.0.23:
mvn -pl client -am -DskipITs \ -Dtest=MQClientManagerTest \ -Dsurefire.failIfNoSpecifiedTests=false testResults: 7 tests passed; SpotBugs reported 0 findings and checkstyle reported 0 violations.
Java 8 compatibility on Oracle JDK 8u291:
mvn -pl client -am -DskipITs -Dspotbugs.skip=true \ -Dtest=MQClientManagerTest \ -Dsurefire.failIfNoSpecifiedTests=false testResults: 7 tests passed.
javap -verbosereports class-file major version 52.The new tests use barriers, bounded futures, Awaitility assertions, executor termination waits, and isolated thread groups rather than fixed sleeps. They cover:
Compatibility / release note
Before this change, an
MQClientInstancethat failed duringstart()remained inMQClientManager, so later lookups for the same client ID returned the sameSTART_FAILEDinstance. After this change, the failed instance is removed by identity after best-effort cleanup, and a latergetOrCreateMQClientInstancecall may create a replacement.Callers that already hold the failed instance still observe
START_FAILEDand are not migrated automatically. If a persistent configuration or environment error is retried without backoff, callers may repeatedly create and clean up replacement instances; retry and backoff policy remains the caller responsibility.