qianye1001 opened a new issue, #10906:
URL: https://github.com/apache/rocketmq/issues/10906

   ### Before Creating the Bug Report
   
   - [x] This is a bug rather than a support question.
   - [x] I searched existing GitHub issues and discussions and did not find a 
duplicate.
   - [x] The bug is in this repository.
   
   ### Runtime platform environment
   
   - macOS 15.1.1 (24B91), arm64
   - Reproduced by a deterministic JUnit test; no broker is required.
   
   ### RocketMQ version
   
   - Branch: `develop`
   - Commit: `e3458616d207ee636b1762f0f8dcf788a590d59d`
   - Version in the reactor: 5.5.0
   - This commit was the tip of `apache/develop` when the reproduction was run.
   
   ### JDK Version
   
   - Amazon Corretto 11.0.23
   - Also verified with Oracle JDK 8u291.
   
   ### Describe the Bug
   
   The first concurrent calls to `MQClientManager.getOrCreateMQClientInstance` 
for the same client ID can construct multiple `MQClientInstance` candidates 
even though only one is stored and returned.
   
   The current sequence is:
   
   ```text
   factoryTable.get(clientId)
   new MQClientInstance(...)
   factoryTable.putIfAbsent(clientId, candidate)
   ```
   
   All callers that observe the initial miss construct independently. The 
losing candidates are discarded without cleanup.
   
   This is a resource leak, not only an allocation race. During construction, 
every candidate creates a `ConsumerStatsManager`. It creates five 
`StatsItemSet` objects, and every `StatsItemSet` registers six fixed-rate tasks 
on the instance's single-thread `MQClientFactoryScheduledThread` executor. The 
zero-delay sampling tasks start that non-daemon thread before 
`MQClientInstance.start()`.
   
   The surviving reference chain for a losing candidate is:
   
   ```text
   live Thread
     -> ThreadPoolExecutor.Worker
     -> ScheduledThreadPoolExecutor
     -> DelayedWorkQueue
     -> periodic ScheduledFutureTask
     -> StatsItemSet task
   ```
   
   The tasks do not need to retain the outer `MQClientInstance`. Therefore the 
losing `MQClientInstance` itself can be garbage-collected while its scheduler 
thread and 30 periodic tasks remain live indefinitely.
   
   There is a related failure-path leak. Construction schedules all 30 stats 
tasks before creating the optional concurrent-heartbeat pool. Enabling 
concurrent heartbeat with pool size 0 makes `Executors.newFixedThreadPool(0, 
...)` throw `IllegalArgumentException`; no map entry is installed, but the 
already-started stats scheduler remains live.
   
   ### Steps to Reproduce
   
   1. Check out `e3458616d207ee636b1762f0f8dcf788a590d59d`.
   2. Start 8 callers for one client ID.
   3. Use a barrier in a test `ClientConfig.cloneClientConfig()` so every 
caller has completed the initial map miss before any candidate can finish 
construction.
   4. Capture each constructed candidate with a test-only observer.
   5. Record returned identities, the candidate schedulers and their queue 
sizes.
   6. Drop all strong references to losing `MQClientInstance` objects, retain 
only their schedulers, and use `WeakReference` plus bounded GC polling.
   7. Use bounded polling for thread counts and `awaitTermination`; no fixed 
sleep is required.
   
   The test was run with:
   
   ```bash
   mvn -pl client -am -DskipITs -Dspotbugs.skip=true \
     -Dtest=MQClientManagerLeakReproductionTest \
     -Dsurefire.failIfNoSpecifiedTests=false test
   ```
   
   Repeatable evidence from the unmodified code:
   
   ```text
   candidates=8
   returnedIdentity=1
   liveSchedulerThreads=8
   periodicTasks=240
   losersGc=7/7
   ```
   
   After explicitly calling `shutdownNow()` on all captured schedulers and 
waiting for termination, the scheduler-thread count returns to the baseline.
   
   ### What Did You Expect to See?
   
   For one client ID, concurrent first access should perform at most one 
successful `MQClientInstance` construction and publish that instance to all 
callers. Failed construction or failed initialization should not leave a map 
entry, live executor thread, periodic task, or partially started client 
resource.
   
   Creation for different client IDs should not be guarded by a global lock.
   
   ### What Did You See Instead?
   
   Eight concurrent callers constructed eight candidates. They all returned one 
winner, but all eight scheduler threads and all 240 periodic tasks remained 
live. Seven losing outer instances were garbage-collected, proving that 
outer-object reachability and background-resource reachability are different.
   
   A late constructor failure also left the scheduler thread and its 30 
periodic tasks live even though `factoryTable` had no entry for the client ID.
   
   ### Additional Context
   
   A constructor-path audit found these resource-bearing components:
   
   - `MQClientInstance.scheduledExecutorService`: its thread starts during 
`ConsumerStatsManager` construction because stats tasks have zero initial delay.
   - `ConsumerStatsManager`: five `StatsItemSet` instances, six periodic tasks 
each.
   - `MQClientAPIImpl -> NettyRemotingClient`: allocates a timer, event-loop 
group, selector, and executors; most threads start lazily, but partial 
construction still needs rollback.
   - `PullMessageService`: allocates a scheduled executor and retains the 
client instance; it starts threads/tasks only on start or submission.
   - `RebalanceService`: retains the client instance; its service thread starts 
in `MQClientInstance.start()`.
   - Inner `DefaultMQProducer` and the optional concurrent-heartbeat executor: 
worker threads are lazy, but both require cleanup if initialization fails.
   - `MQClientInstance.startScheduledTask()`, pull, rebalance, remoting, and 
the inner producer introduce additional resources during `start()`; a mid-start 
exception currently leaves the factory in `START_FAILED` without rollback.
   
   A minimal fix should serialize construction per client ID, preserve parallel 
construction for independent IDs, remove a failed mapping, roll back resources 
on constructor/start failure, and use identity-aware removal so an old instance 
cannot delete a newer replacement.
   


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