Before Creating the Bug Report
Runtime platform environment
ubuntu 24.04
RocketMQ version
branch: develop version: 5.5.0 Git commit id: a6fb9e2
JDK Version
JDK 8u202
Describe the Bug
After a slave truncates its CommitLog to a smaller offset, Timer-related data structures such as TimerLog and TimerWheel may still retain records that reference CommitLog offsets beyond the truncation point.
These stale references can cause the Timer dequeue pipeline to read an unrelated message and retry it indefinitely.
Assume TimerLog contains a record like:
TimerLog entry -> CommitLog offset P
Originally, offset P contains a valid TIMER_TOPIC message.
After CommitLog is truncated to offset T, where T < P, the TimerLog entry is not removed. When CommitLog data is transferred again, offset P may be overwritten by a completely different message.
The resulting state is:
TimerLog entry -> CommitLog offset P
CommitLog[P] -> non-TIMER_TOPIC message
When Timer dequeue processes this TimerLog entry, it reads the new message at P and treats it as the original Timer wrapper message.
The new message may not contain required Timer properties such as:
PROPERTY_REAL_TOPIC
PROPERTY_REAL_QUEUE_ID
This can cause an exception such as:
java.lang.NumberFormatException: null
at java.lang.Integer.parseInt(Integer.java:542)
at java.lang.Integer.parseInt(Integer.java:615)
at org.apache.rocketmq.store.timer.TimerMessageStore.convertMessage(TimerMessageStore.java)
at org.apache.rocketmq.store.timer.TimerMessageStore.convert(TimerMessageStore.java)
at org.apache.rocketmq.store.timer.TimerMessageStore$TimerDequeuePutMessageService.run(TimerMessageStore.java)
With timerSkipUnknownError=false, TimerDequeuePutMessageService keeps retrying the same TimerRequest.
The TimerLog record is not repeatedly read. Instead, the already-created TimerRequest remains in the internal retry loop and fails continuously.
Impact
A single stale TimerLog reference can prevent the current Timer slot from completing.
As a result:
- Timer dequeue progress cannot advance.
- Subsequent scheduled messages cannot be delivered.
- Retry messages using the Timer mechanism, including POP revive retry delivery, may remain blocked.
- The broker continuously reports the same exception.
Steps to Reproduce
This issue can be reproduced with a simulated MessageStore without constructing the complete HA failure sequence.
-
Create a TimerMessageStore with a temporary TimerLog/TimerWheel directory and a mocked MessageStore.
-
Keep the default configuration:
timerSkipUnknownError=false
-
Create a valid Timer message and append a TimerLog record that references a simulated CommitLog offset:
Ensure the corresponding TimerWheel slot references this TimerLog record.
-
Configure the mocked MessageStore so that reading (P, S) now returns a valid ordinary message instead of the original TIMER_TOPIC message.
The replacement message should:
- Have a non-
TIMER_TOPIC topic.
- Not contain
PROPERTY_REAL_TOPIC.
- Not contain
PROPERTY_REAL_QUEUE_ID.
This simulates the state after CommitLog truncation and subsequent HA data overwrite.
-
Enable Timer dequeue and advance the Timer clock until the slot containing the simulated stale record is processed.
-
Run TimerDequeueGetMessageService and TimerDequeuePutMessageService in separate threads with a test timeout.
-
Observe that the replacement message is passed to convertMessage() and triggers:
java.lang.NumberFormatException: null
at java.lang.Integer.parseInt(Integer.java)
at org.apache.rocketmq.store.timer.TimerMessageStore.convertMessage(TimerMessageStore.java)
at org.apache.rocketmq.store.timer.TimerMessageStore.convert(TimerMessageStore.java)
at org.apache.rocketmq.store.timer.TimerMessageStore$TimerDequeuePutMessageService.run(TimerMessageStore.java)
-
Verify that:
- The same
TimerRequest is retried repeatedly.
- The request latch is not completed.
- Timer read progress does not advance.
- A valid Timer message scheduled after this record is not delivered.
-
Stop the Timer service explicitly at the end of the test to avoid leaving the retry thread running.
What Did You Expect to See?
The message referenced by TimerLog should be validated before conversion. If it is no longer a valid TIMER_TOPIC message, the stale request should be released and Timer dequeue should continue.
What Did You See Instead?
The ordinary message is treated as a Timer wrapper message. Conversion fails repeatedly, and the current Timer slot cannot complete.
Additional Context
No response
Before Creating the Bug Report
I found a bug, not just asking a question, which should be created in GitHub Discussions.
I have searched the GitHub Issues and GitHub Discussions of this repository and believe that this is not a duplicate.
I have confirmed that this bug belongs to the current repository, not other repositories of RocketMQ.
Runtime platform environment
ubuntu 24.04
RocketMQ version
branch: develop version: 5.5.0 Git commit id: a6fb9e2
JDK Version
JDK 8u202
Describe the Bug
After a slave truncates its CommitLog to a smaller offset, Timer-related data structures such as TimerLog and TimerWheel may still retain records that reference CommitLog offsets beyond the truncation point.
These stale references can cause the Timer dequeue pipeline to read an unrelated message and retry it indefinitely.
Assume TimerLog contains a record like:
Originally, offset
Pcontains a validTIMER_TOPICmessage.After CommitLog is truncated to offset
T, whereT < P, the TimerLog entry is not removed. When CommitLog data is transferred again, offsetPmay be overwritten by a completely different message.The resulting state is:
When Timer dequeue processes this TimerLog entry, it reads the new message at
Pand treats it as the original Timer wrapper message.The new message may not contain required Timer properties such as:
This can cause an exception such as:
With
timerSkipUnknownError=false,TimerDequeuePutMessageServicekeeps retrying the sameTimerRequest.The TimerLog record is not repeatedly read. Instead, the already-created
TimerRequestremains in the internal retry loop and fails continuously.Impact
A single stale TimerLog reference can prevent the current Timer slot from completing.
As a result:
Steps to Reproduce
This issue can be reproduced with a simulated
MessageStorewithout constructing the complete HA failure sequence.Create a
TimerMessageStorewith a temporary TimerLog/TimerWheel directory and a mockedMessageStore.Keep the default configuration:
timerSkipUnknownError=falseCreate a valid Timer message and append a TimerLog record that references a simulated CommitLog offset:
Ensure the corresponding TimerWheel slot references this TimerLog record.
Configure the mocked
MessageStoreso that reading(P, S)now returns a valid ordinary message instead of the originalTIMER_TOPICmessage.The replacement message should:
TIMER_TOPICtopic.PROPERTY_REAL_TOPIC.PROPERTY_REAL_QUEUE_ID.This simulates the state after CommitLog truncation and subsequent HA data overwrite.
Enable Timer dequeue and advance the Timer clock until the slot containing the simulated stale record is processed.
Run
TimerDequeueGetMessageServiceandTimerDequeuePutMessageServicein separate threads with a test timeout.Observe that the replacement message is passed to
convertMessage()and triggers:Verify that:
TimerRequestis retried repeatedly.Stop the Timer service explicitly at the end of the test to avoid leaving the retry thread running.
What Did You Expect to See?
The message referenced by TimerLog should be validated before conversion. If it is no longer a valid
TIMER_TOPICmessage, the stale request should be released and Timer dequeue should continue.What Did You See Instead?
The ordinary message is treated as a Timer wrapper message. Conversion fails repeatedly, and the current Timer slot cannot complete.
Additional Context
No response