Before Creating the Bug Report
Runtime platform environment
Linux x86_64. The reproducer is a deterministic broker unit test and does not require a running RocketMQ cluster.
RocketMQ version
Branch: develop
Commit: 00e45b8a6db23efbe756d0306f10716156cfd4dd
JDK Version
OpenJDK 8
Describe the Bug
BroadcastOffsetManager.queryInitOffset initializes a missing client entry with a non-atomic get / create / put sequence. updateOffset initializes and updates the same entry atomically with ConcurrentHashMap.compute.
If an update wins after the query observes a missing entry but before the query performs its put, the query replaces the updated BroadcastTimedOffsetStore with a new empty instance. The just-committed client offset is lost, and the query falls back to the consumer offset or queue maximum.
Steps to Reproduce
- Create an existing
BroadcastOffsetData with no entry for a client.
- Start
queryInitOffset for that client and block it after it observes the missing entry but before its map insertion.
- Call
updateOffset for the same client and queue with offset 100; this inserts a store and records 100.
- Resume the query insertion.
- Read the query result and the stored client offset.
A latch-controlled ConcurrentHashMap test reproduces this ordering without sleeps or random stress. On the unmodified commit above, it failed 5/5 times with:
expected:<100> but was:<10>
Here 10 is the mocked queue maximum used after the updated store has been overwritten.
What Did You Expect to See?
The winning client store and its offset 100 should remain in clientOffsetStore. A concurrent initialization must not overwrite an offset already recorded by updateOffset.
What Did You See Instead?
The query's later put replaces the store created by updateOffset. Both the initial query and a subsequent query observe the fallback offset 10, so the client update to 100 is lost.
Additional Context
The race is scoped to the per-client initialization in BroadcastOffsetManager; no public protocol change is required. I am willing to submit a focused fix with the deterministic regression test.
Before Creating the Bug Report
Runtime platform environment
Linux x86_64. The reproducer is a deterministic broker unit test and does not require a running RocketMQ cluster.
RocketMQ version
Branch:
developCommit:
00e45b8a6db23efbe756d0306f10716156cfd4ddJDK Version
OpenJDK 8
Describe the Bug
BroadcastOffsetManager.queryInitOffsetinitializes a missing client entry with a non-atomicget/ create /putsequence.updateOffsetinitializes and updates the same entry atomically withConcurrentHashMap.compute.If an update wins after the query observes a missing entry but before the query performs its
put, the query replaces the updatedBroadcastTimedOffsetStorewith a new empty instance. The just-committed client offset is lost, and the query falls back to the consumer offset or queue maximum.Steps to Reproduce
BroadcastOffsetDatawith no entry for a client.queryInitOffsetfor that client and block it after it observes the missing entry but before its map insertion.updateOffsetfor the same client and queue with offset100; this inserts a store and records100.A latch-controlled
ConcurrentHashMaptest reproduces this ordering without sleeps or random stress. On the unmodified commit above, it failed 5/5 times with:Here
10is the mocked queue maximum used after the updated store has been overwritten.What Did You Expect to See?
The winning client store and its offset
100should remain inclientOffsetStore. A concurrent initialization must not overwrite an offset already recorded byupdateOffset.What Did You See Instead?
The query's later
putreplaces the store created byupdateOffset. Both the initial query and a subsequent query observe the fallback offset10, so the client update to100is lost.Additional Context
The race is scoped to the per-client initialization in
BroadcastOffsetManager; no public protocol change is required. I am willing to submit a focused fix with the deterministic regression test.