[ISSUE #10527] Reduce auxiliary component allocation via AtomicLong, string caches, StringBuilder reuse, and dirty flag#10528
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR focuses on reducing allocation/flush overhead in the store layer by introducing cached key builders, adding a “dirty” optimization for timer wheel flushing, and improving queue offset updates with atomic counters.
Changes:
- Add key caching and primitive
intqueueId APIs inBrokerStatsManagerto reduce stats-key allocation. - Optimize
TimerWheel.flush()using adirtyflag and 8-byte aligned comparisons to reduce unnecessary IO/copying. - Replace queue offset maps from
Longvalues toAtomicLongfor safer concurrent increments; add key-building reuse inIndexService.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| store/src/test/java/org/apache/rocketmq/store/stats/BrokerStatsManagerTest.java | Align test constant type (Integer → int) with production API changes. |
| store/src/main/java/org/apache/rocketmq/store/timer/TimerWheel.java | Add dirty gating and faster flush loop to reduce forced writes. |
| store/src/main/java/org/apache/rocketmq/store/stats/BrokerStatsManager.java | Add multiple caches for stats key strings and switch queueId params to primitive int. |
| store/src/main/java/org/apache/rocketmq/store/queue/QueueOffsetOperator.java | Use AtomicLong to avoid lost updates on concurrent offset increments; add snapshot/convert helpers. |
| store/src/main/java/org/apache/rocketmq/store/index/IndexService.java | Reuse a StringBuilder for key construction to reduce temporary allocations. |
| store/src/main/java/org/apache/rocketmq/store/AppendMessageResult.java | Add an additional constructor overload mirroring existing fields. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Review by github-manager-bot
Summary
This PR reduces auxiliary component allocation in the store layer through five optimization strategies: reusable StringBuilder in IndexService, AtomicLong-based offset tracking in QueueOffsetOperator, string caching in BrokerStatsManager, dirty-flag flush gating in TimerWheel, and a new AppendMessageResult constructor overload. Overall direction is sound — reducing per-message allocation pressure in hot paths.
Findings
-
[Critical]
IndexService.java:52— Thread-safety violation withreusableKeyBuilder.IndexServiceis shared across threads (commitlog dispatch, query, etc.), andbuildKey()is called from bothqueryOffset()(read lock) andbuildIndex()(write lock). A shared mutableStringBuilderwithout synchronization will produce corrupted keys under concurrent access. The JVM may even reordersetLength(0)andappend()calls across threads. This is a data corruption risk — keys will be silently wrong, causing index lookups to miss or return incorrect offsets.- Fix: Either use a
ThreadLocal<StringBuilder>, pass the builder as a parameter from callers that already hold appropriate locks, or revert to the original string concatenation (the JIT will optimizetopic + "#" + keyinto aStringBuilderanyway, and the allocation is short-lived).
- Fix: Either use a
-
[Critical]
TimerWheel.java:58,137,151,303,313,326,332—dirtyflag race condition. The patternif (!dirty) return; ... dirty = false;is a non-atomic check-then-act. Between theif (!dirty)guard and the finaldirty = false, another thread callingsetDirty(true)can have its update lost — the flush proceeds but then resets the flag, losing the knowledge that new data was written. Over time this can cause the timer wheel to skip flushes, leading to data loss on crash.- Fix: Use
AtomicBooleanwithcompareAndSet(true, false)to atomically claim the dirty state, or synchronize the flush method. Alternatively, accept that some flushes may be redundant (safe) by checkingdirtyagain after the flush.
- Fix: Use
-
[Warning]
BrokerStatsManager.java:496— Unbounded cache growth. ThestatsKeyByGroupCacheand similarConcurrentHashMap<String, String[]>caches grow without eviction. In deployments with thousands of topics × groups, this can accumulate significant heap. Consider adding a size cap or using a bounded cache (e.g., Caffeine withmaximumSize). -
[Info]
QueueOffsetOperator.java— The change fromLongtoAtomicLongfor offset values is a good correctness improvement (atomic increment vs. non-atomic read-modify-write). ThegetTopicQueueTable()snapshot copy is appropriate for iteration safety.
Suggestions
- The
IndexServicethread-safety issue is a blocker — please fix before merge. - The
TimerWheeldirty flag issue should also be addressed; considerAtomicBoolean.getAndSet(false)as a minimal fix. - Consider adding a JMH benchmark for the hot paths to quantify the allocation reduction.
Automated review by github-manager-bot
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #10528 +/- ##
=============================================
- Coverage 48.28% 48.19% -0.10%
+ Complexity 13445 13414 -31
=============================================
Files 1378 1378
Lines 100845 100883 +38
Branches 13044 13051 +7
=============================================
- Hits 48695 48617 -78
- Misses 46201 46294 +93
- Partials 5949 5972 +23 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
958a954 to
f2503ca
Compare
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Review by github-manager-bot (Re-review after new commits)
Summary
Re-review after new commit f2503ca0 (2026-06-19). The update adds a new AppendMessageResult constructor overload and refines QueueOffsetOperator to use AtomicLong for offset tracking. However, the two critical issues from the previous review remain unaddressed.
Unresolved Critical Issues
-
[Critical]
IndexService.java—reusableKeyBuilderthread-safety violation persists. The sharedStringBuilderinstance field is still used inbuildKey()without synchronization.IndexServiceis accessed from multiple threads (dispatch, query). Under concurrent access,setLength(0)+append()interleaving will produce corrupted keys, causing silent index lookup failures.- Fix: Use
ThreadLocal<StringBuilder>, or revert totopic + "#" + key(JIT optimizes this).
- Fix: Use
-
[Critical]
TimerWheel.java—dirtyflag race condition persists. Theif (!dirty) return; ... dirty = false;pattern is a non-atomic check-then-act. Between the guard and the reset, another thread can setdirty = truewhich then gets silently cleared, causing missed flushes and potential data loss on crash.- Fix: Use
AtomicBoolean.compareAndSet(true, false)to atomically claim the dirty state.
- Fix: Use
New Changes
-
[Info]
AppendMessageResult.java— New 8-arg constructor (dropsqueueOffsetandlogicsOffset). Callers that don't need these fields can use this lighter constructor. No issues. -
[Info]
QueueOffsetOperator.java—AtomicLong-based offset tracking is a good correctness improvement. ThegetTopicQueueTable()snapshot copy is appropriate for iteration safety. ThesetTopicQueueTableconversion fromMap<String, Long>toMap<String, AtomicLong>is a one-time cost during load, acceptable. -
[Info]
BrokerStatsManagerTest.java—Integer QUEUE_ID→int QUEUE_IDavoids autoboxing. Minor but correct.
Action Required
The two critical thread-safety issues must be resolved before merge. Please fix IndexService and TimerWheel as suggested.
Automated re-review by github-manager-bot
Server Benchmark Results (2026-06-22)Environment: 8C30G broker, Dragonwell JDK 21, 1KB msg, 128 queues, sync mode, 256 threads, 20s warmup + 30s collect Producer-only (steady state, lines 3-5)
Auxiliary component optimizations (QueueOffsetOperator AtomicLong, IndexService StringBuilder reuse, TimerWheel dirty flag) reduce boxing and string allocation on the put message path. |
…Long, string caches, StringBuilder reuse, and dirty flag - QueueOffsetOperator: ConcurrentMap<String,Long> -> ConcurrentMap<String,AtomicLong> - BrokerStatsManager: cache buildStatsKey/topicQueueKey/consumerOffset strings, Integer->int params - IndexService: reusable StringBuilder via ThreadLocal - TimerWheel: volatile dirty flag to skip unnecessary flush - AppendMessageResult: constructor with pre-computed fields - Fix BrokerStatsManagerTest: QUEUE_ID Integer->int
f2503ca to
bee9c4a
Compare
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Review by github-manager-bot
Summary
Reduces auxiliary component allocation via AtomicLong counters, cached string keys, StringBuilder reuse, and dirty flag patterns.
Issues Found
[Critical] IndexService.reusableKeyBuilder is not thread-safe
private final StringBuilder reusableKeyBuilder = new StringBuilder(128);
private String buildKey(final String topic, final String key) {
reusableKeyBuilder.setLength(0);
return reusableKeyBuilder.append(topic).append(\"#\").append(key).toString();
}IndexService is a shared service used by multiple threads (dispatch threads, query threads). buildKey() is called from queryOffset() and buildIndex() which can execute concurrently. StringBuilder is not thread-safe — concurrent setLength(0) + append() calls will produce corrupted keys or throw StringIndexOutOfBoundsException.
Suggested fix: Use ThreadLocal<StringBuilder> instead:
private static final ThreadLocal<StringBuilder> KEY_BUILDER = ThreadLocal.withInitial(() -> new StringBuilder(128));
private String buildKey(final String topic, final String key) {
StringBuilder sb = KEY_BUILDER.get();
sb.setLength(0);
return sb.append(topic).append(\"#\").append(key).toString();
}Other Findings
- [Info]
QueueOffsetOperatorAtomicLong migration — Clean. Eliminates boxing/unboxing on every offset update. TheConcurrentHashMap<String, AtomicLong>pattern is correct. - [Info]
AppendMessageResultnew constructor — Good, avoids unused StringBuilder parameter. - [Info]
ConsumeQueue.topicQueueKeycaching — Good optimization, avoids repeated string concatenation.
Automated review by github-manager-bot
Which Issue(s) This PR Fixes
Fixes #10527
Brief Description
Reduce allocation in auxiliary store components through four independent optimizations:
QueueOffsetOperator— ReplaceConcurrentMap<String, Long>withConcurrentMap<String, AtomicLong>. EliminatesLongboxing on every queue offset update.BrokerStatsManager— ChangeIntegerparameters tointinincQueue*methods to eliminate autoboxing. (String caching was removed after microbenchmark showed ConcurrentHashMap lookup is slower than JIT-optimized String concat.)IndexService— ReuseStringBuildervia member field instead of allocating per-call.TimerWheel— Add volatiledirtyflag to skip flush when no changes occurred since last flush.AppendMessageResult— Add constructor with pre-computed fields to avoid redundant allocation.How Did You Test This Change?
BrokerStatsManagerTesttests pass.storemodule compiles cleanly on JDK 21.