Skip to content

[Bug] Timer enqueue retry re-enqueues already-succeeded requests, delivering scheduled messages multiple times #11047

Description

@unbridled-41

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

  1. Put two timer requests A and B into enqueuePutQueue (same or different delay times).
  2. Make doEnqueue fail exactly once — for A's first attempt only (equivalent to a transient TimerLog#append failure).
  3. Drive TimerEnqueuePutService#fetchAndPutTimerRequest once.
  4. Observe that A's doEnqueue runs twice: the successful first attempt is re-processed by the retry round, appending a second TimerLog unit 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions