{"record":{"id":"c072c9d32d666ebf","repo":"apache/rocketmq","slug":"cannot-accept-equal-epoch-with-null-data","errorCode":null,"errorMessage":"Cannot accept equal epoch with null data","messagePattern":"Cannot accept equal epoch with null data","errorType":"validation","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"broker/src/main/java/org/apache/rocketmq/broker/topic/TopicQueueMappingManager.java","lineNumber":117,"sourceCode":"                });\n                topicQueueMappingTable.put(newDetail.getTopic(), newDetail);\n                updated = true;\n                return;\n            }\n            //do more check\n            if (newDetail.getEpoch() < oldDetail.getEpoch()) {\n                throw new RuntimeException(String.format(\"Can't accept data with small epoch %d < %d\", newDetail.getEpoch(), oldDetail.getEpoch()));\n            }\n            if (!newDetail.getScope().equals(oldDetail.getScope())) {\n                throw new RuntimeException(String.format(\"Can't accept data with unmatched scope %s != %s\", newDetail.getScope(), oldDetail.getScope()));\n            }\n            boolean epochEqual = newDetail.getEpoch() == oldDetail.getEpoch();\n            for (Integer globalId : oldDetail.getHostedQueues().keySet()) {\n                List<LogicQueueMappingItem> oldItems = oldDetail.getHostedQueues().get(globalId);\n                List<LogicQueueMappingItem> newItems = newDetail.getHostedQueues().get(globalId);\n                if (newItems == null) {\n                    if (epochEqual) {\n                        throw new RuntimeException(\"Cannot accept equal epoch with null data\");\n                    } else {\n                        newDetail.getHostedQueues().put(globalId, oldItems);\n                    }\n                } else {\n                    TopicQueueMappingUtils.makeSureLogicQueueMappingItemImmutable(oldItems, newItems, epochEqual, isClean);\n                }\n            }\n            topicQueueMappingTable.put(newDetail.getTopic(), newDetail);\n            updated = true;\n        }  finally {\n            if (locked) {\n                this.lock.unlock();\n            }\n            if (updated && flush) {\n                this.dataVersion.nextVersion();\n                this.persist();\n                log.info(\"Update topic queue mapping from [{}] to [{}], force {}\", oldDetail, newDetail, force);\n            }","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/apache/rocketmq/blob/293f5885719fc4aa3619446a1900f58ccfcfdd29/broker/src/main/java/org/apache/rocketmq/broker/topic/TopicQueueMappingManager.java#L99-L135","documentation":"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).","triggerScenarios":"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.","commonSituations":"Client retries an update after trimming the mapping; serialization dropping empty lists to null; hand-edited mapping files missing queue entries.","solutions":["Bump the epoch when the queue set genuinely changes, so omitted ids are inherited rather than rejected.","If it is a pure retry, resend the COMPLETE mapping (all hosted queues) with the same epoch.","Fix tooling that nulls-out empty or unchanged queue lists before submission."],"exampleFix":"// before: same epoch, queue 1 missing -> rejected\nnewDetail.setEpoch(5); hostedQueues: {0: [...]}  // old had {0:[...], 1:[...]}\n\n// after: bump epoch for a real change\nnewDetail.setEpoch(6); hostedQueues: {0: [...]}  // queue 1 inherited from old detail","handlingStrategy":"validation","validationCode":"TopicQueueMappingDetail old = manager.queryTopicQueueMapping(topic);\nboolean epochEqual = old != null && newDetail.getEpoch() == old.getEpoch();\nif (epochEqual) {\n    for (Integer id : old.getHostedQueues().keySet()) {\n        if (!newDetail.getHostedQueues().containsKey(id)) {\n            newDetail.getHostedQueues().put(id, old.getHostedQueues().get(id)); // complete the detail\n        }\n    }\n}","typeGuard":"boolean isCompleteForEqualEpoch(TopicQueueMappingDetail oldD, TopicQueueMappingDetail newD) {\n    return oldD == null || newD.getEpoch() > oldD.getEpoch()\n        || newD.getHostedQueues().keySet().containsAll(oldD.getHostedQueues().keySet());\n}","tryCatchPattern":"catch (RuntimeException e) { if (e.getMessage().contains(\"equal epoch with null data\")) { completeMissingQueuesFromCurrent(); resubmit(); } else throw e; }","preventionTips":["Bump the epoch whenever the hosted queue set changes.","Same-epoch retries must resend the full mapping, every queue id included."],"tags":["rocketmq","broker","topic-queue-mapping","static-topic","epoch","controller","metadata"],"backgroundTag":null,"analyzedSha":"293f5885719fc4aa3619446a1900f58ccfcfdd29","analyzedAt":"2026-08-14T11:50:13.822Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}