[Bug] Timer enqueue retry re-enqueues already-succeeded requests, delivering scheduled messages multiple times
Before Creating the Bug Report
- I found a bug, not just a question.
- I searched open GitHub Issues and pull requests and found no duplicate.
- I confirmed that this bug belongs to Apache RocketMQ.
Runtime platform environment
All platforms; reproduced with a deterministic unit test on the current develop branch.
RocketMQ version
develop at ff8f6f74c
JDK Version
All
Describe the Bug
When a batch of timer requests is enqueued, TimerEnqueuePutService#fetchAndPutTimerRequest retries the whole batch after any single request fails. The retry loop re-invokes putMessageToTimerWheel for every request in trs without checking TimerRequest#isSucc, so requests whose doEnqueue already succeeded in an earlier round are enqueued again — each retry round appends another TimerLog unit for the same message into the same timer-wheel slot.
On dequeue every unit is an independent MAGIC_DEFAULT record, so the same scheduled message is converted and delivered to the real topic multiple times (and the slot num counter is inflated, skewing getAllNum/isReject flow-control decisions).
A partial batch failure is realistic: TimerLog#append returns -1 when a new mapped file cannot be allocated in time (IO pressure at file rollover), and any unexpected throwable while timerSkipUnknownError=false fails the current request only — the other requests of the batch have already succeeded.
Steps to Reproduce
- Put two timer requests A and B into
enqueuePutQueue(same or different delay times). - Make
doEnqueuefail exactly once — for A's first attempt only (equivalent to a transientTimerLog#appendfailure). - Drive
TimerEnqueuePutService#fetchAndPutTimerRequestonce. - Observe that A's
doEnqueueruns twice: the successful first attempt is re-processed by the retry round, appending a secondTimerLogunit for the same message.
What Did You Expect to See?
The retry round must only re-process the requests that did not succeed; already-succeeded requests are never re-enqueued, so the message is delivered exactly once.
What Did You See Instead?
Round 2 re-enqueues A and the scheduled message is delivered twice (once per appended TimerLog unit).
Additional Context
Proposed fix: filter the batch on TimerRequest#isSucc() before each retry round (requests routed to the dequeue path are released with succ=true before the shared latch completes, so they are never re-put either). A regression test in TimerMessageStoreTest counts doEnqueue invocations per physical offset and fails on unmodified develop.
Corresponding PR
- PR apache/rocketmq#11048
Source: apache/rocketmq