Skip to content

[Bug] Pop long-polling: retry topic message with null properties throws NPE in the reput thread and stalls broker-wide dispatch #10990

Description

@unbridled-41

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 this is not a duplicate.
  • I have confirmed that this bug belongs to the current repository, not other repositories of RocketMQ.

Runtime platform environment

  • OS: Linux
  • Component: Broker (PopLongPollingService + DefaultMessageStore.ReputMessageService)

RocketMQ version

  • branch: develop
  • Git commit id: e348efa

JDK Version

JDK 8

Describe the Bug

PopLongPollingService#notifyMessageArrivingFromRetry dereferences the dispatch request's property map without a null check:

private void notifyMessageArrivingFromRetry(String topic, int queueId, Long tagsCode, long msgStoreTime,
    byte[] filterBitMap, Map<String, String> properties) {
    String prefix = MixAll.RETRY_GROUP_TOPIC_PREFIX;
    String originGroup = properties.get(MessageConst.PROPERTY_ORIGIN_GROUP);   // NPE when properties == null

A DispatchRequest legitimately carries a null properties map: MessageDecoder.string2messageProperties returns null for a message stored without properties (e.g. a message written by a non-Java client, or through any path that stores a message on a %RETRY%-prefixed topic without user properties). The store's own code acknowledges this — DefaultMessageStore#notifyMessageArrive4MultiQueue explicitly guards prop == null, and PullRequestHoldService#notifyMessageArriving guards properties != null — but the pop retry branch does not.

Why this is severe: the listener is invoked from DefaultMessageStore.ReputMessageService#doReput before reputFromOffset is advanced, and doReput only catches RocksDBException. The resulting NullPointerException propagates to ServiceThread.run, which logs and loops — then re-reads the same commitlog record and throws again, forever. The broker stops dispatching all messages (consume queues stop advancing, no long-polling wakeups, consumers see a full outage) while flooding the log at ~1000 lines/s. A single poison message is enough; it stays poisoned across restarts because reputFromOffset is recovered from the consume queue state.

Steps to Reproduce

  1. On a broker with pop enabled, store a message with no properties on a topic named %RETRY%<group> (any client able to write that topic; the properties map in the dispatch request is then null).
  2. Watch the reput thread loop on service has exception. NullPointerException at PopLongPollingService.notifyMessageArrivingFromRetry, with reputFromOffset frozen.

In a unit test, calling notifyMessageArrivingWithRetryTopic("%RETRY%g", -1, -1, -1L, 0L, null, null) throws NPE on current develop.

What Did You Expect to See?

The notification is skipped (there is no origin group to wake up), and dispatch continues.

What Did You See Instead?

NPE in the reput thread → the same message is re-dispatched forever → broker-wide dispatch stall.

Additional Context

Fix: return early when properties == null (a retry topic message without properties can't be mapped back to an origin group, so there is nothing to wake up), consistent with the existing guards in PullRequestHoldService and notifyMessageArrive4MultiQueue. I will submit a PR with a regression test.

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