apache/rocketmq · error · RuntimeException

Cannot accept equal epoch with null data

Error message

Cannot accept equal epoch with null data

What it means

Non-force update with epoch EQUAL to the stored one must contain data for every global queue id the old mapping hosted; if newItems is null for an existing globalId while epochs are equal, it throws RuntimeException. Equal-epoch updates are treated as re-submissions and must be complete, whereas a greater epoch may omit ids (old items are inherited).

Solutions

  1. Bump the epoch when the queue set genuinely changes, so omitted ids are inherited rather than rejected.
  2. If it is a pure retry, resend the COMPLETE mapping (all hosted queues) with the same epoch.
  3. Fix tooling that nulls-out empty or unchanged queue lists before submission.

Example fix

// before: same epoch, queue 1 missing -> rejected
newDetail.setEpoch(5); hostedQueues: {0: [...]}  // old had {0:[...], 1:[...]}

// after: bump epoch for a real change
newDetail.setEpoch(6); hostedQueues: {0: [...]}  // queue 1 inherited from old detail
Defensive patterns

Strategy: validation

Validate before calling

TopicQueueMappingDetail old = manager.queryTopicQueueMapping(topic);
boolean epochEqual = old != null && newDetail.getEpoch() == old.getEpoch();
if (epochEqual) {
    for (Integer id : old.getHostedQueues().keySet()) {
        if (!newDetail.getHostedQueues().containsKey(id)) {
            newDetail.getHostedQueues().put(id, old.getHostedQueues().get(id)); // complete the detail
        }
    }
}

Type guard

boolean isCompleteForEqualEpoch(TopicQueueMappingDetail oldD, TopicQueueMappingDetail newD) {
    return oldD == null || newD.getEpoch() > oldD.getEpoch()
        || newD.getHostedQueues().keySet().containsAll(oldD.getHostedQueues().keySet());
}

Try / catch

catch (RuntimeException e) { if (e.getMessage().contains("equal epoch with null data")) { completeMissingQueuesFromCurrent(); resubmit(); } else throw e; }

Prevention

When it happens

Trigger: Retransmitting a mapping detail with the same epoch but with some hosted-queues entries removed/null; partial mapping JSON built by a tool dropping queues.

Common situations: Client retries an update after trimming the mapping; serialization dropping empty lists to null; hand-edited mapping files missing queue entries.

Related errors


AI-assisted analysis of apache/rocketmq@293f588571 (2026-08-14). Data as JSON: /api/errors/c072c9d32d666ebf. Report an issue: GitHub.

Appendix: source

Thrown at broker/src/main/java/org/apache/rocketmq/broker/topic/TopicQueueMappingManager.java:117

                });
                topicQueueMappingTable.put(newDetail.getTopic(), newDetail);
                updated = true;
                return;
            }
            //do more check
            if (newDetail.getEpoch() < oldDetail.getEpoch()) {
                throw new RuntimeException(String.format("Can't accept data with small epoch %d < %d", newDetail.getEpoch(), oldDetail.getEpoch()));
            }
            if (!newDetail.getScope().equals(oldDetail.getScope())) {
                throw new RuntimeException(String.format("Can't accept data with unmatched scope %s != %s", newDetail.getScope(), oldDetail.getScope()));
            }
            boolean epochEqual = newDetail.getEpoch() == oldDetail.getEpoch();
            for (Integer globalId : oldDetail.getHostedQueues().keySet()) {
                List<LogicQueueMappingItem> oldItems = oldDetail.getHostedQueues().get(globalId);
                List<LogicQueueMappingItem> newItems = newDetail.getHostedQueues().get(globalId);
                if (newItems == null) {
                    if (epochEqual) {
                        throw new RuntimeException("Cannot accept equal epoch with null data");
                    } else {
                        newDetail.getHostedQueues().put(globalId, oldItems);
                    }
                } else {
                    TopicQueueMappingUtils.makeSureLogicQueueMappingItemImmutable(oldItems, newItems, epochEqual, isClean);
                }
            }
            topicQueueMappingTable.put(newDetail.getTopic(), newDetail);
            updated = true;
        }  finally {
            if (locked) {
                this.lock.unlock();
            }
            if (updated && flush) {
                this.dataVersion.nextVersion();
                this.persist();
                log.info("Update topic queue mapping from [{}] to [{}], force {}", oldDetail, newDetail, force);
            }

View on GitHub (pinned to 293f588571)